Skip to content
This repository was archived by the owner on Sep 2, 2025. It is now read-only.

Commit 526eefa

Browse files
authored
Allow base64-service-account-json key auth Issue: #923 (#1245)
* added base64 functionality and basic testing * Change log * fix conftest to allow json * Change method name from camel to snake case * change type hinting to be py3.9 compatible --------- Co-authored-by: Robele Baker <>
1 parent f454c47 commit 526eefa

File tree

5 files changed

+135
-2
lines changed

5 files changed

+135
-2
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Features
2+
body: Add support for base 64 encoded json keyfile credentials
3+
time: 2024-05-16T12:57:35.383416-07:00
4+
custom:
5+
Author: robeleb1
6+
Issue: "923"

dbt/adapters/bigquery/connections.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@
4040
from dbt.adapters.events.types import SQLQuery
4141
from dbt_common.events.functions import fire_event
4242
from dbt.adapters.bigquery import __version__ as dbt_version
43+
from dbt.adapters.bigquery.utility import is_base64, base64_to_string
4344

4445
from dbt_common.dataclass_schema import ExtensibleDbtClassMixin, StrEnum
4546

47+
4648
logger = AdapterLogger("BigQuery")
4749

4850
BQ_QUERY_JOB_SPLIT = "-----Query Job SQL Follows-----"
@@ -125,7 +127,7 @@ class BigQueryCredentials(Credentials):
125127
job_creation_timeout_seconds: Optional[int] = None
126128
job_execution_timeout_seconds: Optional[int] = None
127129

128-
# Keyfile json creds
130+
# Keyfile json creds (unicode or base 64 encoded)
129131
keyfile: Optional[str] = None
130132
keyfile_json: Optional[Dict[str, Any]] = None
131133

@@ -332,6 +334,8 @@ def get_google_credentials(cls, profile_credentials) -> GoogleCredentials:
332334

333335
elif method == BigQueryConnectionMethod.SERVICE_ACCOUNT_JSON:
334336
details = profile_credentials.keyfile_json
337+
if is_base64(profile_credentials.keyfile_json):
338+
details = base64_to_string(details)
335339
return creds.from_service_account_info(details, scopes=profile_credentials.scopes)
336340

337341
elif method == BigQueryConnectionMethod.OAUTH_SECRETS:

dbt/adapters/bigquery/utility.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import base64
2+
import binascii
13
import json
2-
from typing import Any, Optional
4+
from typing import Any, Optional, Union
35

46
import dbt_common.exceptions
57

@@ -43,3 +45,39 @@ def sql_escape(string):
4345
if not isinstance(string, str):
4446
raise dbt_common.exceptions.CompilationError(f"cannot escape a non-string: {string}")
4547
return json.dumps(string)[1:-1]
48+
49+
50+
def is_base64(s: Union[str, bytes]) -> bool:
51+
"""
52+
Checks if the given string or bytes object is valid Base64 encoded.
53+
54+
Args:
55+
s: The string or bytes object to check.
56+
57+
Returns:
58+
True if the input is valid Base64, False otherwise.
59+
"""
60+
61+
if isinstance(s, str):
62+
# For strings, ensure they consist only of valid Base64 characters
63+
if not s.isascii():
64+
return False
65+
# Convert to bytes for decoding
66+
s = s.encode("ascii")
67+
68+
try:
69+
# Use the 'validate' parameter to enforce strict Base64 decoding rules
70+
base64.b64decode(s, validate=True)
71+
return True
72+
except TypeError:
73+
return False
74+
except binascii.Error: # Catch specific errors from the base64 module
75+
return False
76+
77+
78+
def base64_to_string(b):
79+
return base64.b64decode(b).decode("utf-8")
80+
81+
82+
def string_to_base64(s):
83+
return base64.b64encode(s.encode("utf-8"))

tests/conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22
import os
33
import json
4+
from dbt.adapters.bigquery.utility import is_base64, base64_to_string
45

56
# Import the fuctional fixtures as a plugin
67
# Note: fixtures with session scope need to be local
@@ -38,6 +39,8 @@ def oauth_target():
3839

3940
def service_account_target():
4041
credentials_json_str = os.getenv("BIGQUERY_TEST_SERVICE_ACCOUNT_JSON").replace("'", "")
42+
if is_base64(credentials_json_str):
43+
credentials_json_str = base64_to_string(credentials_json_str)
4144
credentials = json.loads(credentials_json_str)
4245
project_id = credentials.get("project_id")
4346
return {
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import json
2+
import pytest
3+
from dbt.adapters.bigquery.utility import string_to_base64, is_base64
4+
5+
6+
@pytest.fixture
7+
def example_json_keyfile():
8+
keyfile = json.dumps(
9+
{
10+
"type": "service_account",
11+
"project_id": "",
12+
"private_key_id": "",
13+
"private_key": "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n",
14+
"client_email": "",
15+
"client_id": "",
16+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
17+
"token_uri": "https://oauth2.googleapis.com/token",
18+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
19+
"client_x509_cert_url": "",
20+
}
21+
)
22+
23+
return keyfile
24+
25+
26+
@pytest.fixture
27+
def example_json_keyfile_b64():
28+
keyfile = json.dumps(
29+
{
30+
"type": "service_account",
31+
"project_id": "",
32+
"private_key_id": "",
33+
"private_key": "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n",
34+
"client_email": "",
35+
"client_id": "",
36+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
37+
"token_uri": "https://oauth2.googleapis.com/token",
38+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
39+
"client_x509_cert_url": "",
40+
}
41+
)
42+
43+
return string_to_base64(keyfile)
44+
45+
46+
def test_valid_base64_strings(example_json_keyfile_b64):
47+
valid_strings = [
48+
"SGVsbG8gV29ybGQh", # "Hello World!"
49+
"Zm9vYmFy", # "foobar"
50+
"QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU2Nzg5", # A long string
51+
"", # Empty string
52+
example_json_keyfile_b64.decode("utf-8"),
53+
]
54+
55+
for s in valid_strings:
56+
assert is_base64(s) is True
57+
58+
59+
def test_valid_base64_bytes(example_json_keyfile_b64):
60+
valid_bytes = [
61+
b"SGVsbG8gV29ybGQh", # "Hello World!"
62+
b"Zm9vYmFy", # "foobar"
63+
b"QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU2Nzg5", # A long string
64+
b"", # Empty bytes
65+
example_json_keyfile_b64,
66+
]
67+
for s in valid_bytes:
68+
assert is_base64(s) is True
69+
70+
71+
def test_invalid_base64(example_json_keyfile):
72+
invalid_inputs = [
73+
"This is not Base64",
74+
"SGVsbG8gV29ybGQ", # Incorrect padding
75+
"Invalid#Base64",
76+
12345, # Not a string or bytes
77+
b"Invalid#Base64",
78+
"H\xffGVsbG8gV29ybGQh", # Contains invalid character \xff
79+
example_json_keyfile,
80+
]
81+
for s in invalid_inputs:
82+
assert is_base64(s) is False

0 commit comments

Comments
 (0)