Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions docs/node-fleet-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ Use a branch ref for live defaults or a full commit SHA for fixed policy.
The inventory ref used to fetch this list is independent of the defaults ref
inside it. See [shared defaults](shared-defaults.md) for inheritance rules.

This repository's [inventory](../defaults/node-repositories.json) lists ten public
repositories: Hydra, OmegaConf, and eight personal Node projects. Repositories
that are not installed yet remain listed so their missing setup stays visible.
This repository's [inventory](../defaults/node-repositories.json) lists Hydra and
OmegaConf. Repositories that are not installed yet can also remain in an
inventory so their missing setup stays visible.
Other maintainers can keep their own inventories in their own repositories; no
registration here is required.

Expand Down Expand Up @@ -107,7 +107,12 @@ secrets:

The optional token goes only to the scanner. The reporter uses the host's
built-in token. Shared defaults sources remain public, as required by the
maintenance resolver. No private key or secret content is inspected.
maintenance resolver. Pinned central workflow repositories must also be public:
the scanner reads their metadata and files without authentication. The optional
token remains in use for the inventory, listed repositories, and their Actions
status. This avoids applying the central organization's token policies to a
credential intended for your private repositories. No private key or secret
content is inspected.

## 3. Run and inspect results

Expand Down
4 changes: 3 additions & 1 deletion node/runtime/fleet.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ def __init__(self, token=""):
self.defaults_lock = Lock()
# Defaults remain public and unauthenticated, as in the individual audit.
self.cached_defaults = functools.cache(CONFIGURATION["public_read"])
# Public workflow metadata must not inherit private repository credentials.
self.public_github = GitHub() if token else self

def read_defaults(self, url, accept="application/json"):
# Resolve each shared ref once, even when several repositories need it.
Expand Down Expand Up @@ -442,7 +444,7 @@ def check_caller(root, config, selected, github):
raise ValueError(
"Caller must serialize runs with concurrency and cancel-in-progress: false"
)
source = github.snapshot(match[1], match[2])
source = github.public_github.snapshot(match[1], match[2])
contract = CONFIGURATION["json_object"](
source.read("node/contract.json"), "pinned Node contract"
)
Expand Down
55 changes: 55 additions & 0 deletions node/tests/test_fleet_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import base64
import copy
import hashlib
import io
import json
import runpy
import subprocess
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from threading import Barrier, Event, Lock
Expand Down Expand Up @@ -884,6 +887,58 @@ def opener(handler):
assert observed[0].get_header("Authorization") == "Bearer test-token"


@pytest.mark.parametrize("central_error", [None, 403])
def test_public_workflow_reads_do_not_inherit_private_monitoring_token(
monkeypatch, central_error
):
server = GitHub("npm")
observed = []

class Opener:
def open(self, request, timeout):
path = urlparse(request.full_url).path.lstrip("/")
if urlparse(request.full_url).query:
path += "?" + urlparse(request.full_url).query
authorization = request.get_header("Authorization")
observed.append((path, authorization))
if path.startswith("repos/owner/actions"):
# Reproduce an organization rejecting an otherwise valid PAT.
if authorization or central_error:
raise urllib.error.HTTPError(
request.full_url, 403, "organization policy", {}, None
)
else:
# Inventory, project files and Actions state still need the PAT.
assert authorization == "Bearer test-token"
return io.BytesIO(json.dumps(server.get(path)).encode())

monkeypatch.setattr(urllib.request, "build_opener", lambda handler: Opener())
github = FLEET["GitHub"]("test-token")
github.read_defaults = server.public
source = {
"repository": "owner/settings",
"path": "defaults/repos.json",
"ref": "main",
}
report = FLEET["check_fleet"](github, source)
result = report["results"][0]
assert result["status"] == ("unverified" if central_error else "ok")
central_reads = [
item for item in observed if item[0].startswith("repos/owner/actions")
]
assert central_reads and all(auth is None for _, auth in central_reads)
if central_error:
assert "HTTP 403" in result["diagnostic"]
assert len(central_reads) == 1 # No retry with the private credential.
else:
assert any("git/blobs/" in path for path, _ in central_reads)
assert any("actions/workflows/" in path for path, _ in observed)
# Repeated installations share the unauthenticated pinned snapshot.
before = len(observed)
assert FLEET["check_fleet"](github, source)["status"] == "ok"
assert len(observed) == before


def test_pinned_workflow_must_support_the_resolved_toolchain():
github = GitHub()
support = json.loads(github.repos["owner/actions"]["node/runtime/toolchain.json"])
Expand Down
Loading