mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 12:05:27 +08:00
158 lines
4.5 KiB
TypeScript
158 lines
4.5 KiB
TypeScript
/** Least-privilege Kubernetes Secret authority assembly boundary. */
|
|
import {
|
|
ClusterPromptOutputKubernetesSecretKeyring,
|
|
type ClusterPromptOutputKubernetesSecretApi,
|
|
type ClusterPromptOutputKubernetesSecretKeyringOptions,
|
|
} from './promptOutputKubernetesSecretKeyring';
|
|
|
|
interface AccessReviewAttributes {
|
|
readonly namespace?: string;
|
|
readonly verb: string;
|
|
readonly resource: string;
|
|
readonly name?: string;
|
|
}
|
|
|
|
interface AuthorizationApi {
|
|
createSelfSubjectAccessReview(
|
|
request: Readonly<{
|
|
body: Readonly<{
|
|
apiVersion: 'authorization.k8s.io/v1';
|
|
kind: 'SelfSubjectAccessReview';
|
|
spec: Readonly<{ resourceAttributes: AccessReviewAttributes }>;
|
|
}>;
|
|
}>,
|
|
): Promise<
|
|
Readonly<{
|
|
status?: Readonly<{ allowed?: boolean; denied?: boolean }>;
|
|
}>
|
|
>;
|
|
}
|
|
|
|
function accessMatrix(
|
|
options: ClusterPromptOutputKubernetesSecretKeyringOptions,
|
|
): Readonly<{
|
|
allowed: readonly AccessReviewAttributes[];
|
|
denied: readonly AccessReviewAttributes[];
|
|
}> {
|
|
return Object.freeze({
|
|
allowed: Object.freeze([
|
|
{
|
|
namespace: options.namespace,
|
|
verb: 'get',
|
|
resource: 'secrets',
|
|
name: options.secretName,
|
|
},
|
|
{
|
|
namespace: options.namespace,
|
|
verb: 'update',
|
|
resource: 'secrets',
|
|
name: options.secretName,
|
|
},
|
|
]),
|
|
denied: Object.freeze([
|
|
{ namespace: options.namespace, verb: 'list', resource: 'secrets' },
|
|
{ namespace: options.namespace, verb: 'watch', resource: 'secrets' },
|
|
{ namespace: options.namespace, verb: 'create', resource: 'secrets' },
|
|
{
|
|
namespace: options.namespace,
|
|
verb: 'delete',
|
|
resource: 'secrets',
|
|
name: options.secretName,
|
|
},
|
|
{
|
|
namespace: options.namespace,
|
|
verb: 'patch',
|
|
resource: 'secrets',
|
|
name: options.secretName,
|
|
},
|
|
{
|
|
namespace: options.namespace,
|
|
verb: 'get',
|
|
resource: 'secrets',
|
|
name: `${options.secretName}-other`,
|
|
},
|
|
{ namespace: options.namespace, verb: 'get', resource: 'configmaps' },
|
|
{ namespace: options.namespace, verb: 'get', resource: 'pods' },
|
|
]),
|
|
});
|
|
}
|
|
|
|
async function assertExactKubernetesAuthority(
|
|
api: AuthorizationApi,
|
|
options: ClusterPromptOutputKubernetesSecretKeyringOptions,
|
|
): Promise<void> {
|
|
const matrix = accessMatrix(options);
|
|
for (const [expected, checks] of [
|
|
[true, matrix.allowed],
|
|
[false, matrix.denied],
|
|
] as const) {
|
|
for (const attributes of checks) {
|
|
const review = await api.createSelfSubjectAccessReview({
|
|
body: {
|
|
apiVersion: 'authorization.k8s.io/v1',
|
|
kind: 'SelfSubjectAccessReview',
|
|
spec: { resourceAttributes: attributes },
|
|
},
|
|
});
|
|
if (
|
|
review.status?.allowed !== expected ||
|
|
(expected && review.status.denied === true)
|
|
) {
|
|
throw new TypeError(
|
|
'Kubernetes Secret lifecycle authority is not exact',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function openPromptOutputKubernetesSecretAuthority(
|
|
options: ClusterPromptOutputKubernetesSecretKeyringOptions,
|
|
): Promise<
|
|
Readonly<{
|
|
materials: ClusterPromptOutputKubernetesSecretKeyring;
|
|
dispose(): void;
|
|
}>
|
|
> {
|
|
const kubernetes = await import('@kubernetes/client-node');
|
|
const config = new kubernetes.KubeConfig();
|
|
config.loadFromCluster();
|
|
const cluster = config.getCurrentCluster();
|
|
let server: URL;
|
|
try {
|
|
server = new URL(cluster?.server ?? '');
|
|
} catch {
|
|
throw new TypeError('Kubernetes cluster authority is invalid');
|
|
}
|
|
if (
|
|
!cluster ||
|
|
server.protocol !== 'https:' ||
|
|
server.username !== '' ||
|
|
server.password !== '' ||
|
|
server.hash !== '' ||
|
|
cluster.skipTLSVerify === true ||
|
|
(typeof cluster.caData !== 'string' && typeof cluster.caFile !== 'string')
|
|
) {
|
|
throw new TypeError('Kubernetes cluster authority is invalid');
|
|
}
|
|
const secrets = config.makeApiClient(
|
|
kubernetes.CoreV1Api,
|
|
) as unknown as ClusterPromptOutputKubernetesSecretApi;
|
|
const authorization = config.makeApiClient(
|
|
kubernetes.AuthorizationV1Api,
|
|
) as unknown as AuthorizationApi;
|
|
await assertExactKubernetesAuthority(authorization, options);
|
|
let active = true;
|
|
return Object.freeze({
|
|
materials: new ClusterPromptOutputKubernetesSecretKeyring(secrets, options),
|
|
dispose() {
|
|
if (!active) return;
|
|
active = false;
|
|
for (const user of config.getUsers()) {
|
|
(user as { token?: string }).token = '';
|
|
}
|
|
config.setCurrentContext('disposed');
|
|
},
|
|
});
|
|
}
|