Skip to content

Commit cb97c3e

Browse files
author
Lihan Li
committed
feat: Add service account impersonation
1 parent 45fcad4 commit cb97c3e

File tree

5 files changed

+32
-2
lines changed

5 files changed

+32
-2
lines changed

README.rst

+11-2
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,8 @@ Here are examples of all the supported arguments. Any not present are either for
236236
'priority=INTERACTIVE' '&'
237237
'schema_update_options=ALLOW_FIELD_ADDITION,ALLOW_FIELD_RELAXATION' '&'
238238
'use_query_cache=true' '&'
239-
'write_disposition=WRITE_APPEND'
239+
'write_disposition=WRITE_APPEND' '&'
240+
'with_subject={email}'
240241
)
241242
242243
In cases where you wish to include the full credentials in the connection URI you can base64 the credentials JSON file and supply the encoded string to the ``credentials_base64`` parameter.
@@ -259,13 +260,21 @@ In cases where you wish to include the full credentials in the connection URI yo
259260
'priority=INTERACTIVE' '&'
260261
'schema_update_options=ALLOW_FIELD_ADDITION,ALLOW_FIELD_RELAXATION' '&'
261262
'use_query_cache=true' '&'
262-
'write_disposition=WRITE_APPEND'
263+
'write_disposition=WRITE_APPEND' '&'
264+
'with_subject={email}'
263265
)
264266
265267
To create the base64 encoded string you can use the command line tool ``base64``, or ``openssl base64``, or ``python -m base64``.
266268

267269
Alternatively, you can use an online generator like `www.base64encode.org <https://www.base64encode.org>_` to paste your credentials JSON file to be encoded.
268270

271+
with_subject impersonation
272+
^^^^^^^^^^^^^^^^^^^^^^^^^^
273+
274+
If the service account has `domain-wide delegation authority`_, you may pass in `with_subject={email}` to impersonate the user.
275+
276+
.. _domain-wide delegation authority: https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority
277+
269278

270279
Supplying Your Own BigQuery Client
271280
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

sqlalchemy_bigquery/_helpers.py

+4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def create_bigquery_client(
3636
default_query_job_config=None,
3737
location=None,
3838
project_id=None,
39+
with_subject=None,
3940
):
4041
default_project = None
4142

@@ -57,6 +58,9 @@ def create_bigquery_client(
5758
else:
5859
credentials, default_project = google.auth.default(scopes=SCOPES)
5960

61+
if with_subject:
62+
credentials = credentials.with_subject(with_subject)
63+
6064
if project_id is None:
6165
project_id = default_project
6266

sqlalchemy_bigquery/base.py

+2
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,7 @@ def create_connect_args(self, url):
821821
credentials_base64,
822822
default_query_job_config,
823823
list_tables_page_size,
824+
with_subject,
824825
user_supplied_client,
825826
) = parse_url(url)
826827

@@ -846,6 +847,7 @@ def create_connect_args(self, url):
846847
project_id=project_id,
847848
location=self.location,
848849
default_query_job_config=default_query_job_config,
850+
with_subject=with_subject,
849851
)
850852
return ([], {"client": client})
851853

sqlalchemy_bigquery/parse_url.py

+8
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def parse_url(url): # noqa: C901
7171
credentials_base64 = None
7272
list_tables_page_size = None
7373
user_supplied_client = False
74+
with_subject = None
7475

7576
# location
7677
if "location" in query:
@@ -106,6 +107,10 @@ def parse_url(url): # noqa: C901
106107
if "user_supplied_client" in query:
107108
user_supplied_client = query.pop("user_supplied_client").lower() == "true"
108109

110+
# Impersonation support (delegation)
111+
if "with_subject" in query:
112+
with_subject = query.pop('with_subject')
113+
109114
# if only these "non-config" values were present, the dict will now be empty
110115
if not query:
111116
# if a dataset_id exists, we need to return a job_config that isn't None
@@ -120,6 +125,7 @@ def parse_url(url): # noqa: C901
120125
credentials_base64,
121126
QueryJobConfig(),
122127
list_tables_page_size,
128+
with_subject,
123129
user_supplied_client,
124130
)
125131
else:
@@ -132,6 +138,7 @@ def parse_url(url): # noqa: C901
132138
credentials_base64,
133139
None,
134140
list_tables_page_size,
141+
with_subject,
135142
user_supplied_client,
136143
)
137144

@@ -282,5 +289,6 @@ def parse_url(url): # noqa: C901
282289
credentials_base64,
283290
job_config,
284291
list_tables_page_size,
292+
with_subject,
285293
user_supplied_client,
286294
)

tests/unit/test_parse_url.py

+7
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def url_with_everything():
6464
"&use_query_cache=true"
6565
"&write_disposition=WRITE_APPEND"
6666
"&user_supplied_client=true"
67+
6768
)
6869

6970

@@ -78,6 +79,7 @@ def test_basic(url_with_everything):
7879
job_config,
7980
list_tables_page_size,
8081
user_supplied_client,
82+
with_subject,
8183
) = parse_url(url_with_everything)
8284

8385
assert project_id == "some-project"
@@ -89,6 +91,7 @@ def test_basic(url_with_everything):
8991
assert credentials_base64 == "eyJrZXkiOiJ2YWx1ZSJ9Cg=="
9092
assert isinstance(job_config, QueryJobConfig)
9193
assert user_supplied_client
94+
assert with_subject
9295

9396

9497
@pytest.mark.parametrize(
@@ -191,6 +194,7 @@ def test_empty_with_non_config():
191194
job_config,
192195
list_tables_page_size,
193196
user_supplied_credentials,
197+
with_subject,
194198
) = url
195199

196200
assert project_id is None
@@ -202,6 +206,7 @@ def test_empty_with_non_config():
202206
assert job_config is None
203207
assert list_tables_page_size is None
204208
assert not user_supplied_credentials
209+
assert not with_subject
205210

206211

207212
def test_only_dataset():
@@ -215,6 +220,7 @@ def test_only_dataset():
215220
credentials_base64,
216221
job_config,
217222
list_tables_page_size,
223+
with_subject,
218224
user_supplied_credentials,
219225
) = url
220226

@@ -227,6 +233,7 @@ def test_only_dataset():
227233
assert list_tables_page_size is None
228234
assert isinstance(job_config, QueryJobConfig)
229235
assert not user_supplied_credentials
236+
assert not user_supplied_credentials
230237
# we can't actually test that the dataset is on the job_config,
231238
# since we take care of that afterwards, when we have a client to fill in the project
232239

0 commit comments

Comments
 (0)