#!/usr/bin/env bats # Unit test: mongodb-rd cozyrds must deliver the TLS trust anchor without # private key material. # # The PSMDB operator creates -ca-cert holding the CA PRIVATE KEY and # -ssl / -ssl-internal holding server private keys. The tenant # must receive only the key-free .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 }