Files
NetMesh/electron/plugins/secureCapabilities.cjs

188 lines
8.5 KiB
JavaScript
Raw Normal View History

"use strict";
const { PluginRpcError, RPC_ERRORS } = require("./rpcRouter.cjs");
const { assertSecretKey } = require("./secretStore.cjs");
function assertSecretParams(params, options = {}) {
if (!params || typeof params !== "object" || Array.isArray(params)) {
throw new PluginRpcError(RPC_ERRORS.invalidArgument, "Plugin secret parameters are invalid");
}
const key = assertSecretKey(params.key);
if (options.value) {
if (typeof params.value !== "string") {
throw new PluginRpcError(RPC_ERRORS.invalidArgument, "Plugin secret value is invalid");
}
return { key, value: params.value };
}
return { key };
}
function secretAuthorization(params, action) {
const value = assertSecretParams(params);
return {
permission: "secrets",
resources: [`secret:${value.key}`],
reason: `${action} plugin secret ${value.key}`,
operationId: `secret:${action.toLowerCase()}:${value.key}`,
};
}
function registerSecurePluginCapabilities(registry, options) {
registry.use(options.quotaManager.createMiddleware());
registry.use(options.permissionEngine.createMiddleware());
registry.registerRequest("secrets.get", async ({ key }, context) => {
const secret = options.secretStore.getReference(context.pluginId, key);
return secret ? { found: true, secret } : { found: false };
}, {
metadata: { capability: "secrets", mutating: false, permission: "secrets" },
validateParams: (params) => assertSecretParams(params),
authorization: (params) => secretAuthorization(params, "Read"),
});
registry.registerRequest("secrets.set", async ({ key, value }, context) => {
await context.assertActive();
return { secret: options.secretStore.set(context.pluginId, key, value) };
}, {
metadata: { capability: "secrets", mutating: true, permission: "secrets" },
validateParams: (params) => assertSecretParams(params, { value: true }),
authorization: (params) => secretAuthorization(params, "Store"),
});
registry.registerRequest("secrets.delete", async ({ key }, context) => {
await context.assertActive();
options.secretStore.delete(context.pluginId, key);
return null;
}, {
metadata: { capability: "secrets", mutating: true, permission: "secrets" },
validateParams: (params) => assertSecretParams(params),
authorization: (params) => secretAuthorization(params, "Delete"),
});
registry.registerRequest("credentials.createLease", (params, context) => (
options.credentialBroker.createLease(params, context)
), {
metadata: { capability: "credentials", mutating: true, permission: "secrets" },
validateParams: options.assertLeaseParams,
authorization: (params) => options.credentialBroker.describeAuthorization(params),
});
registry.registerRequest("network.request", async (params, context) => {
// params are already registry-validated (preserve credentialLease/authorization).
const validated = params;
const lease = validated?.credentialLease;
if (lease === undefined) {
return options.networkBroker.request(validated, context);
}
if (!options.credentialBroker || typeof options.credentialBroker.consumeLease !== "function") {
throw new PluginRpcError(RPC_ERRORS.unavailable, "Plugin credential leases are unavailable");
}
// Bind consumption to the host-owned network operation id (network:<origin>),
// not the plugin-echoed lease.operationId, so leases cannot be replayed across ops.
const hostOperationId = options.networkBroker.describeAuthorization(validated).operationId;
const plaintext = await options.credentialBroker.consumeLease(
context,
lease,
hostOperationId,
);
if (typeof plaintext !== "string" || plaintext.length < 1) {
throw new PluginRpcError(RPC_ERRORS.dataLoss, "Plugin credential lease resolved to an empty secret");
}
const authorization = validated?.authorization && typeof validated.authorization === "object"
&& !Array.isArray(validated.authorization)
? validated.authorization
: { scheme: "Bearer" };
const scheme = authorization.scheme === "Basic" ? "Basic" : "Bearer";
const headers = { ...(validated.headers ?? {}) };
if (scheme === "Basic") {
const username = typeof authorization.username === "string" ? authorization.username : "";
headers.authorization = `Basic ${Buffer.from(`${username}:${plaintext}`, "utf8").toString("base64")}`;
} else {
headers.authorization = `Bearer ${plaintext}`;
}
const { credentialLease: _lease, authorization: _auth, ...requestParams } = validated;
return options.networkBroker.request({
...requestParams,
headers,
}, context);
}, {
metadata: { capability: "network", mutating: true, permission: "network" },
validateParams: (params) => options.networkBroker.validate(params),
authorization: (params) => options.networkBroker.describeAuthorization(params),
});
registry.registerRequest("filesystem.readFile", (params, context) => (
options.filesystemBroker.readFile(params, context)
), {
metadata: { capability: "filesystem", mutating: false, permission: "filesystem.read" },
validateParams: (params) => options.filesystemBroker.validateRead(params),
authorization: (params) => options.filesystemBroker.describeReadAuthorization(params, "exact"),
});
registry.registerRequest("filesystem.stat", (params, context) => (
options.filesystemBroker.stat(params, context)
), {
metadata: { capability: "filesystem", mutating: false, permission: "filesystem.read" },
validateParams: (params) => options.filesystemBroker.validatePath(params),
authorization: (params) => options.filesystemBroker.describeReadAuthorization(params, "exact"),
});
registry.registerRequest("filesystem.readDirectory", (params, context) => (
options.filesystemBroker.readDirectory(params, context)
), {
metadata: { capability: "filesystem", mutating: false, permission: "filesystem.read" },
validateParams: (params) => options.filesystemBroker.validatePath(params),
authorization: (params) => options.filesystemBroker.describeReadAuthorization(params, "directory"),
});
registry.registerRequest("filesystem.writeFile", (params, context) => (
options.filesystemBroker.writeFile(params, context)
), {
metadata: { capability: "filesystem", mutating: true, permission: "filesystem.write" },
validateParams: (params) => options.filesystemBroker.validateWrite(params),
authorization: (params) => options.filesystemBroker.describeWriteAuthorization(params),
});
registry.registerRequest("companion.start", (params, context) => (
options.companionSupervisor.start(params, context)
), {
metadata: { capability: "companion", mutating: true, permission: "companion.execute" },
validateParams: (params) => options.companionSupervisor.validateStart(params),
authorization: (params, context) => (
options.companionSupervisor.describeStartAuthorization(params, context)
),
});
registry.registerRequest("companion.request", async (params, context) => {
const validated = options.companionSupervisor.validateRequest(params);
if (validated.credentialLeases === undefined) {
return options.companionSupervisor.request(validated, context);
}
const credentials = {};
for (const [name, lease] of Object.entries(validated.credentialLeases)) {
credentials[name] = await options.credentialBroker.consumeLease(
context,
lease,
validated.operationId,
);
}
await context.assertActive();
return options.companionSupervisor.request({
handleId: validated.handleId,
method: validated.method,
params: Object.freeze({
payload: validated.params ?? null,
credentials: Object.freeze(credentials),
}),
timeoutMs: validated.timeoutMs,
}, context);
}, {
metadata: { capability: "companion", mutating: true, permission: "companion.execute" },
validateParams: (params) => options.companionSupervisor.validateRequest(params),
authorization: (params, context) => options.companionSupervisor.describeHandleAuthorization(params, context),
});
registry.registerRequest("companion.stop", (params, context) => (
options.companionSupervisor.stop(params, context)
), {
metadata: { capability: "companion", mutating: true, permission: "companion.execute" },
validateParams: (params) => options.companionSupervisor.validateStop(params),
authorization: (params, context) => options.companionSupervisor.describeHandleAuthorization(params, context),
});
}
module.exports = { assertSecretParams, registerSecurePluginCapabilities, secretAuthorization };