Files
cozystack/hack/check-nats-rd-secrets.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

136 lines
6.7 KiB
Bash

#!/usr/bin/env bats
# Unit test: the nats trust anchor reaches tenants as a key-free projection, and
# the two key-BEARING Secrets the chart creates never do.
#
# The chart issues nats-<name>-ca (CA certificate AND CA private key, the latter
# under tls.key) and nats-<name>-tls (server certificate AND server private key).
# Neither may be handed to a tenant: read access to the CA key would let the
# holder issue certificates for anything rather than merely verify the server.
#
# What a client actually needs is ca.crt alone. That is delivered by the
# CA-extraction controller as the key-free <release>.tenant-ca projection,
# selected here by LABEL rather than by name. The label is stamped only on an
# object the controller itself produced, whereas a name grant conveys whatever
# happens to occupy the name.
#
# These assertions exist because both halves are one edit away from breaking
# with a fully green chart suite, for two different reasons. The
# ResourceDefinition is rendered, by the nats-rd chart inlining cozyrds/*, but
# that chart carries neither a tests directory nor the test target
# hack/helm-unit-tests.sh looks for.
# The dashboard Role is rendered by a chart that has both, and no suite under
# packages/apps/nats/tests/ asserts on it.
#
# The grants are asserted as WHITELISTS against rendered output rather than as
# a blacklist of forbidden names over source text. A blacklist grepping for
# "{{ .Release.Name }}-ca" is defeated by quoting the scalar, by $.Release.Name,
# or by a fullname helper -- all of which grant the CA private key while the
# guard stays green.
load test_helper
REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
COZYRDS="$REPO_ROOT/packages/system/nats-rd/cozyrds/nats.yaml"
CHART="$REPO_ROOT/packages/apps/nats"
# The complete set of grants the tenant holds, asserted as one value rather than
# per-dimension: a name whitelist alone passes an added label selector, and a
# label whitelist alone passes an added name. Either dimension can reach a
# key-bearing Secret -- controller.cert-manager.io/fao is stamped on both of
# them, so a selector on it is a one-line grant of the CA private key.
EXPECTED_RD_INCLUDE='[{"resourceNames":["nats-{{ .name }}-credentials"]},{"matchLabels":{"internal.cozystack.io/tenant-ca":"true"}}]'
# The dashboard template's entire RBAC output: every rule of the Role, and the
# kinds it renders. Asserted whole rather than filtered down to the rules that mention
# secrets, because every filter admits the spelling it did not anticipate: a rule
# with no resourceNames names no Secret, a rule with resources ["*"] names no
# resource, and a ClusterRole is not a Role. Each of those reaches both
# key-bearing Secrets. There is no filter that is right for all of them, and one
# comparison against the whole output needs none.
EXPECTED_ROLE_RULES='[{"apiGroups":[""],"resources":["services"],"resourceNames":["nats-test"],"verbs":["get","list","watch"]},{"apiGroups":[""],"resources":["secrets"],"resourceNames":["nats-test-credentials"],"verbs":["get","list","watch"]},{"apiGroups":["cozystack.io"],"resources":["workloadmonitors"],"resourceNames":["nats-test"],"verbs":["get","list","watch"]}]'
EXPECTED_RBAC_KINDS='["Role","RoleBinding"]'
# Subsumed by the whitelist below, which cannot match without this entry. Kept
# because the runner stops at the first failing test, so this one fires first
# and says what the loss costs rather than printing two JSON blobs to diff.
@test "nats-rd cozyrds selects the key-free tenant-CA projection by label" {
if [ ! -f "$COZYRDS" ]; then
echo "ResourceDefinition not found at $COZYRDS -- did it move?" >&2
exit 1
fi
count=$(yq eval \
'[.spec.secrets.include[] | select(.matchLabels."internal.cozystack.io/tenant-ca" == "true")] | length' \
"$COZYRDS") || exit 1
if [ "$count" -lt 1 ]; then
echo 'nats-rd does not select internal.cozystack.io/tenant-ca: "true"' >&2
echo "Without it the tenant cannot read ca.crt and TLS verification is impossible." >&2
exit 1
fi
}
@test "nats-rd cozyrds grants nothing beyond the credentials Secret and the trust anchor" {
if [ ! -f "$COZYRDS" ]; then
echo "ResourceDefinition not found at $COZYRDS -- did it move?" >&2
exit 1
fi
include=$(yq eval --output-format=json --indent=0 '.spec.secrets.include' "$COZYRDS") || exit 1
if [ "$include" != "$EXPECTED_RD_INCLUDE" ]; then
echo "Unexpected nats-rd spec.secrets.include: $include" >&2
echo "Expected exactly: $EXPECTED_RD_INCLUDE" >&2
echo "nats-<name>-ca holds the CA private key and nats-<name>-tls the server key;" >&2
echo "neither may be granted to a tenant, by name or by label." >&2
exit 1
fi
}
# Asserted against RENDERED output, so the spelling used in the template --
# quoted, helper-derived, or otherwise -- cannot smuggle a name past the check.
@test "nats dashboard Role renders exactly the grants it is supposed to" {
if [ ! -d "$CHART" ]; then
echo "Chart not found at $CHART -- did it move?" >&2
exit 1
fi
# stderr is deliberately not discarded: when the render breaks, helm's own
# message is the only thing that says why, and a CI red showing just
# "helm template failed" sends the reader back here to rerun it by hand.
rendered=$(helm template nats-test "$CHART" \
--namespace tenant-test \
--set '_cluster.cluster-domain=cozy.local' \
--show-only templates/dashboard-resourcemap.yaml) || {
echo "helm template failed for $CHART" >&2
exit 1
}
# eval-all aggregates the documents into one result, so the Role may sit at any
# position among them.
kinds=$(printf '%s\n' "$rendered" | yq eval-all --output-format=json --indent=0 \
'[.kind]' -) || exit 1
if [ "$kinds" != "$EXPECTED_RBAC_KINDS" ]; then
echo "Unexpected RBAC kinds rendered by the nats dashboard template: $kinds" >&2
echo "Expected exactly: $EXPECTED_RBAC_KINDS" >&2
echo "A ClusterRole here would grant across namespaces and is not covered by" >&2
echo "the rule comparison below, which reads the namespaced Role." >&2
exit 1
fi
# Fails closed on an empty list: a Role that stopped granting anything, or a
# render that silently produced nothing, does not match the expected rules.
rules=$(printf '%s\n' "$rendered" | yq eval-all --output-format=json --indent=0 \
'[select(.kind == "Role") | .rules[]]' -) || exit 1
if [ "$rules" != "$EXPECTED_ROLE_RULES" ]; then
echo "Unexpected rules in the nats dashboard Role: $rules" >&2
echo "Expected exactly: $EXPECTED_ROLE_RULES" >&2
echo "nats-<release>-ca holds the CA PRIVATE KEY and nats-<release>-tls the" >&2
echo "server private key. The trust anchor reaches the tenant as the key-free" >&2
echo "<release>.tenant-ca projection via tenantsecrets, not by a Role grant." >&2
exit 1
fi
}