@cloud-copilot/iam-lens
Version:
Visibility in IAM in and across AWS accounts
189 lines • 8.68 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const worker_threads_1 = require("worker_threads");
const collect_js_1 = require("../collect/collect.js");
const dynamicImport_js_1 = require("../utils/dynamicImport.js");
const JobRunner_js_1 = require("../workers/JobRunner.js");
const SharedArrayBufferWorkerCache_js_1 = require("../workers/SharedArrayBufferWorkerCache.js");
const requestAnalysis_js_1 = require("./requestAnalysis.js");
const WhoCanWorker_js_1 = require("./WhoCanWorker.js");
if (!worker_threads_1.parentPort) {
throw new Error('Must be run as a worker thread');
}
// Get config from the main thread
const { concurrency, collectConfigs, partition, s3AbacOverride, collectGrantDetails, clientFactoryPlugin, workerBootstrapPlugin } = worker_threads_1.workerData;
// Pending task requests from workers, keyed by workerId
const taskPromises = {};
// Pending deny details checks, keyed by a unique id for each check
let denyDetailsCheckId = 0;
const pendingDenyDetailsChecks = {};
/**
* Runs the consumer-provided bootstrap plugin if one was specified.
* Called before any other worker initialization.
*/
async function runBootstrapPlugin() {
if (!workerBootstrapPlugin)
return;
const mod = await (0, dynamicImport_js_1.dynamicImport)(workerBootstrapPlugin.module);
const factory = mod[workerBootstrapPlugin.factoryExport];
if (!factory) {
throw new Error(`Bootstrap export '${workerBootstrapPlugin.factoryExport}' not found in module '${workerBootstrapPlugin.module}'`);
}
else if (typeof factory !== 'function') {
throw new Error(`Bootstrap export '${workerBootstrapPlugin.factoryExport}' in module '${workerBootstrapPlugin.module}' is not a function`);
}
await factory({ data: workerBootstrapPlugin.data, threadId: worker_threads_1.threadId, isMainThread: false });
}
/**
* Creates the lifetime-scoped PullBasedJobRunner. The runner starts
* immediately and processes work items until finishAllWork() is called.
*
* @param collectClient - Promise for the collect client, resolved during bootstrap.
* @returns the PullBasedJobRunner instance.
*/
function createJobRunner(collectClient) {
return new JobRunner_js_1.PullBasedJobRunner(concurrency, async (workerId) => {
return new Promise((resolve) => {
worker_threads_1.parentPort.postMessage({ type: 'requestTask', workerId });
taskPromises[workerId] = resolve;
});
}, (taggedTask) => {
const { requestId, ...workItem } = taggedTask;
return {
properties: { requestId, collectDenyDetails: workItem.collectDenyDetails },
execute: async () => {
return (0, WhoCanWorker_js_1.executeWhoCan)(workItem, collectClient, {
s3AbacOverride,
collectDenyDetails: workItem.collectDenyDetails,
collectGrantDetails,
strictContextKeys: workItem.strictContextKeys
});
}
};
}, async (result) => {
const { requestId, collectDenyDetails } = result.properties;
if (result.status === 'fulfilled') {
const executionResult = result.value;
if (executionResult.type === 'allowed') {
worker_threads_1.parentPort.postMessage({
type: 'result',
requestId,
result: {
status: 'fulfilled',
value: executionResult.allowed,
properties: {}
}
});
}
else {
// Check if deny details check will follow
const hasDetails = executionResult.type === 'denied_single' || executionResult.type === 'denied_wildcard';
const denyDetailsCheckWillFollow = collectDenyDetails && hasDetails;
// Post count-only result for denied simulations
worker_threads_1.parentPort.postMessage({
type: 'result',
requestId,
denyDetailsCheckWillFollow,
result: {
status: 'fulfilled',
value: undefined,
properties: {}
}
});
if (denyDetailsCheckWillFollow) {
const lightAnalysis = (0, requestAnalysis_js_1.toLightRequestAnalysis)(executionResult);
const checkId = denyDetailsCheckId++;
worker_threads_1.parentPort.postMessage({
type: 'checkDenyDetails',
requestId,
checkId,
workItem: executionResult.workItem,
lightAnalysis
});
const shouldInclude = await new Promise((resolve) => {
pendingDenyDetailsChecks[checkId] = resolve;
});
if (shouldInclude) {
worker_threads_1.parentPort.postMessage({
type: 'denyDetailsResult',
requestId,
denyDetail: (0, requestAnalysis_js_1.convertToDenialDetails)(executionResult)
});
}
}
}
}
else {
// Error case — pass through with requestId
worker_threads_1.parentPort.postMessage({
type: 'result',
requestId,
result: result
});
}
});
}
/**
* Async entry point for the worker. Initializes bootstrap, collect client,
* message handler, and job runner in the correct order.
*/
async function main() {
// 1. Run bootstrap plugin if present
await runBootstrapPlugin();
// 2. Start creating the collect client (resolves asynchronously — the
// shared-cache bridge is already installed on the main thread so cache
// messages will be served during startup)
const collectClientPromise = await (0, collect_js_1.getCollectClient)(collectConfigs, partition, {
cacheProvider: new SharedArrayBufferWorkerCache_js_1.SharedArrayBufferWorkerCache(worker_threads_1.parentPort),
clientFactoryPlugin
});
// 3. Signal ready (tells main thread bootstrap + client init succeeded)
worker_threads_1.parentPort.postMessage({ type: 'ready' });
// 4. Wait for 'start' from the main thread. The main thread sends this
// after installing its lifetime message listeners, so that requestTask
// messages emitted by the job runner are not dropped.
await new Promise((resolve) => {
const onStart = (msg) => {
if (msg.type === 'start') {
worker_threads_1.parentPort.off('message', onStart);
resolve();
}
};
worker_threads_1.parentPort.on('message', onStart);
});
// 5. Install message handler (must be ready before runner pulls tasks).
// jobRunner is assigned after the handler is installed but before the
// runner constructor fires its first requestTask, so the closure is safe.
let jobRunner;
worker_threads_1.parentPort.on('message', (msg) => {
if (msg.type === 'task' && msg.workerId in taskPromises) {
// Task delivered from main thread (may be undefined if nothing is ready)
taskPromises[msg.workerId](msg.task);
delete taskPromises[msg.workerId];
}
else if (msg.type === 'workAvailable') {
jobRunner.notifyWorkAvailable();
}
else if (msg.type === 'finishWork') {
jobRunner.finishAllWork().then(() => {
worker_threads_1.parentPort.postMessage({ type: 'finished' });
});
}
else if (msg.type === 'denyDetailsCheckResult') {
const checkId = msg.checkId;
const resolveFn = pendingDenyDetailsChecks[checkId];
if (resolveFn) {
resolveFn(msg.shouldInclude);
delete pendingDenyDetailsChecks[checkId];
}
}
});
// 6. Create job runner (constructor eagerly starts pulling tasks)
jobRunner = createJobRunner(collectClientPromise);
}
void main().catch((err) => {
// Post explicit error so main thread doesn't depend on unhandled-rejection behavior.
worker_threads_1.parentPort.postMessage({ type: 'startupError', error: String(err) });
process.exit(1);
});
//# sourceMappingURL=WhoCanWorkerThreadWorker.js.map