mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 02:27:44 +08:00
feat(ql3): cut alpha.1 candidate milestone
This commit is contained in:
@@ -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')! }
|
||||
: {}),
|
||||
|
||||
+100
-10
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
+43
-11
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user