Skip to content

Commit c3a6f15

Browse files
author
Jon Wayne Parrott
authored
Consolidate service account file loading logic. (#31)
1 parent 44f91d5 commit c3a6f15

File tree

6 files changed

+199
-65
lines changed

6 files changed

+199
-65
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Copyright 2016 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Helper functions for loading data from a Google service account file."""
16+
17+
import io
18+
import json
19+
20+
import six
21+
22+
from google.auth import crypt
23+
24+
25+
def from_dict(data, require=None):
26+
"""Validates a dictionary containing Google service account data.
27+
28+
Creates and returns a :class:`google.auth.crypt.Signer` instance from the
29+
private key specified in the data.
30+
31+
Args:
32+
data (Mapping[str, str]): The service account data
33+
require (Sequence[str]): List of keys required to be present in the
34+
info.
35+
36+
Returns:
37+
google.auth.crypt.Signer: A signer created from the private key in the
38+
service account file.
39+
40+
Raises:
41+
ValueError: if the data was in the wrong format, or if one of the
42+
required keys is missing.
43+
"""
44+
# Private key is always required.
45+
keys_needed = set(('private_key',))
46+
if require is not None:
47+
keys_needed.update(require)
48+
49+
missing = keys_needed.difference(six.iterkeys(data))
50+
51+
if missing:
52+
raise ValueError(
53+
'Service account info was not in the expected format, missing '
54+
'fields {}.'.format(', '.join(missing)))
55+
56+
# Create a signer.
57+
signer = crypt.Signer.from_string(
58+
data['private_key'], data.get('private_key_id'))
59+
60+
return signer
61+
62+
63+
def from_filename(filename, require=None):
64+
"""Reads a Google service account JSON file and returns its parsed info.
65+
66+
Args:
67+
filename (str): The path to the service account .json file.
68+
require (Sequence[str]): List of keys required to be present in the
69+
info.
70+
71+
Returns:
72+
Tuple[ Mapping[str, str], google.auth.crypt.Signer ]: The verified
73+
info and a signer instance.
74+
"""
75+
with io.open(filename, 'r', encoding='utf-8') as json_file:
76+
data = json.load(json_file)
77+
return data, from_dict(data, require=require)

packages/google-auth/google/auth/jwt.py

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,12 @@
4343
import base64
4444
import collections
4545
import datetime
46-
import io
4746
import json
4847

4948
from six.moves import urllib
5049

5150
from google.auth import _helpers
51+
from google.auth import _service_account_info
5252
from google.auth import credentials
5353
from google.auth import crypt
5454

@@ -318,12 +318,13 @@ def __init__(self, signer, issuer=None, subject=None, audience=None,
318318
self._additional_claims = {}
319319

320320
@classmethod
321-
def from_service_account_info(cls, info, **kwargs):
322-
"""Creates a Credentials instance from parsed service account info.
321+
def _from_signer_and_info(cls, signer, info, **kwargs):
322+
"""Creates a Credentials instance from a signer and service account
323+
info.
323324
324325
Args:
325-
info (Mapping[str, str]): The service account info in Google
326-
format.
326+
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
327+
info (Mapping[str, str]): The service account info.
327328
kwargs: Additional arguments to pass to the constructor.
328329
329330
Returns:
@@ -332,34 +333,44 @@ def from_service_account_info(cls, info, **kwargs):
332333
Raises:
333334
ValueError: If the info is not in the expected format.
334335
"""
336+
kwargs.setdefault('subject', info['client_email'])
337+
return cls(signer, issuer=info['client_email'], **kwargs)
335338

336-
try:
337-
email = info['client_email']
338-
key_id = info['private_key_id']
339-
private_key = info['private_key']
340-
except KeyError:
341-
raise ValueError(
342-
'Service account info was not in the expected format.')
339+
@classmethod
340+
def from_service_account_info(cls, info, **kwargs):
341+
"""Creates a Credentials instance from a dictionary containing service
342+
account info in Google format.
343343
344-
signer = crypt.Signer.from_string(private_key, key_id)
344+
Args:
345+
info (Mapping[str, str]): The service account info in Google
346+
format.
347+
kwargs: Additional arguments to pass to the constructor.
348+
349+
Returns:
350+
google.auth.jwt.Credentials: The constructed credentials.
345351
346-
kwargs.setdefault('subject', email)
347-
return cls(signer, issuer=email, **kwargs)
352+
Raises:
353+
ValueError: If the info is not in the expected format.
354+
"""
355+
signer = _service_account_info.from_dict(
356+
info, require=['client_email'])
357+
return cls._from_signer_and_info(signer, info, **kwargs)
348358

349359
@classmethod
350360
def from_service_account_file(cls, filename, **kwargs):
351-
"""Creates a Credentials instance from a service account json file.
361+
"""Creates a Credentials instance from a service account .json file
362+
in Google format.
352363
353364
Args:
354-
filename (str): The path to the service account json file.
365+
filename (str): The path to the service account .json file.
355366
kwargs: Additional arguments to pass to the constructor.
356367
357368
Returns:
358369
google.auth.jwt.Credentials: The constructed credentials.
359370
"""
360-
with io.open(filename, 'r', encoding='utf-8') as json_file:
361-
info = json.load(json_file)
362-
return cls.from_service_account_info(info, **kwargs)
371+
info, signer = _service_account_info.from_filename(
372+
filename, require=['client_email'])
373+
return cls._from_signer_and_info(signer, info, **kwargs)
363374

364375
def with_claims(self, issuer=None, subject=None, audience=None,
365376
additional_claims=None):

packages/google-auth/google/oauth2/service_account.py

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,10 @@
7171
"""
7272

7373
import datetime
74-
import io
75-
import json
7674

7775
from google.auth import _helpers
76+
from google.auth import _service_account_info
7877
from google.auth import credentials
79-
from google.auth import crypt
8078
from google.auth import jwt
8179
from google.oauth2 import _client
8280

@@ -149,6 +147,27 @@ def __init__(self, signer, service_account_email, token_uri, scopes=None,
149147
else:
150148
self._additional_claims = {}
151149

150+
@classmethod
151+
def _from_signer_and_info(cls, signer, info, **kwargs):
152+
"""Creates a Credentials instance from a signer and service account
153+
info.
154+
155+
Args:
156+
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
157+
info (Mapping[str, str]): The service account info.
158+
kwargs: Additional arguments to pass to the constructor.
159+
160+
Returns:
161+
google.auth.jwt.Credentials: The constructed credentials.
162+
163+
Raises:
164+
ValueError: If the info is not in the expected format.
165+
"""
166+
return cls(
167+
signer,
168+
service_account_email=info['client_email'],
169+
token_uri=info['token_uri'], **kwargs)
170+
152171
@classmethod
153172
def from_service_account_info(cls, info, **kwargs):
154173
"""Creates a Credentials instance from parsed service account info.
@@ -165,19 +184,9 @@ def from_service_account_info(cls, info, **kwargs):
165184
Raises:
166185
ValueError: If the info is not in the expected format.
167186
"""
168-
try:
169-
email = info['client_email']
170-
key_id = info['private_key_id']
171-
private_key = info['private_key']
172-
token_uri = info['token_uri']
173-
except KeyError:
174-
raise ValueError(
175-
'Service account info was not in the expected format.')
176-
177-
signer = crypt.Signer.from_string(private_key, key_id)
178-
179-
return cls(
180-
signer, service_account_email=email, token_uri=token_uri, **kwargs)
187+
signer = _service_account_info.from_dict(
188+
info, require=['client_email', 'token_uri'])
189+
return cls._from_signer_and_info(signer, info, **kwargs)
181190

182191
@classmethod
183192
def from_service_account_file(cls, filename, **kwargs):
@@ -191,9 +200,9 @@ def from_service_account_file(cls, filename, **kwargs):
191200
google.auth.service_account.Credentials: The constructed
192201
credentials.
193202
"""
194-
with io.open(filename, 'r', encoding='utf-8') as json_file:
195-
info = json.load(json_file)
196-
return cls.from_service_account_info(info, **kwargs)
203+
info, signer = _service_account_info.from_filename(
204+
filename, require=['client_email', 'token_uri'])
205+
return cls._from_signer_and_info(signer, info, **kwargs)
197206

198207
@property
199208
def requires_scopes(self):

packages/google-auth/tests/oauth2/test_service_account.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -85,19 +85,6 @@ def test_from_service_account_info_args(self):
8585
assert credentials._subject == subject
8686
assert credentials._additional_claims == additional_claims
8787

88-
def test_from_service_account_bad_key(self):
89-
info = SERVICE_ACCOUNT_INFO.copy()
90-
info['private_key'] = 'garbage'
91-
92-
with pytest.raises(ValueError) as excinfo:
93-
service_account.Credentials.from_service_account_info(info)
94-
95-
assert excinfo.match(r'No key could be detected')
96-
97-
def test_from_service_account_bad_format(self):
98-
with pytest.raises(ValueError):
99-
service_account.Credentials.from_service_account_info({})
100-
10188
def test_from_service_account_file(self):
10289
info = SERVICE_ACCOUNT_INFO.copy()
10390

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Copyright 2016 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import json
16+
import os
17+
18+
import pytest
19+
import six
20+
21+
from google.auth import _service_account_info
22+
from google.auth import crypt
23+
24+
25+
DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
26+
SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, 'service_account.json')
27+
28+
with open(SERVICE_ACCOUNT_JSON_FILE, 'r') as fh:
29+
SERVICE_ACCOUNT_INFO = json.load(fh)
30+
31+
32+
def test_from_dict():
33+
signer = _service_account_info.from_dict(SERVICE_ACCOUNT_INFO)
34+
assert isinstance(signer, crypt.Signer)
35+
assert signer.key_id == SERVICE_ACCOUNT_INFO['private_key_id']
36+
37+
38+
def test_from_dict_bad_private_key():
39+
info = SERVICE_ACCOUNT_INFO.copy()
40+
info['private_key'] = 'garbage'
41+
42+
with pytest.raises(ValueError) as excinfo:
43+
_service_account_info.from_dict(info)
44+
45+
assert excinfo.match(r'No key could be detected')
46+
47+
48+
def test_from_dict_bad_format():
49+
with pytest.raises(ValueError) as excinfo:
50+
_service_account_info.from_dict({})
51+
52+
assert excinfo.match(r'missing fields')
53+
54+
55+
def test_from_filename():
56+
info, signer = _service_account_info.from_filename(
57+
SERVICE_ACCOUNT_JSON_FILE)
58+
59+
for key, value in six.iteritems(SERVICE_ACCOUNT_INFO):
60+
assert info[key] == value
61+
62+
assert isinstance(signer, crypt.Signer)
63+
assert signer.key_id == SERVICE_ACCOUNT_INFO['private_key_id']

packages/google-auth/tests/test_jwt.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -231,19 +231,6 @@ def test_from_service_account_info_args(self):
231231
assert credentials._audience == self.AUDIENCE
232232
assert credentials._additional_claims == self.ADDITIONAL_CLAIMS
233233

234-
def test_from_service_account_bad_private_key(self):
235-
info = SERVICE_ACCOUNT_INFO.copy()
236-
info['private_key'] = 'garbage'
237-
238-
with pytest.raises(ValueError) as excinfo:
239-
jwt.Credentials.from_service_account_info(info)
240-
241-
assert excinfo.match(r'No key could be detected')
242-
243-
def test_from_service_account_bad_format(self):
244-
with pytest.raises(ValueError):
245-
jwt.Credentials.from_service_account_info({})
246-
247234
def test_from_service_account_file(self):
248235
info = SERVICE_ACCOUNT_INFO.copy()
249236

0 commit comments

Comments
 (0)