feat(db-bigquery): accept the service account key as a string, from the environment - #3039
Merged
Merged
Conversation
… string
`serviceAccountKey` is a `json`-typed property, and a json-typed slot takes
its value literally — the config compiler refuses reference indirection into
structured config, so an `{env: "..."}` reference is never resolved in one. A
deployment whose credentials live in an environment variable rather than on
disk therefore cannot reach the slot at all: the literal `{env: "..."}` object
is what lands in the credentials, and the SDK reports it as "the incoming JSON
object does not contain a client_email field" — an error that names neither
the property nor the reason.
Add `serviceAccountKeyJson` and `serviceAccountKeyJsonBase64`, both `secret`
strings, so a reference resolves and the connection parses what arrives. The
base64 form is for transports that mangle the quoting and embedded newlines of
raw JSON. Going through JSON also restores the private key's newlines from its
`\n` escapes, which supplying client_email/private_key as separate strings
does not.
Precedence is serviceAccountKey, then Json, then JsonBase64, then
client_email/private_key; an empty value is skipped, as an unset environment
variable produces. Parse failures name the property and what it expects
without quoting the value, since JSON.parse's own SyntaxError would put a
private key in the message. All three key-bearing properties are destructured
out of the retained config, so raw key material never comes to rest there.
Signed-off-by: lloyd tabb <lloyd@malloydata.org>
Collaborator
Author
|
@mtoy-googly-moogly do you think "serviceAccountKeyJsonBase64": {"env": "BIGQUERY_CREDENTIALS_JSON_B64"} is overkill? I saw it being used on the web so I thought to do it, but maybe the plain json is enough. |
…te the key
Review follow-ups on the property added in the previous commit.
One property, not two. The encoding is detected rather than declared: JSON
always begins with `{`, and `{` is not in the base64 alphabet, so the two
cannot be confused. A deployment that switches encodings no longer has to edit
config as well. Values are trimmed first, since a key that arrived through a
here-doc or `$(cat key.json)` carries a trailing newline that would otherwise
be read as content by both the sniff and the emptiness check.
Validate what was parsed. Checking only for "an object" let `{}` and any
unrelated JSON reach the SDK and come back as "the incoming JSON object does
not contain a client_email field" — the exact error this property exists to
eliminate. A key must now carry client_email and private_key, or be an
`external_account` config, which is the other shape the SDK's `credentials`
option accepts and which carries no key of its own. The check is a type
predicate, so the cast to CredentialBody is gone.
Correct the base64 rationale. It said raw JSON's newlines get mangled in
transit; a downloaded key file carries its newlines as `\n` escapes, so the
JSON is a single line and has none to mangle. The real reason is quoting —
braces and quotes survive a shell, a CI secret editor, and a `.env` file
poorly, and base64 is one token that survives all three.
Also: document the property in connection/CONTEXT.md, whose BigQuery list was
missing serviceAccountKey as well; and fix a test name that claimed to cover
an unset environment variable when an unset reference is dropped before the
connection sees it. It covers set-but-empty, now also set-but-whitespace.
Signed-off-by: lloyd tabb <lloyd@malloydata.org>
`packages/malloy-db-*/**/*.unit.spec.ts` are hermetic — they stub the SDK or test config parsing — but they sit in the same directories as the tests that need a live warehouse, so they ran only in that backend's credentialed CI job. A hermetic test of BigQuery config parsing sat out every PR that didn't touch BigQuery, which is exactly the PR that breaks it. Four specs across three connector packages were in that position. Add a `connector-unit` project that claims them and run it in `ci-core`. The backend projects exclude the same pattern, so each file still runs in exactly one project: a file matched by two would appear twice in `jest --listTests` and fail scripts/ci-test-sanity-check.sh, which diffs that list against the source tree. Signed-off-by: lloyd tabb <lloyd@malloydata.org>
lloydtabb
added a commit
to malloydata/malloydata.github.io
that referenced
this pull request
Aug 14, 2026
Follows the change in malloydata/malloy#3039 to a single `serviceAccountKeyJson` that takes JSON or base64 and detects which. Drops the separate base64 property, and states the true reason base64 is offered: quoting, not newlines — a downloaded key file carries its newlines as `\n` escapes, so the JSON is one line with none to mangle. Signed-off-by: lloyd tabb <lloyd@malloydata.org>
lloydtabb
added a commit
to malloydata/malloydata.github.io
that referenced
this pull request
Aug 15, 2026
…onment (#339) * docs(config): document supplying a BigQuery service account key from the environment `serviceAccountKey` is a `json` property, and json properties take their value literally — an `{"env": "..."}` inside one is never resolved. A server holding its key in an environment variable therefore cannot use that property, and the failure arrives as an error from BigQuery about a missing client_email field rather than anything naming the config. Document `serviceAccountKeyJson` and `serviceAccountKeyJsonBase64` (malloydata/malloy#3039), which are `secret` strings and do take references, with the shell commands that produce each form. Also state the json-property exception in the Environment Variables section, which until now claimed without qualification that any property value can be replaced with a reference. Signed-off-by: lloyd tabb <lloyd@malloydata.org> * docs(config): one BigQuery key property, with the encoding detected Follows the change in malloydata/malloy#3039 to a single `serviceAccountKeyJson` that takes JSON or base64 and detects which. Drops the separate base64 property, and states the true reason base64 is offered: quoting, not newlines — a downloaded key file carries its newlines as `\n` escapes, so the JSON is one line with none to mangle. Signed-off-by: lloyd tabb <lloyd@malloydata.org> * docs(config): drop the encoding-detection and json-exception paragraphs Review feedback on #339. The detection rule (JSON starts with `{`, base64 can't) is implementation detail nobody reading this page needs — both encodings work, which the examples already show. The json-property exception in the Environment Variables section is dropped too; the note in the BigQuery section covers the case a reader actually hits, and postgres and trino already carry their own. Signed-off-by: lloyd tabb <lloyd@malloydata.org> --------- Signed-off-by: lloyd tabb <lloyd@malloydata.org>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
BigQuery has two credential slots today, and neither one works for a server that holds its service account key in an environment variable:
serviceAccountKeyPath—file-typed, so{env: …}resolves, but it needs a file.serviceAccountKey— takes the key itself, but it isjson-typed, and a json slot holds its value literally.config_compile.tsrefuses reference indirection into structured config on purpose ("the security invariant that prevents reference injection into structured config"), so{env: …}is never resolved there.The result is that this config:
hands the SDK the literal object
{env: "BIGQUERY_CREDENTIALS_JSON"}as its credentials, which fails as:The environment variable is never read, so the error is identical whether it holds the key, a path, or nothing — and it names neither the property nor the reason.
client_email/private_keyare honored by the constructor but are not declared properties, so the compiler drops them with an "unknown property" warning before the connection sees them.The change
One new
secret-typed (string) property, where references do resolve:It takes the entire key file as either raw JSON or base64, detected rather than declared — JSON always starts with
{, and{is not in the base64 alphabet, so the two can be told apart with certainty. Base64 is worth accepting because it is a single unquoted token: a JSON object full of braces and quotes survives a shell, a CI secret editor, and a.envfile poorly. (An earlier revision of this PR had two properties and justified base64 by newline mangling. That was wrong — a downloaded key file carries the private key's newlines as\nescapes, so the JSON is one line with no newlines to mangle. One property, correct reason.)Details worth a second look in review:
client_email+private_key, or be anexternal_accountconfig — the other shape the SDK'scredentialsoption accepts (GoogleAuthOptions.credentials: CredentialBody | ExternalAccountClientOptions), which carries no key of its own. Without this,{}and any unrelated JSON reach the SDK and come back as the veryclient_emailerror this property exists to eliminate.serviceAccountKey→serviceAccountKeyJson→client_email/private_key. Existing behavior is unchanged when the new property is unset.{env: X}withXset-but-empty resolves to""and must not read as "the key is here", and a key that arrived via a here-doc or$(cat key.json)carries a trailing newline that would otherwise break the encoding sniff.JSON.parse's ownSyntaxErrorembeds the input it choked on, which here is a private key. One message covers both encodings, becauseBuffer's base64 decoder drops unrecognized characters rather than failing — by the time the parse fails, there is no way to know which encoding was meant.this.config; every key-bearing property is destructured out.Tests
packages/malloy-db-bigquery/src/bigquery_credentials.unit.spec.ts, 20 hermetic tests (no warehouse): both encodings through one property, whitespace tolerance, newline restoration from\nescapes, external-account acceptance, key material staying out of the retained config, empty/whitespace fallthrough, the precedence ladder, and each error path — including that the malformed message contains neither the key nor the text it came from, and that{}and a half-a-pair key are rejected here rather than at the first query.Three drive the real overlay stack through
MalloyConfigwith a set environment variable — the end-to-end claim this PR makes. One pins the old behavior for contrast:serviceAccountKey: {env: …}still lands the reference object in the credentials, so if malloy ever resolves references into json slots, that test fails and says so.Two more read the registration back: that the property is
secret(retyping it tojsonwould silently restore the whole gap — verified by mutation that exactly those tests fail when it does), and that no second base64 property exists.The live
db-bigquerysuite (149 tests) also passes against a real warehouse with this change.Two follow-ups included
connection/CONTEXT.mdnow documents the property and the precedence order. Its BigQuery list was also missingserviceAccountKeyentirely; that is fixed. Per the cross-repo obligation in that file, the companion docs PR is docs(config): supplying a BigQuery service account key from the environment malloydata.github.io#339.connector-unitjest project (separate commit, droppable on its own).packages/malloy-db-*/**/*.unit.spec.tsare hermetic but live beside tests that need a warehouse, so they ran only in that backend's credentialed CI job — four specs across three connector packages, including this one. They now run inci-coreon every PR. The backend projects exclude the same pattern so each file runs in exactly one project; a file matched by two would appear twice injest --listTestsand failscripts/ci-test-sanity-check.sh. Verified locally: sanity check passes, no duplicates, all 54 connector unit tests run with no credentials present.