chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
knotless-run: A run says which secrets it was not granted
.changeset/secret-not-granted.mdnew+7−0
| @@ -0,0 +1,7 @@ | ||
--- | ||
default: patch | ||
--- | ||
| ||
# A run says which secrets it was not granted | ||
| ||
A run whose workflow lists a secret the runner does not grant it now says so at the top of its log, `==> NAME is not granted to <repo> at <ref>`, rather than leaving the secret silently absent. A pull's or merge-queue batch's run says the same of each, as it gets none. | ||
crates/knotless-run/src/lib.rs+31−6
| @@ -2606,7 +2606,7 @@ impl Runner { | ||
cancelled, | ||
said, | ||
} = started; | ||
let (vars, secrets) = self.vars(watch, subject, wf)?; | ||
let (vars, secrets, ungranted) = self.vars(watch, subject, wf)?; | ||
let dir = match &self.opts.work { | ||
Some(work) => { | ||
std::fs::create_dir_all(work)?; | ||
| @@ -2649,6 +2649,7 @@ impl Runner { | ||
let given = given(subject, wf).map(|(k, v)| format!(" {k}={v}")); | ||
writeln!(log, "==> asked for by name{}", given.collect::<String>())?; | ||
} | ||
log.write_all(not_granted(&ungranted, watch, subject).as_bytes())?; | ||
// What it writes goes to the appview as it is written, ending | ||
// once all of it is, before it is filed. | ||
let rkey = run_rkey(watch, subject, &wf.name); | ||
| @@ -2966,13 +2967,19 @@ impl Runner { | ||
/// What a step sees beyond the runner's tools: the workflow's own | ||
/// variables, those a run asked for by name was given, the run's, and | ||
/// the secrets both the workflow asks for and | ||
/// the runner grants this repository, never to a pull's run; and those | ||
/// secrets' values, to keep out of the log. | ||
fn vars(&self, watch: &Watch, subject: &Subject, wf: &Workflow) -> Result<(Vars, Vec<String>)> { | ||
/// the runner grants this repository, never to a pull's run; those | ||
/// secrets' values, to keep out of the log; and the names of those it | ||
/// asks for and is not given. | ||
fn vars( | ||
&self, | ||
watch: &Watch, | ||
subject: &Subject, | ||
wf: &Workflow, | ||
) -> Result<(Vars, Vec<String>, Vec<String>)> { | ||
let mut env = wf.env.clone(); | ||
env.extend(given(subject, wf).cloned()); | ||
let mut vars: Vars = env.into_iter().collect(); | ||
let mut secrets = Vec::new(); | ||
let (mut secrets, mut given) = (Vec::new(), Vec::new()); | ||
let pull = subject.pull.as_ref().map(|p| p.uri.as_str()); | ||
for (k, v) in [ | ||
("KNOTLESS_RUN", Some("1")), | ||
| @@ -2998,13 +3005,31 @@ impl Runner { | ||
if let Some(value) = granted.remove(name).and_then(|s| s.to(&subject.ref_name)) { | ||
secrets.push(value.clone()); | ||
vars.push((name.clone(), value)); | ||
given.push(name); | ||
} | ||
} | ||
} | ||
Ok((vars, secrets)) | ||
let ungranted = wf.secrets.iter().filter(|n| !given.contains(n)); | ||
Ok((vars, secrets, ungranted.cloned().collect())) | ||
} | ||
} | ||
| ||
/// What a run's log says of each secret its workflow asks for and it is | ||
/// not given, which would otherwise be simply absent: a pull's or a | ||
/// batch's run gets none, and a push gets only those the runner grants | ||
/// its repository at its ref. | ||
fn not_granted(names: &[String], watch: &Watch, subject: &Subject) -> String { | ||
let to = if subject.proposed() { | ||
"a pull's or a merge-queue batch's run, which gets no secrets".to_string() | ||
} else { | ||
format!("{} at {}", watch.label, subject.ref_name) | ||
}; | ||
let lines = names | ||
.iter() | ||
.map(|n| format!("==> {n} is not granted to {to}\n")); | ||
lines.collect() | ||
} | ||
| ||
/// Takes the grants to `label` out of the secrets file at `path`, if it | ||
/// has any, keeping the rest as they are. | ||
fn ungrant(path: &std::path::Path, label: &str) -> Result<()> { | ||
crates/knotless-run/tests/run.rs+11−0
| @@ -290,6 +290,13 @@ fn a_push_runs_its_workflows() { | ||
assert!(!log.contains("s3cret"), "{log}"); | ||
// A secret granted to some refs alone reaches only their pushes. | ||
assert!(log.contains("main=*** tags=none"), "{log}"); | ||
// The log says which it asked for and was not given. | ||
let label = format!("{}/proj", owner.account.handle); | ||
for name in ["UNGRANTED", "TAGS_ONLY"] { | ||
let said = format!("==> {name} is not granted to {label} at refs/heads/main\n"); | ||
assert!(log.contains(&said), "{log}"); | ||
} | ||
assert!(!log.contains("TOKEN is not granted"), "{log}"); | ||
| ||
let fail = check_of(&runner, &owner, &key, "fail"); | ||
assert_eq!(fail["status"], "failure"); | ||
| @@ -2172,6 +2179,10 @@ fn a_merged_pull_passes_its_push() { | ||
assert_eq!(ran("ci"), format!("{head}\n")); | ||
// A trusted pull's run that passed keeps what it wrote to its cache. | ||
assert_eq!(kept().len(), 1); | ||
// A pull's run of what asks for secrets gets none, and says so. | ||
let secret = log_of(&check_on(&runner, &owner, &key, &topic, "secret")); | ||
let none = "==> TOKEN is not granted to a pull's or a merge-queue batch's run, which gets no secrets\n"; | ||
assert!(secret.contains(none), "{secret}"); | ||
| ||
// Squashed into main: its tree is the one the pull ran at. | ||
let squashed = merged(&["merge", &url, &topic]); | ||
docs/design/runner.md+4−1
| @@ -746,7 +746,10 @@ only if the workflow lists it, the file grants it to the repository and, | ||
when the secret names `refs`, to the ref, and the run is not a pull's or a | ||
batch's. A secret limited to `refs/heads/main` cannot be read from another | ||
branch, which matters because a branch can change the workflow itself. A | ||
secret not granted is simply absent. | ||
secret not granted is absent from the step's environment, and the log | ||
names it before the steps (`==> NAME is not granted to <repo> at <ref>`), | ||
so a workflow that fails for want of one says why. The line names the | ||
secret, never a value. | ||
| ||
The runner re-reads the file on every run that needs secrets, so a change | ||
takes effect without a restart. A workflow that asks for none never reads | ||
docs/reference/workflows.md+4−2
| @@ -258,8 +258,10 @@ those refs: | ||
} | ||
``` | ||
| ||
`refs` match as a trigger's do. A secret not granted is simply absent. A | ||
pull's or merge-queue batch's run gets no secrets. In the log, which is | ||
`refs` match as a trigger's do. A secret not granted is absent, and the | ||
log says so before the steps: `==> NAME is not granted to <repo> at | ||
<ref>`. A pull's or merge-queue batch's run gets no secrets, and its log | ||
says that of each the workflow lists. In the log, which is | ||
public, each granted secret's value is replaced by `***`, and so is each | ||
trimmed line of a value with several lines, such as a PEM key, the longest | ||
first. Values shorter than four bytes are not masked. | ||