mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 12:05:27 +08:00
feat(ql3): harden cluster secret projection checks
This commit is contained in:
@@ -250,6 +250,11 @@
|
|||||||
"require": "./dist/plugin-package/management/pluginPackageManagement.js",
|
"require": "./dist/plugin-package/management/pluginPackageManagement.js",
|
||||||
"default": "./dist/plugin-package/management/pluginPackageManagement.js"
|
"default": "./dist/plugin-package/management/pluginPackageManagement.js"
|
||||||
},
|
},
|
||||||
|
"./plugin-package-secret-existence-inspector": {
|
||||||
|
"types": "./dist/plugin-package/secret-binding/projectedSecretExistenceInspector.d.ts",
|
||||||
|
"require": "./dist/plugin-package/secret-binding/projectedSecretExistenceInspector.js",
|
||||||
|
"default": "./dist/plugin-package/secret-binding/projectedSecretExistenceInspector.js"
|
||||||
|
},
|
||||||
"./plugin-package-lifecycle-management": {
|
"./plugin-package-lifecycle-management": {
|
||||||
"types": "./dist/plugin-package/lifecycle/pluginPackageLifecycleManagement.d.ts",
|
"types": "./dist/plugin-package/lifecycle/pluginPackageLifecycleManagement.d.ts",
|
||||||
"require": "./dist/plugin-package/lifecycle/pluginPackageLifecycleManagement.js",
|
"require": "./dist/plugin-package/lifecycle/pluginPackageLifecycleManagement.js",
|
||||||
|
|||||||
+138
@@ -0,0 +1,138 @@
|
|||||||
|
import { lstat, realpath, stat } from 'node:fs/promises';
|
||||||
|
import { isAbsolute, join, normalize, parse, relative } from 'node:path';
|
||||||
|
|
||||||
|
import { secretProjectionFileName } from '@qinglong/runtime-core/secret-projection';
|
||||||
|
import { parseSecretRef } from '@qinglong/runtime-core/secret-reference';
|
||||||
|
|
||||||
|
const MAX_SECRET_ROOT_BYTES = 4096;
|
||||||
|
|
||||||
|
export interface PluginPackageSecretExistenceInspector {
|
||||||
|
assertExists(secretRefs: readonly string[]): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectedPluginPackageSecretExistenceInspectorOptions {
|
||||||
|
readonly rootDirectory: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProjectedPluginPackageSecretExistenceError extends Error {
|
||||||
|
readonly code = 'QL3_PROJECTED_PLUGIN_PACKAGE_SECRET_UNAVAILABLE';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly reason:
|
||||||
|
| 'invalid_configuration'
|
||||||
|
| 'root_unavailable'
|
||||||
|
| 'reference_unavailable',
|
||||||
|
options?: ErrorOptions,
|
||||||
|
) {
|
||||||
|
super(`Projected Plugin Package Secret failed: ${reason}`, options);
|
||||||
|
this.name = 'ProjectedPluginPackageSecretExistenceError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rootDirectory(value: string): string {
|
||||||
|
if (
|
||||||
|
typeof value !== 'string' ||
|
||||||
|
!isAbsolute(value) ||
|
||||||
|
parse(value).root === value ||
|
||||||
|
normalize(value) !== value ||
|
||||||
|
value.includes('\0') ||
|
||||||
|
Buffer.byteLength(value, 'utf8') > MAX_SECRET_ROOT_BYTES
|
||||||
|
) {
|
||||||
|
throw new ProjectedPluginPackageSecretExistenceError(
|
||||||
|
'invalid_configuration',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function remainsBelow(root: string, candidate: string): boolean {
|
||||||
|
const suffix = relative(root, candidate);
|
||||||
|
return (
|
||||||
|
suffix.length > 0 &&
|
||||||
|
!isAbsolute(suffix) &&
|
||||||
|
suffix !== '..' &&
|
||||||
|
!suffix.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolvedRoot(path: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const configured = await lstat(path);
|
||||||
|
if (!configured.isDirectory() || configured.isSymbolicLink()) {
|
||||||
|
throw new Error('root is not a direct directory');
|
||||||
|
}
|
||||||
|
return await realpath(path);
|
||||||
|
} catch (error) {
|
||||||
|
throw new ProjectedPluginPackageSecretExistenceError('root_unavailable', {
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertProjectedReference(
|
||||||
|
root: string,
|
||||||
|
secretRef: string,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const reference = parseSecretRef(secretRef);
|
||||||
|
if (reference.version === undefined) {
|
||||||
|
throw new Error('projected Secret reference is not versioned');
|
||||||
|
}
|
||||||
|
const candidate = join(root, secretProjectionFileName(secretRef));
|
||||||
|
const target = await realpath(candidate);
|
||||||
|
if (!remainsBelow(root, target)) {
|
||||||
|
throw new Error('projected Secret escaped its root');
|
||||||
|
}
|
||||||
|
const metadata = await stat(target);
|
||||||
|
if (
|
||||||
|
!metadata.isFile() ||
|
||||||
|
metadata.nlink !== 1 ||
|
||||||
|
metadata.size < 0 ||
|
||||||
|
(metadata.mode & 0o111) !== 0 ||
|
||||||
|
(await realpath(candidate)) !== target
|
||||||
|
) {
|
||||||
|
throw new Error('projected Secret metadata is unsafe');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new ProjectedPluginPackageSecretExistenceError(
|
||||||
|
'reference_unavailable',
|
||||||
|
{ cause: error },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata-only existence proof for the short-lived Package executor. It never
|
||||||
|
* opens or reads projected Secret material and retains no cache or watcher.
|
||||||
|
*/
|
||||||
|
export class ProjectedPluginPackageSecretExistenceInspector
|
||||||
|
implements PluginPackageSecretExistenceInspector
|
||||||
|
{
|
||||||
|
private readonly rootDirectory: string;
|
||||||
|
|
||||||
|
constructor(options: ProjectedPluginPackageSecretExistenceInspectorOptions) {
|
||||||
|
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
||||||
|
throw new ProjectedPluginPackageSecretExistenceError(
|
||||||
|
'invalid_configuration',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.rootDirectory = rootDirectory(options.rootDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
async assertExists(secretRefs: readonly string[]): Promise<void> {
|
||||||
|
if (
|
||||||
|
!Array.isArray(secretRefs) ||
|
||||||
|
secretRefs.length < 1 ||
|
||||||
|
secretRefs.length > 64 ||
|
||||||
|
new Set(secretRefs).size !== secretRefs.length
|
||||||
|
) {
|
||||||
|
throw new ProjectedPluginPackageSecretExistenceError(
|
||||||
|
'reference_unavailable',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const root = await resolvedRoot(this.rootDirectory);
|
||||||
|
for (const secretRef of secretRefs) {
|
||||||
|
await assertProjectedReference(root, secretRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+96
@@ -0,0 +1,96 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const {
|
||||||
|
chmod,
|
||||||
|
mkdir,
|
||||||
|
mkdtemp,
|
||||||
|
rm,
|
||||||
|
symlink,
|
||||||
|
writeFile,
|
||||||
|
} = require('node:fs/promises');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { test } = require('node:test');
|
||||||
|
|
||||||
|
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
|
||||||
|
const {
|
||||||
|
secretProjectionFileName,
|
||||||
|
} = require('@qinglong/runtime-core/secret-projection');
|
||||||
|
const {
|
||||||
|
ProjectedPluginPackageSecretExistenceError,
|
||||||
|
ProjectedPluginPackageSecretExistenceInspector,
|
||||||
|
} = require('@qinglong/cluster-admin/plugin-package-secret-existence-inspector');
|
||||||
|
|
||||||
|
const SECRET_REF = createSecretRef({
|
||||||
|
projectId: 'project-1',
|
||||||
|
name: 'api-token',
|
||||||
|
version: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
test('proves an exact projected Secret without reading its bytes', async (t) => {
|
||||||
|
const root = await mkdtemp(path.join(os.tmpdir(), 'ql3-secret-inspect-'));
|
||||||
|
t.after(() => rm(root, { recursive: true, force: true }));
|
||||||
|
await chmod(root, 0o700);
|
||||||
|
const file = path.join(root, secretProjectionFileName(SECRET_REF));
|
||||||
|
await writeFile(file, 'unreadable-to-executor', { mode: 0o000 });
|
||||||
|
|
||||||
|
const inspector = new ProjectedPluginPackageSecretExistenceInspector({
|
||||||
|
rootDirectory: root,
|
||||||
|
});
|
||||||
|
await inspector.assertExists([SECRET_REF]);
|
||||||
|
await assert.rejects(
|
||||||
|
inspector.assertExists([
|
||||||
|
createSecretRef({
|
||||||
|
projectId: 'project-1',
|
||||||
|
name: 'missing',
|
||||||
|
version: 1,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
(error) =>
|
||||||
|
error instanceof ProjectedPluginPackageSecretExistenceError &&
|
||||||
|
error.reason === 'reference_unavailable',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepts an in-root projection symlink and rejects an escape', async (t) => {
|
||||||
|
const root = await mkdtemp(path.join(os.tmpdir(), 'ql3-secret-project-'));
|
||||||
|
const outside = await mkdtemp(path.join(os.tmpdir(), 'ql3-secret-outside-'));
|
||||||
|
t.after(async () => {
|
||||||
|
await rm(root, { recursive: true, force: true });
|
||||||
|
await rm(outside, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
await chmod(root, 0o700);
|
||||||
|
await chmod(outside, 0o700);
|
||||||
|
const name = secretProjectionFileName(SECRET_REF);
|
||||||
|
const generation = path.join(root, '..data-v1');
|
||||||
|
await mkdir(generation, { mode: 0o700 });
|
||||||
|
await writeFile(path.join(generation, name), 'value', { mode: 0o000 });
|
||||||
|
await symlink(path.join('..data-v1', name), path.join(root, name));
|
||||||
|
|
||||||
|
const inspector = new ProjectedPluginPackageSecretExistenceInspector({
|
||||||
|
rootDirectory: root,
|
||||||
|
});
|
||||||
|
await inspector.assertExists([SECRET_REF]);
|
||||||
|
|
||||||
|
await rm(path.join(root, name));
|
||||||
|
await writeFile(path.join(outside, name), 'value', { mode: 0o000 });
|
||||||
|
await symlink(path.join(outside, name), path.join(root, name));
|
||||||
|
await assert.rejects(inspector.assertExists([SECRET_REF]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects duplicate, unversioned and noncanonical references', async (t) => {
|
||||||
|
const root = await mkdtemp(path.join(os.tmpdir(), 'ql3-secret-invalid-'));
|
||||||
|
t.after(() => rm(root, { recursive: true, force: true }));
|
||||||
|
await chmod(root, 0o700);
|
||||||
|
const inspector = new ProjectedPluginPackageSecretExistenceInspector({
|
||||||
|
rootDirectory: root,
|
||||||
|
});
|
||||||
|
await assert.rejects(inspector.assertExists([SECRET_REF, SECRET_REF]));
|
||||||
|
await assert.rejects(
|
||||||
|
inspector.assertExists([
|
||||||
|
createSecretRef({ projectId: 'project-1', name: 'unversioned' }),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
await assert.rejects(inspector.assertExists(['not-a-secret-ref']));
|
||||||
|
});
|
||||||
@@ -1,18 +1,7 @@
|
|||||||
// Remote Execution owns mounted Secret resolution for authenticated delivery.
|
// Remote Execution owns mounted Secret resolution for authenticated delivery.
|
||||||
import { createHash } from 'node:crypto';
|
|
||||||
import { constants } from 'node:fs';
|
import { constants } from 'node:fs';
|
||||||
import {
|
import { lstat, open, realpath } from 'node:fs/promises';
|
||||||
lstat,
|
import { isAbsolute, join, normalize, parse, relative } from 'node:path';
|
||||||
open,
|
|
||||||
realpath,
|
|
||||||
} from 'node:fs/promises';
|
|
||||||
import {
|
|
||||||
isAbsolute,
|
|
||||||
join,
|
|
||||||
normalize,
|
|
||||||
parse,
|
|
||||||
relative,
|
|
||||||
} from 'node:path';
|
|
||||||
import {
|
import {
|
||||||
MAX_REMOTE_SECRET_DELIVERY_TOTAL_VALUE_BYTES,
|
MAX_REMOTE_SECRET_DELIVERY_TOTAL_VALUE_BYTES,
|
||||||
MAX_REMOTE_SECRET_VALUE_BYTES,
|
MAX_REMOTE_SECRET_VALUE_BYTES,
|
||||||
@@ -21,10 +10,9 @@ import {
|
|||||||
type RemoteWorkerSecretResolution,
|
type RemoteWorkerSecretResolution,
|
||||||
type RemoteWorkerSecretValueProvider,
|
type RemoteWorkerSecretValueProvider,
|
||||||
} from '@qinglong/runtime-core/remote-secret-delivery';
|
} from '@qinglong/runtime-core/remote-secret-delivery';
|
||||||
import { parseSecretRef } from '@qinglong/runtime-core/secret-reference';
|
import { secretProjectionFileName } from '@qinglong/runtime-core/secret-projection';
|
||||||
|
|
||||||
const MAX_SECRET_ROOT_BYTES = 4096;
|
const MAX_SECRET_ROOT_BYTES = 4096;
|
||||||
const SECRET_FILE_NAME = /^[0-9a-f]{64}$/;
|
|
||||||
|
|
||||||
export interface ClusterMountedSecretProviderOptions {
|
export interface ClusterMountedSecretProviderOptions {
|
||||||
/**
|
/**
|
||||||
@@ -69,25 +57,13 @@ function rootDirectory(value: string): string {
|
|||||||
* non-reversible name also prevents Project/name input from becoming a path.
|
* non-reversible name also prevents Project/name input from becoming a path.
|
||||||
*/
|
*/
|
||||||
export function clusterMountedSecretFileName(secretRef: string): string {
|
export function clusterMountedSecretFileName(secretRef: string): string {
|
||||||
let canonical: string;
|
|
||||||
try {
|
try {
|
||||||
const parsed = parseSecretRef(secretRef);
|
return secretProjectionFileName(secretRef);
|
||||||
canonical = secretRef;
|
|
||||||
if (
|
|
||||||
parsed.projectId.length < 1 ||
|
|
||||||
parsed.name.length < 1
|
|
||||||
) throw new Error('invalid SecretRef');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new ClusterMountedSecretProviderError(
|
throw new ClusterMountedSecretProviderError('invalid_configuration', {
|
||||||
'invalid_configuration',
|
cause: error,
|
||||||
{ cause: error },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const name = createHash('sha256').update(canonical, 'utf8').digest('hex');
|
|
||||||
if (!SECRET_FILE_NAME.test(name)) {
|
|
||||||
throw new ClusterMountedSecretProviderError('invalid_configuration');
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function remainsBelow(root: string, candidate: string): boolean {
|
function remainsBelow(root: string, candidate: string): boolean {
|
||||||
@@ -108,17 +84,13 @@ async function resolvedRoot(path: string): Promise<string> {
|
|||||||
}
|
}
|
||||||
return await realpath(path);
|
return await realpath(path);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new ClusterMountedSecretProviderError(
|
throw new ClusterMountedSecretProviderError('root_unavailable', {
|
||||||
'root_unavailable',
|
cause: error,
|
||||||
{ cause: error },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readMaterial(
|
async function readMaterial(root: string, secretRef: string): Promise<Buffer> {
|
||||||
root: string,
|
|
||||||
secretRef: string,
|
|
||||||
): Promise<Buffer> {
|
|
||||||
const candidate = join(root, clusterMountedSecretFileName(secretRef));
|
const candidate = join(root, clusterMountedSecretFileName(secretRef));
|
||||||
let handle;
|
let handle;
|
||||||
try {
|
try {
|
||||||
@@ -152,10 +124,9 @@ async function readMaterial(
|
|||||||
}
|
}
|
||||||
return bytes;
|
return bytes;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new ClusterMountedSecretProviderError(
|
throw new ClusterMountedSecretProviderError('material_unavailable', {
|
||||||
'material_unavailable',
|
cause: error,
|
||||||
{ cause: error },
|
});
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
await handle?.close().catch(() => undefined);
|
await handle?.close().catch(() => undefined);
|
||||||
}
|
}
|
||||||
@@ -169,10 +140,9 @@ function secretValue(bytes: Buffer): string {
|
|||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new ClusterMountedSecretProviderError(
|
throw new ClusterMountedSecretProviderError('material_unavailable', {
|
||||||
'material_unavailable',
|
cause: error,
|
||||||
{ cause: error },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,10 +175,9 @@ export class ClusterMountedSecretProvider
|
|||||||
try {
|
try {
|
||||||
normalized = normalizeRemoteWorkerSecretDeliveryAuthority(authority);
|
normalized = normalizeRemoteWorkerSecretDeliveryAuthority(authority);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new ClusterMountedSecretProviderError(
|
throw new ClusterMountedSecretProviderError('material_unavailable', {
|
||||||
'material_unavailable',
|
cause: error,
|
||||||
{ cause: error },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const root = await resolvedRoot(this.rootDirectory);
|
const root = await resolvedRoot(this.rootDirectory);
|
||||||
const buffers: Buffer[] = [];
|
const buffers: Buffer[] = [];
|
||||||
@@ -220,9 +189,7 @@ export class ClusterMountedSecretProvider
|
|||||||
buffers.push(bytes);
|
buffers.push(bytes);
|
||||||
totalBytes += bytes.byteLength;
|
totalBytes += bytes.byteLength;
|
||||||
if (totalBytes > MAX_REMOTE_SECRET_DELIVERY_TOTAL_VALUE_BYTES) {
|
if (totalBytes > MAX_REMOTE_SECRET_DELIVERY_TOTAL_VALUE_BYTES) {
|
||||||
throw new ClusterMountedSecretProviderError(
|
throw new ClusterMountedSecretProviderError('material_unavailable');
|
||||||
'material_unavailable',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
values.push(
|
values.push(
|
||||||
Object.freeze({
|
Object.freeze({
|
||||||
@@ -243,10 +210,9 @@ export class ClusterMountedSecretProvider
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
for (const bytes of buffers) bytes.fill(0);
|
for (const bytes of buffers) bytes.fill(0);
|
||||||
if (error instanceof ClusterMountedSecretProviderError) throw error;
|
if (error instanceof ClusterMountedSecretProviderError) throw error;
|
||||||
throw new ClusterMountedSecretProviderError(
|
throw new ClusterMountedSecretProviderError('material_unavailable', {
|
||||||
'material_unavailable',
|
cause: error,
|
||||||
{ cause: error },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,9 @@
|
|||||||
"plugin-package-secret-binding-plan": [
|
"plugin-package-secret-binding-plan": [
|
||||||
"dist/plugin-package/secret-binding/plan.d.ts"
|
"dist/plugin-package/secret-binding/plan.d.ts"
|
||||||
],
|
],
|
||||||
|
"secret-projection": [
|
||||||
|
"dist/secret/secretProjection.d.ts"
|
||||||
|
],
|
||||||
"plugin-package-task-reconciliation": [
|
"plugin-package-task-reconciliation": [
|
||||||
"dist/plugin-package/pluginPackageTaskReconciliation.d.ts"
|
"dist/plugin-package/pluginPackageTaskReconciliation.d.ts"
|
||||||
],
|
],
|
||||||
@@ -387,6 +390,11 @@
|
|||||||
"require": "./dist/plugin-package/secret-binding/plan.js",
|
"require": "./dist/plugin-package/secret-binding/plan.js",
|
||||||
"default": "./dist/plugin-package/secret-binding/plan.js"
|
"default": "./dist/plugin-package/secret-binding/plan.js"
|
||||||
},
|
},
|
||||||
|
"./secret-projection": {
|
||||||
|
"types": "./dist/secret/secretProjection.d.ts",
|
||||||
|
"require": "./dist/secret/secretProjection.js",
|
||||||
|
"default": "./dist/secret/secretProjection.js"
|
||||||
|
},
|
||||||
"./plugin-package-task-reconciliation": {
|
"./plugin-package-task-reconciliation": {
|
||||||
"types": "./dist/plugin-package/pluginPackageTaskReconciliation.d.ts",
|
"types": "./dist/plugin-package/pluginPackageTaskReconciliation.d.ts",
|
||||||
"require": "./dist/plugin-package/pluginPackageTaskReconciliation.js",
|
"require": "./dist/plugin-package/pluginPackageTaskReconciliation.js",
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
|
||||||
|
import { parseSecretRef } from './secretReference';
|
||||||
|
|
||||||
|
export const SECRET_PROJECTION_FILE_NAME_PATTERN = /^[0-9a-f]{64}$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps one canonical SecretRef to a path-free, non-reversible projection key.
|
||||||
|
* The function does not inspect or resolve Secret material.
|
||||||
|
*/
|
||||||
|
export function secretProjectionFileName(secretRef: string): string {
|
||||||
|
parseSecretRef(secretRef);
|
||||||
|
const result = createHash('sha256').update(secretRef, 'utf8').digest('hex');
|
||||||
|
if (!SECRET_PROJECTION_FILE_NAME_PATTERN.test(result)) {
|
||||||
|
throw new TypeError('Secret projection file name is invalid');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -9,6 +9,9 @@ const {
|
|||||||
createLocalSecretRef,
|
createLocalSecretRef,
|
||||||
parseLocalSecretRef,
|
parseLocalSecretRef,
|
||||||
} = require('../dist/secret/localSecret');
|
} = require('../dist/secret/localSecret');
|
||||||
|
const {
|
||||||
|
secretProjectionFileName,
|
||||||
|
} = require('@qinglong/runtime-core/secret-projection');
|
||||||
|
|
||||||
test('keeps qlsecret:v1 profile-neutral and byte-compatible with local aliases', () => {
|
test('keeps qlsecret:v1 profile-neutral and byte-compatible with local aliases', () => {
|
||||||
const reference = { projectId: 'default', name: 'TOKEN', version: 2 };
|
const reference = { projectId: 'default', name: 'TOKEN', version: 2 };
|
||||||
@@ -20,12 +23,40 @@ test('keeps qlsecret:v1 profile-neutral and byte-compatible with local aliases',
|
|||||||
assert.equal(Object.isFrozen(parseSecretRef(value)), true);
|
assert.equal(Object.isFrozen(parseSecretRef(value)), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('maps only canonical SecretRefs to stable path-free projection names', () => {
|
||||||
|
const first = createSecretRef({
|
||||||
|
projectId: 'default',
|
||||||
|
name: 'TOKEN',
|
||||||
|
version: 2,
|
||||||
|
});
|
||||||
|
const second = createSecretRef({
|
||||||
|
projectId: 'default',
|
||||||
|
name: 'TOKEN',
|
||||||
|
version: 3,
|
||||||
|
});
|
||||||
|
assert.match(secretProjectionFileName(first), /^[0-9a-f]{64}$/);
|
||||||
|
assert.equal(
|
||||||
|
secretProjectionFileName(first),
|
||||||
|
secretProjectionFileName(first),
|
||||||
|
);
|
||||||
|
assert.notEqual(
|
||||||
|
secretProjectionFileName(first),
|
||||||
|
secretProjectionFileName(second),
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => secretProjectionFileName('not-a-secret-ref'),
|
||||||
|
InvalidSecretReferenceError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('rejects non-canonical, cross-shape and unbounded Secret references', () => {
|
test('rejects non-canonical, cross-shape and unbounded Secret references', () => {
|
||||||
for (const value of [
|
for (const value of [
|
||||||
'qlsecret:v1:',
|
'qlsecret:v1:',
|
||||||
'qlsecret:v1:***',
|
'qlsecret:v1:***',
|
||||||
'local-secret:default:TOKEN',
|
'local-secret:default:TOKEN',
|
||||||
`qlsecret:v1:${Buffer.from('{"name":"TOKEN","projectId":"default"}').toString('base64url')}`,
|
`qlsecret:v1:${Buffer.from(
|
||||||
|
'{"name":"TOKEN","projectId":"default"}',
|
||||||
|
).toString('base64url')}`,
|
||||||
]) {
|
]) {
|
||||||
assert.throws(() => parseSecretRef(value), InvalidSecretReferenceError);
|
assert.throws(() => parseSecretRef(value), InvalidSecretReferenceError);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user