celmatch

package
v0.0.4 Latest Latest
Warning

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

Go to latest
Published: Sep 27, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package celmatch evaluates a strict subset of CEL against a claim set.

It exists to answer one question — "may this workload identity be trusted?" — from an expression an operator typed into a settings form. That is the same job Google's workload identity federation gives its `attribute_condition`, and the syntax here is deliberately the syntax an operator would write there, because someone federating GitHub Actions has almost certainly read those docs.

Why a subset rather than cel-go

This is a security gate, and the property that matters is that an operator who reads an expression and a machine that evaluates it reach the same conclusion. Full CEL is a large language: macros that bind variables (`all`, `exists`, `map`), dynamic dispatch, timestamp and duration arithmetic, protobuf well-known types, optional chaining. Every one of those is a way for the two readings to diverge, and none of them is needed to say "this repository, on this branch, in this environment".

So the grammar below is closed. It accepts literals, identifiers, field selection, four string methods, membership, equality, and boolean connectives. Anything else is a *parse* error, surfaced when the rule is saved rather than when a pipeline presents a token — which is the whole point of refusing early. An operator who needs more than this is describing a policy the audit reader cannot check by eye, and should be writing several narrow rules instead of one wide one.

Failure is denial, never permission

Compile refuses anything it does not fully understand. Eval returns an error — not false — when an expression references a claim that is absent or compares values of different types, and every caller is required to treat a non-nil error as "no match". There is deliberately no lenient mode: the failure modes of a trust policy are asymmetric, and an expression that evaluates to true because a claim was missing is the one outcome worth engineering against.

The conformance contract

Being a subset is only safe in one direction. The operator who writes a rule takes their mental model from GitHub's and Google's OIDC documentation, both of which describe real CEL — so any expression this evaluator admits and CEL would deny is an authorization bypass that the operator cannot find by re-reading their own policy. Their policy means what they think it means; this evaluator is the thing that disagrees. The reverse costs nothing: a refusal is a denial with a clearer message.

So the invariant, asserted in conformance_test.go, is one-directional:

celmatch admits  =>  real CEL admits

It is held against cel.dev/cel-go rather than against anybody's reading of the specification. testdata/conformance.json is the corpus, testdata/celprobe is a nested module that runs it through the reference implementation, and testdata/conformance-table.txt is the side-by-side that results — currently ~50 rows where the two disagree, every one of them this package being stricter. FuzzCompile and FuzzEvalClaims extend the same invariant past the corpus using the oracle in oracle_test.go.

Before changing any semantics here, read that table. Several of the divergences look like bugs and are not: no commutative absorption for `&&`/`||`, an error rather than false for cross-type equality, and a refusal rather than a verdict for a mixed-type list literal are all load-bearing.

Index

Constants

View Source
const MaxExpressionBytes = 4096

MaxExpressionBytes bounds the source an operator may submit. Expressions are typed into a form by a human; anything past this is either a mistake or an attempt to find a parser limit, and both are better refused at the door than discovered by the parser's recursion depth.

View Source
const RootVar = "assertion"

RootVar is the name an expression uses to reach the token's claims. It matches the identifier Google's workload identity federation uses, so an expression written against those docs compiles here unchanged.

Variables

View Source
var ErrNoSuchKey = errors.New("celmatch: no such key")

ErrNoSuchKey reports a selector naming a claim the token did not carry.

It is a distinct error because callers want to tell an operator the difference between "your expression is wrong" and "this token does not have that claim" — the second is routine when a workflow runs outside an environment, and reads as a typo if reported as a generic failure.

Functions

This section is empty.

Types

type Program

type Program struct {
	// contains filtered or unexported fields
}

Program is a compiled expression. It is immutable after Compile and safe for concurrent use.

func Compile

func Compile(src string) (*Program, error)

Compile parses src and returns a Program, or an error describing the first thing it could not accept.

The error is written for the operator who typed the expression, and names the byte offset, because a policy editor that says only "invalid" leaves someone bisecting a one-line expression by hand.

func (*Program) Eval

func (p *Program) Eval(claims map[string]any) (bool, error)

Eval evaluates the program against claims and reports whether it matched.

A non-nil error means the expression could not be evaluated — a missing claim, or a comparison between values of different types — and the caller must treat that as "no match". Eval never returns (true, non-nil error).

func (*Program) References

func (p *Program) References() []string

References returns the claim names the expression reads, sorted and deduplicated. Only selections rooted at the activation variable are reported; `assertion.repository` yields "repository".

func (*Program) Source

func (p *Program) Source() string

Source returns the expression text the Program was compiled from.

Jump to

Keyboard shortcuts

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