Help us shape the future of Go! Take the 2026 Go Developer Survey

audit_readonly_graphql

command
v3.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 20, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Command audit_readonly_graphql fails when an action the canonical catalog classifies ReadOnly can reach a GraphQL mutation.

--read-only removes actions through FilterReadOnlyActions, which keys on the action's catalog classification, not on what its handler does. An action classified ReadOnly whose handler issues a GraphQL mutation therefore survives the filter and executes a write precisely where a write is supposed to be impossible, with nothing anywhere reporting it: GitLab performs the write, because the credential's scope is whatever the caller's token actually carries. The surface served to a read_api token used to be narrowed the same way; it is now derived from what each handler sends (register row AUT-001), and a mutation needs api, so that narrowing withholds such an action and the operator's flag is the one left exposed.

The HTTP method cannot be the test. client-go sends every GraphQL request as a POST, so around twenty read-only actions legitimately POST and the verb separates nothing. The operation type in the document is the whole of the distinction, and it is in the source: a mutation is "mutation ..." in the document handed to GraphQL.Do, against "query ..." or a bare selection set for a read.

So the audit resolves every read-only catalog action to the function its route runs, walks what that function can call, and classifies every GraphQL document those bodies name. An action that sends no GraphQL is not a finding, and neither is a mutation reached from an action already classified as mutating.

Where the documents come from

The inventory is cmd/internal/graphqldocs, the same reading the schema gate judges, and only the operation-type rule is this command's own. It used to have a document walk of its own that read string constants and package-level variables, which is every document this repository writes today and not every document it may write tomorrow: a document moved into a .graphql file and pulled in with an embed directive folds to nothing for the type checker, so that walk saw none of it while the schema gate read it straight off disk. Two detectors of the same thing disagreeing is how a gate ends up answering a narrower question than the one it prints.

What one shared inventory does not fix is attribution. This audit places a document through the object that declares it, or through the body that writes it inline; a .graphql file belongs to no function and no object, so it can be read and still not be tied to the handler that sends it. Rather than pass what it cannot classify, the run reports every such document as a finding and exits non-zero, which is the same answer it gives to a read-only action whose handler it cannot resolve.

There is one rule, and it is not here

The inventory used to have a pre-filter of its own and this command used to have an operation-type rule of its own, and the two parted company in both directions. The pre-filter was the narrower one: it wanted the operation keyword at the very start of the comment-stripped text, where the rule here accepted it at the start of any line, and it refused a brace-wrapped single word with no space in it, the shape an OpenTelemetry unit annotation is written in. A mutation written in either shape left the inventory and was judged by nothing, while this command went on printing that no read-only action reaches a mutation, having read fewer documents than its own rule described. Nothing in the repository was written that way, so it was latent; a gate that narrows silently is the class this repository keeps writing gates against, so it is not a state to leave alone.

Both questions are answered by the inventory now. graphqldocs.LooksLikeDocument says whether a string is a document, and graphqldocs.DefinesMutation says whether it carries a mutation, so this command sees exactly the documents the schema gate judges and every one of them is classified. What stays here is what the answer means, which is that an action classified ReadOnly must not be able to reach a mutation, and the two shapes above are the ones the convergence had to keep: both have a test in this package that drives the real loader over a fixture written that way.

Neither rule depends on where a keyword sits on its line any longer (issue 1321). A document that parses is classified by gqlparser, so a query and a mutation written on one line is a mutation; a shell that does not parse is read by the line rule, which accepts the keyword behind a comma, a byte order mark, a closing brace or a hole as well as at the start of a line, and the inventory admits both. A test drives the real loader over a fixture written in each of those shapes.

One narrowness is left, on purpose and in one place: a bare selection set of a single field with no spaces, `{id}`, is a unit annotation as far as any text rule can tell, and is refused. That costs nothing this gate is about. The brace form declares no operation, so it is a read by construction, and a mutation cannot be written without its keyword.

Usage:

go run ./cmd/audit_readonly_graphql/
go run ./cmd/audit_readonly_graphql/ -v

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL