Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix the mismatch between specifier and resource identifier in attenuators policy AND allow skiping powerless packages #1838

Merged
merged 6 commits into from
Dec 12, 2023
3 changes: 3 additions & 0 deletions packages/compartment-mapper/src/import-archive.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ const makeArchiveImportHookMaker = (
// module is a "builtin" module and the policy needs to be enforced.
enforceModulePolicy(moduleSpecifier, compartmentDescriptor, {
exit: true,
errorHint: `Blocked in loading. ${q(
moduleSpecifier,
)} was not in the archive and an attempt was made to load it as a builtin`,
});
const record = await exitModuleImportHook(moduleSpecifier);
if (record) {
Expand Down
3 changes: 3 additions & 0 deletions packages/compartment-mapper/src/import-hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ export const makeImportHookMaker = (
// hook returns something. Otherwise, we need to fall back to the 'cannot find' error below.
enforceModulePolicy(moduleSpecifier, compartmentDescriptor, {
exit: true,
errorHint: `Blocked in loading. ${q(
moduleSpecifier,
)} was not in the compartment map and an attempt was made to load it as a builtin`,
});
if (archiveOnly) {
// Return a place-holder.
Expand Down
29 changes: 12 additions & 17 deletions packages/compartment-mapper/src/link.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { parseExtension } from './extension.js';
import {
enforceModulePolicy,
ATTENUATORS_COMPARTMENT,
diagnoseMissingCompartmentError,
attenuateGlobals,
makeDeferredAttenuatorsProvider,
} from './policy.js';
Expand Down Expand Up @@ -232,10 +231,14 @@ const makeModuleMapHook = (
return undefined; // fall through to import hook
}
if (foreignModuleSpecifier !== undefined) {
// archive goes through foreignModuleSpecifier for local modules too
if (!moduleSpecifier.startsWith('./')) {
// archive goes through foreignModuleSpecifier for local modules too
// This code path seems to only be reached on subsequent imports of the same specifier in the same compartment.
// The check should be redundant and is only left here out of abundance of caution.
enforceModulePolicy(moduleSpecifier, compartmentDescriptor, {
exit: false,
errorHint:
'This check should not be reachable. If you see this error, please file an issue.',
});
}

Expand All @@ -244,12 +247,7 @@ const makeModuleMapHook = (
throw Error(
`Cannot import from missing compartment ${q(
foreignCompartmentName,
)}${diagnoseMissingCompartmentError({
moduleSpecifier,
compartmentDescriptor,
foreignModuleSpecifier,
foreignCompartmentName,
})}`,
)}}`,
);
}
return foreignCompartment.module(foreignModuleSpecifier);
Expand Down Expand Up @@ -279,20 +277,17 @@ const makeModuleMapHook = (
throw Error(
`Cannot import from missing compartment ${q(
foreignCompartmentName,
)}${diagnoseMissingCompartmentError({
moduleSpecifier,
compartmentDescriptor,
foreignModuleSpecifier,
foreignCompartmentName,
})}`,
)}`,
);
}

// Despite all non-exit modules not allowed by policy being dropped
// while building the graph, this check is necessary because module
// is written back to the compartment map below.
kriskowal marked this conversation as resolved.
Show resolved Hide resolved
enforceModulePolicy(scopePrefix, compartmentDescriptor, {
exit: false,
errorHint: `Blocked in linking. ${q(
moduleSpecifier,
)} is part of the compartment map and resolves to ${q(
foreignCompartmentName,
)}.`,
});
// The following line is weird.
// Information is flowing backward.
Expand Down
7 changes: 1 addition & 6 deletions packages/compartment-mapper/src/node-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ const graphPackages = async (
* @param {Graph} graph
* @param {Set<string>} tags - build tags about the target environment
* for selecting relevant exports, e.g., "browser" or "node".
* @param {object} policy
* @param {object|undefined} policy
* @returns {CompartmentMapDescriptor}
*/
const translateGraph = (
Expand Down Expand Up @@ -611,11 +611,6 @@ const translateGraph = (
},
policy,
);
// do not include compartments for packages not covered by policy
if (policy && !packagePolicy) {
// eslint-disable-next-line no-continue
continue;
}
naugtur marked this conversation as resolved.
Show resolved Hide resolved

/**
* @param {string} dependencyName
Expand Down
2 changes: 1 addition & 1 deletion packages/compartment-mapper/src/policy-format.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export const assertPackagePolicy = (allegedPackagePolicy, path, url) => {
const packagePolicy = Object(allegedPackagePolicy);
assert(
allegedPackagePolicy === packagePolicy && !isArray(allegedPackagePolicy),
`${path} must be an object, got ${allegedPackagePolicy}${inUrl}`,
`${path} must be an object, got ${q(allegedPackagePolicy)}${inUrl}`,
);
const {
packages,
Expand Down
121 changes: 23 additions & 98 deletions packages/compartment-mapper/src/policy.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
isAllowingEverything,
} from './policy-format.js';

const { entries, values, assign, keys, freeze } = Object;
const { create, entries, values, assign, keys, freeze } = Object;
const q = JSON.stringify;

/**
Expand Down Expand Up @@ -113,29 +113,6 @@ export const dependencyAllowedByPolicy = (namingKit, packagePolicy) => {
return !!policyLookupHelper(packagePolicy, 'packages', canonicalName);
};

const validateDependencies = (policy, canonicalName) => {
kriskowal marked this conversation as resolved.
Show resolved Hide resolved
const packages = policy.resources[canonicalName].packages;
if (!packages || isAllowingEverything(packages)) {
return;
}

const packageNames = keys(packages);
const attenuators = detectAttenuators(policy);
// Join attenuators with packageNames into a Set to deduplicate and check if all are listed in policy.resources
const allSpecifiers = new Set([...packageNames, ...attenuators]);
for (const specifier of allSpecifiers) {
if (!(specifier in policy.resources)) {
throw Error(
`Package ${q(specifier)} is allowed for ${q(
canonicalName,
)} to import but its policy is not defined. Please add a policy for ${q(
specifier,
)}`,
);
}
}
};

/**
* Returns the policy applicable to the canonicalName of the package
*
Expand All @@ -154,20 +131,14 @@ export const getPolicyForPackage = (namingKit, policy) => {
if (canonicalName === ATTENUATORS_COMPARTMENT) {
return {
defaultAttenuator: policy.defaultAttenuator,
packages: detectAttenuators(policy).reduce((packages, specifier) => {
packages[specifier] = true;
return packages;
}, {}),
packages: 'any',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this removed section had the bug where we mistook specifier for a resource id. and it was all unnecessary.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I trust that https://www.npmjs.com/package/any is not an issue with this property’s range of expressible values?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

packages is normally a key/value map. there's only one string value allowed and that's the "any". There's no room for a mistake here

};
}
if (policy.resources && policy.resources[canonicalName]) {
validateDependencies(policy, canonicalName);
if (policy.resources && policy.resources[canonicalName] !== undefined) {
return policy.resources[canonicalName];
} else {
console.warn(
`No policy for '${canonicalName}', omitting from compartment map.`,
);
return undefined;
// Allow skipping policy entries for packages with no powers.
return create(null);
}
};

Expand Down Expand Up @@ -357,25 +328,37 @@ export const attenuateGlobals = (
freezeGlobalThisUnlessOptedOut();
};

const diagnoseModulePolicy = errorHint => {
if (!errorHint) {
return '';
}
return ` (info: ${errorHint})`;
};
/**
* Throws if importing of the specifier is not allowed by the policy
*
* @param {string} specifier
* @param {object} compartmentDescriptor
* @param {import('./types.js').CompartmentDescriptor} compartmentDescriptor
* @param {object} [info]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this info type be made more specific?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll unwrap errorHint

*/
export const enforceModulePolicy = (specifier, compartmentDescriptor, info) => {
const { policy, modules } = compartmentDescriptor;
export const enforceModulePolicy = (
specifier,
compartmentDescriptor,
info = {},
) => {
const { policy, modules, label } = compartmentDescriptor;
if (!policy) {
return;
}

if (!info.exit) {
if (!modules[specifier]) {
throw Error(
`Importing ${q(specifier)} was not allowed by policy packages:${q(
`Importing ${q(specifier)} in ${q(
label,
)} was not allowed by packages policy ${q(
policy.packages,
)}`,
)}${diagnoseModulePolicy(info.errorHint)}`,
);
}
return;
Expand All @@ -385,7 +368,7 @@ export const enforceModulePolicy = (specifier, compartmentDescriptor, info) => {
throw Error(
`Importing ${q(specifier)} was not allowed by policy builtins:${q(
policy.builtins,
)}`,
)}${diagnoseModulePolicy(info.errorHint)}`,
);
}
};
Expand Down Expand Up @@ -459,61 +442,3 @@ export const attenuateModuleHook = async (
originalModuleRecord,
});
};

const padDiagnosis = text => ` (${text})`;
/**
* Provide dignostic information for a missing compartment error
*
* @param {object} args
* @param {string} args.moduleSpecifier
* @param {object} args.compartmentDescriptor
* @param {string} args.foreignModuleSpecifier
* @param {string} args.foreignCompartmentName
* @returns {string}
*/
export const diagnoseMissingCompartmentError = ({
moduleSpecifier,
compartmentDescriptor,
foreignModuleSpecifier,
foreignCompartmentName,
}) => {
const { policy, name, scopes } = compartmentDescriptor;

if (policy) {
if (!policy.packages) {
return padDiagnosis(
`There were no allowed packages specified in policy for ${q(name)}`,
);
}
if (name === ATTENUATORS_COMPARTMENT) {
return padDiagnosis(
`Attenuator ${q(
moduleSpecifier,
)} was imported but there is no policy resources entry defined for it.`,
);
}

const scopeNames = entries(scopes)
.filter(([_name, scope]) => scope.compartment === foreignCompartmentName)
.map(([scopeName]) => scopeName);
if (scopeNames.length === 1 && scopeNames[0] === moduleSpecifier) {
return padDiagnosis(
`Package ${q(
moduleSpecifier,
)} is missing. Are you sure there is an entry in policy resources specified for it?`,
);
} else {
return padDiagnosis(
`Package ${q(moduleSpecifier)} resolves to ${q(
foreignModuleSpecifier,
)} in ${q(
foreignCompartmentName,
)} which seems disallowed by policy. There is likely an override defined that causes another package to be imported as ${q(
moduleSpecifier,
)}.`,
);
}
}
// Omit diagnostics when parent package had no policy - it means there was no policy.
return '';
};

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading