Skip to content

Commit

Permalink
Merge pull request #1839 from boneskull/boneskull/add-policy-types
Browse files Browse the repository at this point in the history
feat(compartment-mapper): add policy-related types
  • Loading branch information
boneskull authored Jan 10, 2024
2 parents e9ecfda + 1f322fa commit 1034fce
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 99 deletions.
7 changes: 6 additions & 1 deletion packages/compartment-mapper/demo/policy/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ lockdown({

import fs from 'fs';

import { importLocation, makeArchive, parseArchive } from '../../index.js';
import {
importLocation,
makeArchive,
parseArchive,
} from '@endo/compartment-mapper';

const readPower = async location =>
fs.promises.readFile(new URL(location).pathname);
Expand All @@ -18,6 +22,7 @@ const entrypointPath = new URL('./app.js', import.meta.url).href;
const ApiSubsetOfBuffer = harden({ from: Buffer.from });

const options = {
/** @type {import('@endo/compartment-mapper').Policy} */
policy: {
defaultAttenuator:
'@endo/compartment-mapper-demo-lavamoat-style-attenuator',
Expand Down
2 changes: 1 addition & 1 deletion packages/compartment-mapper/src/import.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export const loadLocation = async (readPowers, moduleLocation, options) => {
* @param {ReadFn | ReadPowers} readPowers
* @param {string} moduleLocation
* @param {ExecuteOptions & ArchiveOptions} [options]
* @returns {Promise<object>} the object of the imported modules exported
* @returns {Promise<import('./types.js').SomeObject>} the object of the imported modules exported
* names.
*/
export const importLocation = async (
Expand Down
35 changes: 21 additions & 14 deletions packages/compartment-mapper/src/node-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,18 @@
* @typedef {Record<string, {spec: string, alias: string}>} CommonDependencyDescriptors
*/

import { pathCompare } from './compartment-map.js';
import { inferExportsAndAliases } from './infer-exports.js';
import { searchDescriptor } from './search.js';
import { parseLocatedJson } from './json.js';
import { unpackReadPowers } from './powers.js';
import { join } from './node-module-specifier.js';
import { assertPolicy } from './policy-format.js';
import {
getPolicyForPackage,
ATTENUATORS_COMPARTMENT,
dependencyAllowedByPolicy,
getPolicyForPackage,
} from './policy.js';
import { join } from './node-module-specifier.js';
import { pathCompare } from './compartment-map.js';
import { assertPolicy } from './policy-format.js';
import { unpackReadPowers } from './powers.js';
import { searchDescriptor } from './search.js';

const { assign, create, keys, values } = Object;

Expand Down 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|undefined} policy
* @param {import('./types.js').Policy} [policy]
* @returns {CompartmentMapDescriptor}
*/
const translateGraph = (
Expand Down Expand Up @@ -612,6 +612,12 @@ const translateGraph = (
policy,
);

/* c8 ignore next */
if (policy && !packagePolicy) {
// this should never happen
throw new TypeError('Unexpectedly falsy package policy');
}

/**
* @param {string} dependencyName
* @param {string} packageLocation
Expand All @@ -626,13 +632,14 @@ const translateGraph = (
const localPath = join(dependencyName, exportPath);
if (
!policy ||
dependencyAllowedByPolicy(
{
name,
path,
},
packagePolicy,
)
(packagePolicy &&
dependencyAllowedByPolicy(
{
name,
path,
},
packagePolicy,
))
) {
moduleDescriptors[localPath] = {
compartment: packageLocation,
Expand Down
82 changes: 54 additions & 28 deletions packages/compartment-mapper/src/policy-format.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
// @ts-check

/** @typedef {import('./types.js').AttenuationDefinition} AttenuationDefinition */
/** @typedef {import('./types.js').UnifiedAttenuationDefinition} UnifiedAttenuationDefinition */

const { entries, keys } = Object;
const { isArray } = Array;
const q = JSON.stringify;

const ATTENUATOR_KEY = 'attenuate';
const ATTENUATOR_PARAMS_KEY = 'params';
const WILDCARD_POLICY_VALUE = 'any';
const POLICY_FIELDS_LOOKUP = ['builtins', 'globals', 'packages'];
const POLICY_FIELDS_LOOKUP = /** @type {const} */ ([
'builtins',
'globals',
'packages',
]);

/**
*
* @param {object} packagePolicy
* @param {string} field
* @param {import('./types.js').PackagePolicy} packagePolicy
* @param {'builtins'|'globals'|'packages'} field
* @param {string} itemName
* @returns {boolean | object}
* @returns {boolean | import('./types.js').AttenuationDefinition}
*/
export const policyLookupHelper = (packagePolicy, field, itemName) => {
if (!POLICY_FIELDS_LOOKUP.includes(field)) {
Expand All @@ -34,38 +35,43 @@ export const policyLookupHelper = (packagePolicy, field, itemName) => {
if (packagePolicy[field] === WILDCARD_POLICY_VALUE) {
return true;
}
if (packagePolicy[field][itemName]) {
return packagePolicy[field][itemName];

const value = /** @type {import('./types.js').AttenuationDefinition} */ (
packagePolicy[field]
);
if (itemName in value) {
return value[itemName];
}
return false;
};

/**
* Checks if the policy value is set to wildcard to allow everything
* Type guard; checks if the policy value is set to the wildcard value to allow everything
*
* @param {any} policyValue
* @returns {boolean}
* @param {unknown} policyValue
* @returns {policyValue is import('./types.js').WildcardPolicy}
*/
export const isAllowingEverything = policyValue =>
policyValue === WILDCARD_POLICY_VALUE;

/**
*
* @param {AttenuationDefinition} potentialDefinition
* @returns {boolean}
* Type guard for `AttenuationDefinition`
* @param {unknown} allegedDefinition
* @returns {allegedDefinition is import('./types.js').AttenuationDefinition}
*/
export const isAttenuationDefinition = potentialDefinition => {
return (
(typeof potentialDefinition === 'object' &&
typeof potentialDefinition[ATTENUATOR_KEY] === 'string') || // object with attenuator name
isArray(potentialDefinition) // params for default attenuator
export const isAttenuationDefinition = allegedDefinition => {
return Boolean(
(allegedDefinition &&
typeof allegedDefinition === 'object' &&
typeof allegedDefinition[ATTENUATOR_KEY] === 'string') || // object with attenuator name
isArray(allegedDefinition), // params for default attenuator
);
};

/**
*
* @param {AttenuationDefinition} attenuationDefinition
* @returns {UnifiedAttenuationDefinition}
* @param {import('./types.js').AttenuationDefinition} attenuationDefinition
* @returns {import('./types.js').UnifiedAttenuationDefinition}
*/
export const getAttenuatorFromDefinition = attenuationDefinition => {
if (!isAttenuationDefinition(attenuationDefinition)) {
Expand All @@ -90,28 +96,45 @@ export const getAttenuatorFromDefinition = attenuationDefinition => {
}
};

// TODO: should be a type guard
const isRecordOf = (item, predicate) => {
if (typeof item !== 'object' || item === null || isArray(item)) {
return false;
}
return entries(item).every(([key, value]) => predicate(value, key));
};

/**
* Type guard for `boolean`
* @param {unknown} item
* @returns {item is boolean}
*/
const isBoolean = item => typeof item === 'boolean';

// TODO: should be a type guard
const predicateOr =
(...predicates) =>
item =>
predicates.some(p => p(item));

/**
* @param {unknown} item
* @returns {item is import('./types.js').PolicyItem}
*/
const isPolicyItem = item =>
item === undefined ||
item === WILDCARD_POLICY_VALUE ||
isRecordOf(item, isBoolean);

/**
* This asserts (i.e., throws) that `allegedPackagePolicy` is a valid `PackagePolicy`.
*
* Mild-mannered during the day, but fights crime at night as a type guard.
*
* @param {unknown} allegedPackagePolicy
* @param {string} path
* @param {string} [url]
* @returns {void}
* @param {unknown} allegedPackagePolicy - Alleged `PackagePolicy` to test
* @param {string} path - Path in the `Policy` object; used for error messages only
* @param {string} [url] - URL of the policy file; used for error messages only
* @returns {asserts allegedPackagePolicy is import('./types.js').PackagePolicy|undefined}
*/
export const assertPackagePolicy = (allegedPackagePolicy, path, url) => {
if (allegedPackagePolicy === undefined) {
Expand Down Expand Up @@ -172,9 +195,12 @@ export const assertPackagePolicy = (allegedPackagePolicy, path, url) => {
};

/**
* This asserts (i.e., throws) that `allegedPolicy` is a valid `Policy`
*
* It also moonlights as a type guard.
*
* @param {unknown} allegedPolicy
* @returns {void}
* @param {unknown} allegedPolicy - Alleged `Policy` to test
* @returns {asserts allegedPolicy is import('./types.js').Policy|undefined}
*/
export const assertPolicy = allegedPolicy => {
if (allegedPolicy === undefined) {
Expand Down
Loading

0 comments on commit 1034fce

Please sign in to comment.