Files
cozystack/hack/cozystack-controller-secret-rbac.bats
T
Myasnikov Daniil cc942fed68 test(tests): run unit suites with Bats and retain POSIX coverage
The shell translator rewrites test bodies and loses Bats reporting.
The unit lane needs complete TAP results and JUnit reports without
changing how live-cluster suites run.

Sourced production helpers still need a dash pass, and unit test bodies
need nounset without exporting shell options into subprocesses.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 11:04:00 +03:00

122 lines
5.8 KiB
Bash

#!/usr/bin/env bats
# -----------------------------------------------------------------------------
# Semantic guard on what packages/system/cozystack-controller grants over core
# Secrets.
#
# The union is over every ClusterRole and Role the chart renders; bindings are
# never consulted. Today that is the same question as "what does the controller's
# own ServiceAccount hold", because the chart's ClusterRoleBinding and RoleBinding
# both name the cozystack-controller ServiceAccount and nothing else. A role added
# here for some other subject would fold into the union too, which errs toward
# refusing a grant rather than missing one.
#
# Two reconcilers share one Secret write grant: WildcardSecretReconciler
# replicates the operator wildcard TLS Secret into each tenant namespace, and
# CACertReconciler writes -- and withdraws -- the key-free "<release>.tenant-ca"
# projection. The controller is cluster-scoped, so any verb it holds on Secrets
# it holds on every Secret in every tenant namespace. That is why the set is
# pinned rather than merely bounded from above: a widening reaches all tenants,
# and a narrowing breaks both reconcilers.
#
# The check reads effective permission off the rendered manifests, not manifest
# text. A rule reaches core Secrets when its apiGroups admit "" and its
# resources admit "secrets" -- by name or via "*" on either axis -- and a "*" in
# its verbs expands to the verb set the API server defines for a namespaced
# resource. The union over every ClusterRole and Role the chart renders must
# equal the declared set exactly.
#
# Expansion is what makes the guard hold on the chart as it stands: the chart
# ends with a blanket `apiGroups: ['*'] resources: ['*'] verbs: [get,list,watch]`
# read rule, which does reach Secrets and whose verbs are already inside the
# declared set. A guard written as "no wildcard anywhere near secrets" would be
# red on an unmodified tree.
#
# The union is over the chart's default rendering: `helm template` is invoked
# with no --values, so a rule that appeared only under some values file would go
# unseen. templates/rbac.yaml carries no templating at all today, so nothing is
# gated behind a value; render the variants here too if that changes.
#
# The check lives here rather than beside the chart's helm-unittest suite because
# helm-unittest compares every field it is handed by value equality. `any: true`
# changes which fields are compared, not how they are compared, so neither the
# whole-rule form nor the `any: true` one can bound what a verbs list may hold --
# `verbs: ["get", "*"]` slips past both. That primitive is the right one for
# pinning a rule as written, which tests/rbac_test.yaml does, and it cannot
# express an upper bound: a wildcard in resources, a wildcard in apiGroups, a "*"
# appended to the declared verb list, an extra field beside the verbs, and a
# wider grant added as a separate rule while the declared one stays untouched all
# pass it.
#
# resourceNames is deliberately ignored. A rule scoped to named Secrets that
# grants "*" on them is still a wildcard verb grant, so the union counts it.
#
# An aggregated ClusterRole is the one widening this cannot see: it renders with
# no `rules` of its own, so whatever the aggregation confers stays outside the
# union. This chart declares no aggregationRule; a rule set assembled that way
# would need its own check.
#
# The assertion is an equality against a non-empty expected set, so a chart that
# fails to render, or a yq/jq breakage that yields nothing, fails the test
# rather than passing it vacuously.
#
# Requires: helm, yq (mikefarah v4+), jq -- the same three
# hack/admin-kubeconfig-invariant.bats needs to render and inspect a chart.
#
# Run with: bats hack/cozystack-controller-secret-rbac.bats
# -----------------------------------------------------------------------------
load test_helper
# What "*" in a rule's verbs means for a namespaced resource such as Secret.
VERB_UNIVERSE='["create","delete","deletecollection","get","list","patch","update","watch"]'
# The grant the chart declares, as a sorted set: read for both reconcilers plus
# the writes the two of them need. Sorted because the comparison below is on
# `unique`d jq output, which is sorted.
DECLARED_SECRET_VERBS="create,delete,get,list,patch,update,watch"
@test "cozystack-controller grants exactly the declared verb set on core Secrets in any spelling" {
chart="packages/system/cozystack-controller"
[ -d "$chart" ]
tmp=$(mktemp -d)
# helm's stderr is left alone: this chart renders silently, so there is nothing
# to suppress, and a chart that stops rendering should say why in the log
# rather than fail this line mutely.
helm template cozystack-controller "$chart" \
--namespace cozy-system \
> "$tmp/rendered.yaml"
# yq streams one JSON object per rendered document; jq -s slurps the stream so
# the rules of every ClusterRole and Role are one collection.
yq --output-format=json eval-all '.' "$tmp/rendered.yaml" \
| jq -s '
map(select(.kind == "ClusterRole" or .kind == "Role"))
| map(.rules // []) | add // []
| map(select(
((.apiGroups // []) | any(. == "" or . == "*")) and
((.resources // []) | any(. == "secrets" or . == "*"))
))
' > "$tmp/secret-rules.json"
effective=$(
jq --raw-output --argjson universe "$VERB_UNIVERSE" '
map(if ((.verbs // []) | any(. == "*")) then $universe else (.verbs // []) end)
| add // [] | unique | join(",")
' "$tmp/secret-rules.json"
)
if [ "$effective" != "$DECLARED_SECRET_VERBS" ]; then
echo "Effective verb set on core Secrets is not the declared one." >&2
echo " declared: $DECLARED_SECRET_VERBS" >&2
echo " rendered: $effective" >&2
echo "Rules reaching core Secrets:" >&2
cat "$tmp/secret-rules.json" >&2
exit 1
fi
echo "Effective verbs on core Secrets: $effective"
rm -rf "$tmp"
}