Skip to content

Commit 06890d6

Browse files
authored
Merge branch 'firebase:master' into master
2 parents c54a453 + ebf1bcd commit 06890d6

9 files changed

Lines changed: 182 additions & 117 deletions

File tree

firebase_admin/_auth_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,8 @@ def update_user(self, uid, **kwargs): # pylint: disable=differing-param-doc
336336
valid_since: An integer signifying the seconds since the epoch (optional). This field
337337
is set by ``revoke_refresh_tokens`` and it is discouraged to set this field
338338
directly.
339+
providers_to_delete: The list of provider IDs to unlink,
340+
eg: 'google.com', 'password', etc.
339341
340342
Returns:
341343
UserRecord: An updated UserRecord instance for the user.

firebase_admin/_auth_utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,15 @@ def validate_action_type(action_type):
266266
Valid values are {1}'.format(action_type, ', '.join(VALID_EMAIL_ACTION_TYPES)))
267267
return action_type
268268

269+
def validate_provider_ids(provider_ids, required=False):
270+
if not provider_ids:
271+
if required:
272+
raise ValueError('Invalid provider IDs. Provider ids should be provided')
273+
return []
274+
for provider_id in provider_ids:
275+
validate_provider_id(provider_id, True)
276+
return provider_ids
277+
269278
def build_update_mask(params):
270279
"""Creates an update mask list from the given dictionary."""
271280
mask = []

firebase_admin/_gapic_utils.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Copyright 2021 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+
"""Internal utilities for interacting with Google API client."""
16+
17+
import io
18+
import socket
19+
20+
import googleapiclient
21+
import httplib2
22+
import requests
23+
24+
from firebase_admin import exceptions
25+
from firebase_admin import _utils
26+
27+
28+
def handle_platform_error_from_googleapiclient(error, handle_func=None):
29+
"""Constructs a ``FirebaseError`` from the given googleapiclient error.
30+
31+
This can be used to handle errors returned by Google Cloud Platform (GCP) APIs.
32+
33+
Args:
34+
error: An error raised by the googleapiclient while making an HTTP call to a GCP API.
35+
handle_func: A function that can be used to handle platform errors in a custom way. When
36+
specified, this function will be called with three arguments. It has the same
37+
signature as ```_handle_func_googleapiclient``, but may return ``None``.
38+
39+
Returns:
40+
FirebaseError: A ``FirebaseError`` that can be raised to the user code.
41+
"""
42+
if not isinstance(error, googleapiclient.errors.HttpError):
43+
return handle_googleapiclient_error(error)
44+
45+
content = error.content.decode()
46+
status_code = error.resp.status
47+
error_dict, message = _utils._parse_platform_error(content, status_code) # pylint: disable=protected-access
48+
http_response = _http_response_from_googleapiclient_error(error)
49+
exc = None
50+
if handle_func:
51+
exc = handle_func(error, message, error_dict, http_response)
52+
53+
return exc if exc else _handle_func_googleapiclient(error, message, error_dict, http_response)
54+
55+
56+
def _handle_func_googleapiclient(error, message, error_dict, http_response):
57+
"""Constructs a ``FirebaseError`` from the given GCP error.
58+
59+
Args:
60+
error: An error raised by the googleapiclient module while making an HTTP call.
61+
message: A message to be included in the resulting ``FirebaseError``.
62+
error_dict: Parsed GCP error response.
63+
http_response: A requests HTTP response object to associate with the exception.
64+
65+
Returns:
66+
FirebaseError: A ``FirebaseError`` that can be raised to the user code or None.
67+
"""
68+
code = error_dict.get('status')
69+
return handle_googleapiclient_error(error, message, code, http_response)
70+
71+
72+
def handle_googleapiclient_error(error, message=None, code=None, http_response=None):
73+
"""Constructs a ``FirebaseError`` from the given googleapiclient error.
74+
75+
This method is agnostic of the remote service that produced the error, whether it is a GCP
76+
service or otherwise. Therefore, this method does not attempt to parse the error response in
77+
any way.
78+
79+
Args:
80+
error: An error raised by the googleapiclient module while making an HTTP call.
81+
message: A message to be included in the resulting ``FirebaseError`` (optional). If not
82+
specified the string representation of the ``error`` argument is used as the message.
83+
code: A GCP error code that will be used to determine the resulting error type (optional).
84+
If not specified the HTTP status code on the error response is used to determine a
85+
suitable error code.
86+
http_response: A requests HTTP response object to associate with the exception (optional).
87+
If not specified, one will be created from the ``error``.
88+
89+
Returns:
90+
FirebaseError: A ``FirebaseError`` that can be raised to the user code.
91+
"""
92+
if isinstance(error, socket.timeout) or (
93+
isinstance(error, socket.error) and 'timed out' in str(error)):
94+
return exceptions.DeadlineExceededError(
95+
message='Timed out while making an API call: {0}'.format(error),
96+
cause=error)
97+
if isinstance(error, httplib2.ServerNotFoundError):
98+
return exceptions.UnavailableError(
99+
message='Failed to establish a connection: {0}'.format(error),
100+
cause=error)
101+
if not isinstance(error, googleapiclient.errors.HttpError):
102+
return exceptions.UnknownError(
103+
message='Unknown error while making a remote service call: {0}'.format(error),
104+
cause=error)
105+
106+
if not code:
107+
code = _utils._http_status_to_error_code(error.resp.status) # pylint: disable=protected-access
108+
if not message:
109+
message = str(error)
110+
if not http_response:
111+
http_response = _http_response_from_googleapiclient_error(error)
112+
113+
err_type = _utils._error_code_to_exception_type(code) # pylint: disable=protected-access
114+
return err_type(message=message, cause=error, http_response=http_response)
115+
116+
117+
def _http_response_from_googleapiclient_error(error):
118+
"""Creates a requests HTTP Response object from the given googleapiclient error."""
119+
resp = requests.models.Response()
120+
resp.raw = io.BytesIO(error.content)
121+
resp.status_code = error.resp.status
122+
return resp

firebase_admin/_user_mgt.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,7 @@ def create_user(self, uid=None, display_name=None, email=None, phone_number=None
688688

689689
def update_user(self, uid, display_name=None, email=None, phone_number=None,
690690
photo_url=None, password=None, disabled=None, email_verified=None,
691-
valid_since=None, custom_claims=None):
691+
valid_since=None, custom_claims=None, providers_to_delete=None):
692692
"""Updates an existing user account with the specified properties"""
693693
payload = {
694694
'localId': _auth_utils.validate_uid(uid, required=True),
@@ -700,6 +700,7 @@ def update_user(self, uid, display_name=None, email=None, phone_number=None,
700700
}
701701

702702
remove = []
703+
remove_provider = _auth_utils.validate_provider_ids(providers_to_delete)
703704
if display_name is not None:
704705
if display_name is DELETE_ATTRIBUTE:
705706
remove.append('DISPLAY_NAME')
@@ -715,7 +716,7 @@ def update_user(self, uid, display_name=None, email=None, phone_number=None,
715716

716717
if phone_number is not None:
717718
if phone_number is DELETE_ATTRIBUTE:
718-
payload['deleteProvider'] = ['phone']
719+
remove_provider.append('phone')
719720
else:
720721
payload['phoneNumber'] = _auth_utils.validate_phone(phone_number)
721722

@@ -726,6 +727,9 @@ def update_user(self, uid, display_name=None, email=None, phone_number=None,
726727
custom_claims, dict) else custom_claims
727728
payload['customAttributes'] = _auth_utils.validate_custom_claims(json_claims)
728729

730+
if remove_provider:
731+
payload['deleteProvider'] = list(set(remove_provider))
732+
729733
payload = {k: v for k, v in payload.items() if v is not None}
730734
body, http_resp = self._make_request('post', '/accounts:update', json=payload)
731735
if not body or not body.get('localId'):

firebase_admin/_utils.py

Lines changed: 0 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,9 @@
1414

1515
"""Internal utilities common to all modules."""
1616

17-
import io
1817
import json
19-
import socket
2018

2119
import google.auth
22-
import googleapiclient
23-
import httplib2
2420
import requests
2521

2622
import firebase_admin
@@ -206,103 +202,6 @@ def handle_requests_error(error, message=None, code=None):
206202
return err_type(message=message, cause=error, http_response=error.response)
207203

208204

209-
def handle_platform_error_from_googleapiclient(error, handle_func=None):
210-
"""Constructs a ``FirebaseError`` from the given googleapiclient error.
211-
212-
This can be used to handle errors returned by Google Cloud Platform (GCP) APIs.
213-
214-
Args:
215-
error: An error raised by the googleapiclient while making an HTTP call to a GCP API.
216-
handle_func: A function that can be used to handle platform errors in a custom way. When
217-
specified, this function will be called with three arguments. It has the same
218-
signature as ```_handle_func_googleapiclient``, but may return ``None``.
219-
220-
Returns:
221-
FirebaseError: A ``FirebaseError`` that can be raised to the user code.
222-
"""
223-
if not isinstance(error, googleapiclient.errors.HttpError):
224-
return handle_googleapiclient_error(error)
225-
226-
content = error.content.decode()
227-
status_code = error.resp.status
228-
error_dict, message = _parse_platform_error(content, status_code)
229-
http_response = _http_response_from_googleapiclient_error(error)
230-
exc = None
231-
if handle_func:
232-
exc = handle_func(error, message, error_dict, http_response)
233-
234-
return exc if exc else _handle_func_googleapiclient(error, message, error_dict, http_response)
235-
236-
237-
def _handle_func_googleapiclient(error, message, error_dict, http_response):
238-
"""Constructs a ``FirebaseError`` from the given GCP error.
239-
240-
Args:
241-
error: An error raised by the googleapiclient module while making an HTTP call.
242-
message: A message to be included in the resulting ``FirebaseError``.
243-
error_dict: Parsed GCP error response.
244-
http_response: A requests HTTP response object to associate with the exception.
245-
246-
Returns:
247-
FirebaseError: A ``FirebaseError`` that can be raised to the user code or None.
248-
"""
249-
code = error_dict.get('status')
250-
return handle_googleapiclient_error(error, message, code, http_response)
251-
252-
253-
def handle_googleapiclient_error(error, message=None, code=None, http_response=None):
254-
"""Constructs a ``FirebaseError`` from the given googleapiclient error.
255-
256-
This method is agnostic of the remote service that produced the error, whether it is a GCP
257-
service or otherwise. Therefore, this method does not attempt to parse the error response in
258-
any way.
259-
260-
Args:
261-
error: An error raised by the googleapiclient module while making an HTTP call.
262-
message: A message to be included in the resulting ``FirebaseError`` (optional). If not
263-
specified the string representation of the ``error`` argument is used as the message.
264-
code: A GCP error code that will be used to determine the resulting error type (optional).
265-
If not specified the HTTP status code on the error response is used to determine a
266-
suitable error code.
267-
http_response: A requests HTTP response object to associate with the exception (optional).
268-
If not specified, one will be created from the ``error``.
269-
270-
Returns:
271-
FirebaseError: A ``FirebaseError`` that can be raised to the user code.
272-
"""
273-
if isinstance(error, socket.timeout) or (
274-
isinstance(error, socket.error) and 'timed out' in str(error)):
275-
return exceptions.DeadlineExceededError(
276-
message='Timed out while making an API call: {0}'.format(error),
277-
cause=error)
278-
if isinstance(error, httplib2.ServerNotFoundError):
279-
return exceptions.UnavailableError(
280-
message='Failed to establish a connection: {0}'.format(error),
281-
cause=error)
282-
if not isinstance(error, googleapiclient.errors.HttpError):
283-
return exceptions.UnknownError(
284-
message='Unknown error while making a remote service call: {0}'.format(error),
285-
cause=error)
286-
287-
if not code:
288-
code = _http_status_to_error_code(error.resp.status)
289-
if not message:
290-
message = str(error)
291-
if not http_response:
292-
http_response = _http_response_from_googleapiclient_error(error)
293-
294-
err_type = _error_code_to_exception_type(code)
295-
return err_type(message=message, cause=error, http_response=http_response)
296-
297-
298-
def _http_response_from_googleapiclient_error(error):
299-
"""Creates a requests HTTP Response object from the given googleapiclient error."""
300-
resp = requests.models.Response()
301-
resp.raw = io.BytesIO(error.content)
302-
resp.status_code = error.resp.status
303-
return resp
304-
305-
306205
def _http_status_to_error_code(status):
307206
"""Maps an HTTP status to a platform error code."""
308207
return _HTTP_STATUS_TO_ERROR_CODE.get(status, exceptions.UNKNOWN)

firebase_admin/messaging.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from firebase_admin import _http_client
2525
from firebase_admin import _messaging_encoder
2626
from firebase_admin import _messaging_utils
27+
from firebase_admin import _gapic_utils
2728
from firebase_admin import _utils
2829

2930

@@ -466,7 +467,7 @@ def _handle_iid_error(self, error):
466467

467468
def _handle_batch_error(self, error):
468469
"""Handles errors received from the googleapiclient while making batch requests."""
469-
return _utils.handle_platform_error_from_googleapiclient(
470+
return _gapic_utils.handle_platform_error_from_googleapiclient(
470471
error, _MessagingService._build_fcm_error_googleapiclient)
471472

472473
@classmethod

integration/test_auth.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,14 @@ def test_disable_user(new_user_with_params):
496496
assert user.disabled is True
497497
assert len(user.provider_data) == 1
498498

499+
def test_remove_provider(new_user_with_provider):
500+
provider_ids = [provider.provider_id for provider in new_user_with_provider.provider_data]
501+
assert 'google.com' in provider_ids
502+
user = auth.update_user(new_user_with_provider.uid, providers_to_delete=['google.com'])
503+
assert user.uid == new_user_with_provider.uid
504+
new_provider_ids = [provider.provider_id for provider in user.provider_data]
505+
assert 'google.com' not in new_provider_ids
506+
499507
def test_delete_user():
500508
user = auth.create_user()
501509
auth.delete_user(user.uid)

0 commit comments

Comments
 (0)