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

147 lines
7.2 KiB
Bash

#!/usr/bin/env bats
# Unit test: mongodb-rd cozyrds must deliver the TLS trust anchor without
# private key material.
#
# The PSMDB operator creates <release>-ca-cert holding the CA PRIVATE KEY and
# <release>-ssl / <release>-ssl-internal holding server private keys. The tenant
# must receive only the key-free <release>.tenant-ca projection, reached by
# label. These names are the contract between this file and the operator, and
# nothing else in the repo checks them at this level.
#
# The declaration that names the extraction source is a TenantProjection the
# chart renders, covered by packages/apps/mongodb/tests/tenant_projection_test.yaml.
load test_helper
REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
COZYRDS="$REPO_ROOT/packages/system/mongodb-rd/cozyrds/mongodb.yaml"
# Bound explicitly rather than read from BATS_TEST_FILENAME inside a test body.
# These files run under hack/cozytest.sh, not bats: it executes each body with
# set -u and never exports that variable, so a bare reference aborts the test —
# and the runner stops at the first failure, taking every later test with it.
# $0 is not a usable fallback either, since it expands to the runner's own path
# and would silently scan the wrong file.
SELF="$REPO_ROOT/hack/check-mongodb-rd-secrets.bats"
@test "mongodb-rd checks use the set -e-safe negation form" {
# Guards the pattern itself, so the next negated assertion added here cannot
# be silently dead. `!`-prefixed commands are exempt from set -e.
# SELF is a hardcoded path and grep exits 2 on a missing file, which an `if`
# reads as simply false — so renaming this file would turn the guard green
# while it inspected nothing. Every other check here reads $COZYRDS through
# `grep -q` or a `$(yq ...)` assignment, both of which abort under set -e;
# this is the one input that can vanish quietly.
if [ ! -f "$SELF" ]; then
echo "SELF does not resolve ($SELF); this guard would be vacuous" >&2
exit 1
fi
# [[:space:]] rather than \s: the latter is a GNU/PCRE extension, not POSIX
# ERE, and degrades to a literal 's' on a BSD grep — which would make this
# guard vacuous, the one property a vacuity guard must not have.
if grep -nE '^[[:space:]]*![[:space:]]' "$SELF"; then
echo "Negated assertion above is exempt from set -e; use if/exit 1 instead" >&2
exit 1
fi
}
@test "mongodb-rd cozyrds selects the key-free projection by label" {
grep -q 'internal.cozystack.io/tenant-ca: "true"' "$COZYRDS"
}
@test "mongodb-rd cozyrds secrets.include path resolves" {
# Positive control for the two structural checks below. Both walk
# .spec.secrets.include, and yq returns 0 for a path that does not exist — so
# a rename of that field would leave them green while asserting nothing. The
# grep-based tests would not notice either, since the strings stay in the
# file. Deleting `include` outright is already caught by the label grep; this
# covers the rename.
n="$(yq '.spec.secrets.include // [] | length' "$COZYRDS")"
if [ "$n" -lt 2 ]; then
echo "spec.secrets.include has $n entries; the structural checks would be vacuous" >&2
exit 1
fi
}
@test "mongodb-rd cozyrds has no catch-all secrets selector" {
# The mine this guards: an include entry with an empty matchLabels and no
# resourceNames. LabelSelectorAsSelector({}) resolves to labels.Everything()
# and a nil resourceNames matches any name, so such an entry promotes EVERY
# Secret in the namespace — including the key-bearing TLS Secrets — to the
# tenant. Checked structurally rather than by grep, because the correct
# selector being present says nothing about a second, wider one alongside it.
empty="$(yq '[.spec.secrets.include[]
| select((.matchLabels // {} | length) == 0 and (.resourceNames // [] | length) == 0)]
| length' "$COZYRDS")"
if [ "$empty" != "0" ]; then
echo "Found $empty secrets.include entry/entries matching every Secret in the namespace" >&2
exit 1
fi
}
@test "mongodb-rd cozyrds does not hand the tenant any key-bearing secret by name" {
# resourceNames entries are name grants. The CA and leaf secrets each carry a
# private key, so neither may appear as one.
#
# Matched on the suffix over the parsed resourceNames lists, not on a literal
# spelling: the same object can be written "mongodb-{{ .name }}-ca-cert" or
# "{{ .release }}-ca-cert", and a grep pinned to either form misses the other.
# Walking the yq path also confines the check to grants, so a legitimate
# mention of one of these names elsewhere in the file cannot trip it.
leaks="$(yq '[.spec.secrets.include[].resourceNames[]?
| select(test("-(ca-cert|ssl|ssl-internal|ssl-old|ssl-internal-old)$"))]
| length' "$COZYRDS")"
if [ "$leaks" != "0" ]; then
echo "Found $leaks key-bearing secret(s) granted to the tenant by name" >&2
exit 1
fi
}
@test "mongodb-rd cozyrds excludes every key-bearing secret" {
# The exclude list is the structural backstop behind the label-only include
# selector, and nothing else pins it: deleting it leaves every other check in
# this file green. Asserted positively — a count alone would go vacuous on a
# renamed path, the same way the include checks would without their own
# positive control.
for name in ca-cert ssl ssl-internal ssl-old ssl-internal-old \
percona-server-mongodb-users mongodb-encryption-key mongodb-keyfile s3-creds; do
hits="$(yq "[.spec.secrets.exclude[].resourceNames[]? | select(. == \"mongodb-{{ .name }}-$name\")] | length" "$COZYRDS")"
if [ "$hits" != "1" ]; then
echo "mongodb-{{ .name }}-$name is not excluded (found $hits entries)" >&2
exit 1
fi
done
}
@test "mongodb-rd cozyrds excludes the operator-internal secrets" {
# These carry the internal- prefix rather than a release suffix, so they do
# not fit the loop above. Quoted: the template braces contain spaces, and an
# unquoted list item would word-split into "internal-mongodb-{{", ".name",
# "}}" and assert on three names that do not exist.
for name in "internal-mongodb-{{ .name }}" "internal-mongodb-{{ .name }}-users"; do
hits="$(yq "[.spec.secrets.exclude[].resourceNames[]? | select(. == \"$name\")] | length" "$COZYRDS")"
if [ "$hits" != "1" ]; then
echo "$name is not excluded (found $hits entries)" >&2
exit 1
fi
done
}
@test "mongodb-rd cozyrds still delivers the credentials secret" {
# The exclude list added alongside the trust anchor is evaluated BEFORE
# include and wins outright, so it now sits upstream of this pre-existing
# grant. Every other line in that block is about withholding a Secret, which
# makes adding this one to it a plausible edit — and one that would silently
# revoke the tenant's access to their own connection string. Assert both
# directions.
inc="$(yq '[.spec.secrets.include[].resourceNames[]? | select(. == "mongodb-{{ .name }}-credentials")] | length' "$COZYRDS")"
if [ "$inc" != "1" ]; then
echo "credentials is not granted via include (found $inc entries)" >&2
exit 1
fi
exc="$(yq '[.spec.secrets.exclude[].resourceNames[]? | select(. == "mongodb-{{ .name }}-credentials")] | length' "$COZYRDS")"
if [ "$exc" != "0" ]; then
echo "credentials appears in exclude, which wins over include" >&2
exit 1
fi
}