Skip to content

Access control for file access and generator execution #353

Description

@mshima

Problem

A Yeoman environment runs third-party code: the generators it composes. Today the environment has no
way to define or enforce a policy over what that code does — neither which generators may execute, nor
which parts of the filesystem they may read and write.

This matters because a generator's configuration is frequently not authored by the person running it.
Configuration is generator input, and it can reach paths and packages far outside the project being
generated. There is currently no boundary between "this generator was asked to scaffold a project here"
and "this generator read or wrote something elsewhere on the machine".

What this is, and what it is not

This is an access-control model for code that uses the official APIs. It is an accident-containment and
untrusted-data boundary, plus an audit point — not a sandbox.

Generator code can call node:fs directly and bypass any of this. Containing genuinely hostile code is
the job of OS-level isolation (Node's permission model, a container). Untrusted code is handled by the
other half of this proposal: the environment refusing to admit the generator in the first place.

Stating this plainly matters, because the model should not be advertised as something it cannot deliver.

Requirements

  • A single access-control policy per execution, shared across multiple environments (some consumers
    run several environments against one shared adapter).
  • A global configuration persisting previously granted permissions, remembered between runs.
  • Session-only permissions, in addition to persisted ones.
  • An asynchronous API supporting prompts, so a decision can be escalated to the user.
  • Denial is either a throw or a prompt.
  • Must work in non-interactive execution (CI, servers), where the answer is a defined deny rather
    than an accident of how a prompt library behaves without a TTY.

High-level decisions

  • Two halves, enforced in different places. Generator execution is admission control, decided by the
    environment at compose time (asynchronous, so it can prompt). File access is decided by a policy and
    enforced at the point the process actually touches the filesystem.
  • The boundary is real filesystem access. In-memory operations on the file store touch neither disk
    reads nor disk writes and are not gated by default. This is a deliberate trade: the store is a shared
    cache, so a file read by one generator can be retrieved by another without any disk access. Under a
    single per-execution policy this is harmless, since anything in the store was authorized by the same
    policy that governs the reader. It only becomes limiting if per-generator scoping is wanted later.
  • Synchronous enforcement over pre-resolved decisions. Some file access happens through synchronous
    APIs, so the enforcement check cannot prompt. Permissions are therefore resolved ahead of time — from
    the implicit roots, from the persisted configuration, or at an asynchronous boundary where the user can
    be prompted — and the synchronous check is a pure lookup that throws when a path is not covered.
  • Permissions live in their own configuration file, not in the existing global generator config.
    That file is written by generators themselves, so storing permissions there would let a generator grant
    itself access. The permissions file is additionally on an absolute deny-list.
  • Deny always wins over grant, and persisted decisions are versioned per package so that grants can be
    invalidated when a package changes major version.
  • The generic file-access interception point belongs in the file-store layer, where every read and write
    already converges. The policy, its configuration and its prompts belong to the adapter, which is already
    the shared, per-execution object and already owns user interaction.

Scope

  • @yeoman/adapter — the interfaces, the policy implementation, global configuration and prompting.
  • yeoman-environment — wiring, generator admission control, enforcement on commit.
  • yeoman-generator — boundary validation and runtime access requests.
  • mem-fs / mem-fs-editor — a small, generic interception hook (tracked separately, linked below).

Future work

Allowing a generator to declare the access it needs, so the environment can resolve permissions during
lookup, is a follow-up. It is worth designing for deliberately: importing a module already executes its
code, so a declaration that can only be read after the import is of limited use, whereas a package manifest
is readable beforehand. The first iteration does not need it — permissions come from the implicit roots, the
persisted configuration, and prompting at asynchronous boundaries.

Implementation details follow in the first comment.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions