feat(ql3): cut alpha.1 candidate milestone

This commit is contained in:
whyour
2026-08-26 01:41:20 +08:00
parent c2df0c7215
commit 07cdc76bae
94 changed files with 1469 additions and 168 deletions
@@ -3,6 +3,8 @@ import type {
PostgresDatabaseResource,
} from '@qinglong/runtime-core';
import { assertApiCredentialPepper } from '@qinglong/runtime-core/api-credential-token';
import { createSingletonApiCredentialPepperKeyring } from '@qinglong/runtime-core/api-credential-pepper-keyring';
import { LEGACY_API_CREDENTIAL_PEPPER_KEY_ID } from '@qinglong/runtime-core/api-credential';
import { assertWorkerCredentialPepper } from '@qinglong/runtime-core/worker-credential-token';
import type { SecurityAuditQueryRepository } from '@qinglong/runtime-core/security-audit-query';
import {
@@ -97,7 +99,10 @@ export async function bootstrapClusterAdmin(
administration: createClusterAdministrationService(
identities,
credentials,
options.apiCredentialPepper,
createSingletonApiCredentialPepperKeyring(
options.apiCredentialPepper,
LEGACY_API_CREDENTIAL_PEPPER_KEY_ID,
).keys[0]!,
{
...(options.now ? { now: options.now } : {}),
...(options.randomBytes ? { randomBytes: options.randomBytes } : {}),
@@ -13,9 +13,11 @@ import {
formatApiCredentialToken,
} from '@qinglong/runtime-core/api-credential-token';
import {
LEGACY_API_CREDENTIAL_PEPPER_KEY_ID,
assertApiCredentialPepperKeyId,
type ApiCredentialRecord,
LEGACY_API_CREDENTIAL_PEPPER_KEY_ID,
} from '@qinglong/runtime-core/api-credential';
import type { ApiCredentialPepperKey } from '@qinglong/runtime-core/api-credential-pepper-keyring';
import {
type AppendIdentitySubjectResult,
IdentityAdministrationMutationConflictError,
@@ -283,7 +285,7 @@ function sameCredentialReplay(
export function createClusterAdministrationService(
identities: IdentityAdministrationRepository,
credentials: ApiCredentialAdministrationRepository,
pepper: string,
activePepperKeyValue: Readonly<ApiCredentialPepperKey> | string,
options: ClusterAdministrationOptions = {},
): ClusterAdministrationService {
if (
@@ -305,10 +307,28 @@ export function createClusterAdministrationService(
'credential repository is invalid',
);
}
const activePepperKey =
typeof activePepperKeyValue === 'string'
? Object.freeze({
pepperKeyId: LEGACY_API_CREDENTIAL_PEPPER_KEY_ID,
pepper: activePepperKeyValue,
})
: activePepperKeyValue;
try {
assertApiCredentialPepper(pepper);
if (
!activePepperKey ||
typeof activePepperKey !== 'object' ||
Array.isArray(activePepperKey) ||
Object.keys(activePepperKey).sort().join(',') !== 'pepper,pepperKeyId'
) {
throw new TypeError();
}
assertApiCredentialPepper(activePepperKey.pepper);
assertApiCredentialPepperKeyId(activePepperKey.pepperKeyId);
} catch {
throw new ClusterAdministrationConfigurationError('pepper is invalid');
throw new ClusterAdministrationConfigurationError(
'active pepper key is invalid',
);
}
exactObject(options, 'options');
const optionKeys = Object.keys(options);
@@ -442,7 +462,7 @@ export function createClusterAdministrationService(
try {
secretBase64Url = secret.toString('base64url');
secretDigest = apiCredentialSecretDigest(
pepper,
activePepperKey.pepper,
request.credentialId,
secretBase64Url,
);
@@ -454,7 +474,7 @@ export function createClusterAdministrationService(
const credential: ApiCredentialRecord = {
credentialId: request.credentialId,
version: request.expectedCurrentVersion + 1,
pepperKeyId: LEGACY_API_CREDENTIAL_PEPPER_KEY_ID,
pepperKeyId: activePepperKey.pepperKeyId,
state: operation === 'revoke' ? 'revoked' : 'active',
subject: request.subject,
subjectStatus: identity.status,
@@ -6,7 +6,7 @@ import {
} from './clusterAdministrationCommand';
const USAGE =
'Usage: ql3-security-admin --command=/absolute/command.json --assertion=/absolute/assertion.jwt --keyset=/absolute/keyset.json --pepper=/absolute/pepper [--delivery=/absolute/token.json]';
'Usage: ql3-security-admin --command=/absolute/command.json --assertion=/absolute/assertion.jwt --keyset=/absolute/keyset.json (--pepper=/absolute/pepper | --pepper-keyring=/absolute/keyring.json) [--delivery=/absolute/token.json]';
function argumentsFrom(argv: readonly string[]) {
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
@@ -14,9 +14,10 @@ function argumentsFrom(argv: readonly string[]) {
}
const values = new Map<string, string>();
for (const argument of argv) {
const match = /^--(command|assertion|keyset|pepper|delivery)=(\/.+)$/.exec(
argument,
);
const match =
/^--(command|assertion|keyset|pepper|pepper-keyring|delivery)=(\/.+)$/.exec(
argument,
);
if (!match || values.has(match[1]!)) {
throw new ClusterAdministrationCommandError('CLI arguments are invalid');
}
@@ -26,7 +27,7 @@ function argumentsFrom(argv: readonly string[]) {
!values.has('command') ||
!values.has('assertion') ||
!values.has('keyset') ||
!values.has('pepper')
values.has('pepper') === values.has('pepper-keyring')
) {
throw new ClusterAdministrationCommandError('CLI arguments are invalid');
}
@@ -36,7 +37,9 @@ function argumentsFrom(argv: readonly string[]) {
commandFile: values.get('command')!,
assertionFile: values.get('assertion')!,
keysetFile: values.get('keyset')!,
pepperFile: values.get('pepper')!,
...(values.has('pepper')
? { pepperFile: values.get('pepper')! }
: { pepperKeyringFile: values.get('pepper-keyring')! }),
...(values.has('delivery')
? { deliveryFile: values.get('delivery')! }
: {}),
@@ -1,7 +1,17 @@
import { createHash } from 'node:crypto';
import { basename } from 'node:path';
import { assertApiCredentialPepper } from '@qinglong/runtime-core/api-credential-token';
import {
createSingletonApiCredentialPepperKeyring,
normalizeApiCredentialPepperKeyring,
type ApiCredentialPepperKeyring,
} from '@qinglong/runtime-core/api-credential-pepper-keyring';
import { LEGACY_API_CREDENTIAL_PEPPER_KEY_ID } from '@qinglong/runtime-core/api-credential';
import {
normalizeApiCredentialPepperReferenceKeyId,
normalizeApiCredentialPepperReferenceLimit,
type ApiCredentialPepperReferenceRepository,
} from '@qinglong/runtime-core/api-credential-pepper-reference';
import { normalizeIdentityAdministrationSubject } from '@qinglong/runtime-core/identity-administration';
import {
normalizeSecurityAuditQuery,
@@ -34,6 +44,7 @@ const MAX_VERSION = 2_147_483_646;
const MAX_COMMAND_BYTES = 64 * 1024;
const MAX_ASSERTION_BYTES = 16 * 1024;
const MAX_PEPPER_BYTES = 256;
const MAX_PEPPER_KEYRING_BYTES = 2 * 1024;
export type ClusterAdministrationCommandOperation =
| 'identity.register'
@@ -42,6 +53,7 @@ export type ClusterAdministrationCommandOperation =
| 'credential.issue'
| 'credential.rotate'
| 'credential.revoke'
| 'pepper.references'
| 'audit.list';
interface BaseMutationRequest {
@@ -79,16 +91,27 @@ interface AuditCommand {
readonly request: SecurityAuditQuery;
}
interface PepperReferenceCommand {
readonly schemaVersion: 1;
readonly operation: 'pepper.references';
readonly request: Readonly<{
readonly pepperKeyId: string;
readonly limit: number;
}>;
}
export type ClusterAdministrationCommand =
| IdentityCommand
| CredentialCommand
| PepperReferenceCommand
| AuditCommand;
export interface ClusterAdministrationCommandPaths {
readonly commandFile: string;
readonly assertionFile: string;
readonly keysetFile: string;
readonly pepperFile: string;
readonly pepperFile?: string;
readonly pepperKeyringFile?: string;
readonly deliveryFile?: string;
}
@@ -115,18 +138,27 @@ export type ClusterAdministrationCommandResult =
schemaVersion: 1;
operation: 'audit.list';
page: Readonly<SecurityAuditQueryPage>;
}>
| Readonly<{
schemaVersion: 1;
operation: 'pepper.references';
pepperKeyId: string;
observedAtMs: number;
credentialIds: readonly string[];
hasMore: boolean;
}>;
export interface ClusterAdministrationCommandAuthority {
readonly administration: ClusterAdministrationService;
readonly audit: SecurityAuditQueryRepository;
readonly pepperReferences: ApiCredentialPepperReferenceRepository;
close(): Promise<void>;
}
export interface ClusterAdministrationCommandDependencies {
readonly openAuthority: (
environment: Readonly<Record<string, string | undefined>>,
pepper: string,
pepperKeyring: Readonly<ApiCredentialPepperKeyring>,
) => Promise<Readonly<ClusterAdministrationCommandAuthority>>;
readonly authenticate: (
keysetFile: string,
@@ -243,6 +275,7 @@ export function normalizeClusterAdministrationCommand(
'credential.issue',
'credential.rotate',
'credential.revoke',
'pepper.references',
'audit.list',
];
if (
@@ -257,6 +290,28 @@ export function normalizeClusterAdministrationCommand(
);
}
const operation = value.operation as ClusterAdministrationCommandOperation;
if (operation === 'pepper.references') {
exactObject(value.request, ['limit', 'pepperKeyId'], 'pepper reference');
try {
return Object.freeze({
schemaVersion: 1 as const,
operation,
request: Object.freeze({
pepperKeyId: normalizeApiCredentialPepperReferenceKeyId(
value.request.pepperKeyId as string,
),
limit: normalizeApiCredentialPepperReferenceLimit(
value.request.limit as number,
),
}),
});
} catch (error) {
throw new ClusterAdministrationCommandError(
'pepper reference query is invalid',
error,
);
}
}
if (operation === 'audit.list') {
let request: Readonly<SecurityAuditQuery>;
try {
@@ -370,9 +425,11 @@ export function createClusterAdministrationCommandRunner(
MAX_ASSERTION_BYTES,
true,
);
const pepperBytes = dependencies.readFile(
paths.pepperFile,
MAX_PEPPER_BYTES,
const pepperKeyringBytes = dependencies.readFile(
paths.pepperKeyringFile ?? paths.pepperFile!,
paths.pepperKeyringFile === undefined
? MAX_PEPPER_BYTES
: MAX_PEPPER_KEYRING_BYTES,
true,
);
let authority:
@@ -380,13 +437,46 @@ export function createClusterAdministrationCommandRunner(
| undefined;
try {
const assertion = strictUtf8(assertionBytes, 'assertion file').trim();
const pepper = strictUtf8(pepperBytes, 'pepper file').trim();
assertApiCredentialPepper(pepper);
let pepperKeyring: Readonly<ApiCredentialPepperKeyring>;
if (paths.pepperKeyringFile === undefined) {
pepperKeyring = createSingletonApiCredentialPepperKeyring(
strictUtf8(pepperKeyringBytes, 'pepper file').trim(),
LEGACY_API_CREDENTIAL_PEPPER_KEY_ID,
);
} else {
try {
pepperKeyring = normalizeApiCredentialPepperKeyring(
JSON.parse(
strictUtf8(pepperKeyringBytes, 'pepper keyring file'),
),
);
} catch (error) {
throw new ClusterAdministrationCommandError(
'pepper keyring file is invalid',
error,
);
}
}
const principal = await dependencies.authenticate(
paths.keysetFile,
assertion,
);
authority = await dependencies.openAuthority(environment, pepper);
authority = await dependencies.openAuthority(
environment,
pepperKeyring,
);
if (command.operation === 'pepper.references') {
void principal;
const inspection = await authority.pepperReferences.inspect(
command.request.pepperKeyId,
command.request.limit,
);
return Object.freeze({
schemaVersion: 1 as const,
operation: command.operation,
...inspection,
});
}
if (command.operation === 'audit.list') {
// Successful verification is the short-lived admin admission. Audit
// queries remain read-only and use the repository's bounded contract.
@@ -460,7 +550,7 @@ export function createClusterAdministrationCommandRunner(
});
} finally {
assertionBytes.fill(0);
pepperBytes.fill(0);
pepperKeyringBytes.fill(0);
await authority?.close();
}
},
@@ -20,9 +20,13 @@ import {
resolve,
} from 'node:path';
import { assertApiCredentialPepper } from '@qinglong/runtime-core/api-credential-token';
import {
activeApiCredentialPepperKey,
type ApiCredentialPepperKeyring,
} from '@qinglong/runtime-core/api-credential-pepper-keyring';
import {
PostgresApiCredentialAdministrationRepository,
PostgresApiCredentialPepperReferenceRepository,
PostgresIdentityAdministrationRepository,
PostgresSecurityAuditQueryRepository,
assertPostgresAdminSchemaReady,
@@ -249,9 +253,9 @@ function defaultDatabaseOpener(
async function openDefaultAuthority(
environment: Readonly<Record<string, string | undefined>>,
pepper: string,
pepperKeyring: Readonly<ApiCredentialPepperKeyring>,
): Promise<Readonly<ClusterAdministrationCommandAuthority>> {
assertApiCredentialPepper(pepper);
const activePepperKey = activeApiCredentialPepperKey(pepperKeyring);
const database = await defaultDatabaseOpener(environment)();
let closePromise: Promise<void> | undefined;
const close = (): Promise<void> => {
@@ -264,9 +268,12 @@ async function openDefaultAuthority(
administration: createClusterAdministrationService(
new PostgresIdentityAdministrationRepository(database.pool),
new PostgresApiCredentialAdministrationRepository(database.pool),
pepper,
activePepperKey,
),
audit: new PostgresSecurityAuditQueryRepository(database.pool),
pepperReferences: new PostgresApiCredentialPepperReferenceRepository(
database.pool,
),
close,
});
} catch (error) {
@@ -388,11 +395,20 @@ export function normalizeClusterAdministrationCommandPaths(
'command paths must be an object',
);
}
const pepperPathKeys = [
value.pepperFile === undefined ? null : 'pepperFile',
value.pepperKeyringFile === undefined ? null : 'pepperKeyringFile',
].filter((key): key is string => key !== null);
if (pepperPathKeys.length !== 1) {
throw new ClusterAdministrationCommandError(
'exactly one pepper source is required',
);
}
const expected = [
'assertionFile',
'commandFile',
'keysetFile',
'pepperFile',
pepperPathKeys[0]!,
...(requiresDelivery ? ['deliveryFile'] : []),
].sort();
const actual = Object.keys(value).sort();
@@ -417,10 +433,19 @@ export function normalizeClusterAdministrationCommandPaths(
value.keysetFile,
'identity keyset file',
),
pepperFile: boundedClusterAdministrationFile(
value.pepperFile,
'pepper file',
),
...(value.pepperFile === undefined
? {
pepperKeyringFile: boundedClusterAdministrationFile(
value.pepperKeyringFile,
'pepper keyring file',
),
}
: {
pepperFile: boundedClusterAdministrationFile(
value.pepperFile,
'pepper file',
),
}),
...(requiresDelivery
? {
deliveryFile: boundedClusterAdministrationFile(
@@ -441,11 +466,18 @@ export function clusterAdministrationCommandFileBeforeAdmission(
);
}
const candidate = value as Record<string, unknown>;
const required = ['assertionFile', 'commandFile', 'keysetFile', 'pepperFile'];
const required = ['assertionFile', 'commandFile', 'keysetFile'];
const pepperSources = ['pepperFile', 'pepperKeyringFile'].filter((key) =>
Object.hasOwn(candidate, key),
);
if (
required.some((key) => !Object.hasOwn(candidate, key)) ||
pepperSources.length !== 1 ||
Object.keys(candidate).some(
(key) => !required.includes(key) && key !== 'deliveryFile',
(key) =>
!required.includes(key) &&
!pepperSources.includes(key) &&
key !== 'deliveryFile',
)
) {
throw new ClusterAdministrationCommandError(