Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Unreleased

-
- [added] Added a new `auth.DELETE_ATTRIBUTE` sentinel value, which can be
used to delete `phone_number`, `display_name`, `photo_url` and `custom_claims`
attributes from a user account. It is now recommended to use this sentinel
value over passing `None` for deleting attributes.

# v2.16.0

Expand Down
21 changes: 14 additions & 7 deletions firebase_admin/_user_mgt.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,18 @@
MAX_LIST_USERS_RESULTS = 1000
MAX_IMPORT_USERS_SIZE = 1000

class _Unspecified(object):
pass

class Sentinel(object):

def __init__(self, description):
self.description = description


# Use this internally, until sentinels are available in the public API.
_UNSPECIFIED = _Unspecified()
_UNSPECIFIED = Sentinel('No value specified')


DELETE_ATTRIBUTE = Sentinel('Value used to delete an attribute from a user profile')


class ApiCallError(Exception):
Expand Down Expand Up @@ -546,26 +553,26 @@ def update_user(self, uid, display_name=_UNSPECIFIED, email=None, phone_number=_

remove = []
if display_name is not _UNSPECIFIED:
if display_name is None:
if display_name is None or display_name is DELETE_ATTRIBUTE:
remove.append('DISPLAY_NAME')
else:
payload['displayName'] = _auth_utils.validate_display_name(display_name)
if photo_url is not _UNSPECIFIED:
if photo_url is None:
if photo_url is None or photo_url is DELETE_ATTRIBUTE:
remove.append('PHOTO_URL')
else:
payload['photoUrl'] = _auth_utils.validate_photo_url(photo_url)
if remove:
payload['deleteAttribute'] = remove

if phone_number is not _UNSPECIFIED:
if phone_number is None:
if phone_number is None or phone_number is DELETE_ATTRIBUTE:
payload['deleteProvider'] = ['phone']
else:
payload['phoneNumber'] = _auth_utils.validate_phone(phone_number)

if custom_claims is not _UNSPECIFIED:
if custom_claims is None:
if custom_claims is None or custom_claims is DELETE_ATTRIBUTE:
custom_claims = {}
json_claims = json.dumps(custom_claims) if isinstance(
custom_claims, dict) else custom_claims
Expand Down
11 changes: 7 additions & 4 deletions firebase_admin/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
__all__ = [
'ActionCodeSettings',
'AuthError',
'DELETE_ATTRIBUTE',
'ErrorInfo',
'ExportedUserRecord',
'ImportUserRecord',
Expand Down Expand Up @@ -68,6 +69,7 @@
]

ActionCodeSettings = _user_mgt.ActionCodeSettings
DELETE_ATTRIBUTE = _user_mgt.DELETE_ATTRIBUTE
ErrorInfo = _user_import.ErrorInfo
ExportedUserRecord = _user_mgt.ExportedUserRecord
ListUsersPage = _user_mgt.ListUsersPage
Expand Down Expand Up @@ -352,17 +354,18 @@ def update_user(uid, **kwargs):

Keyword Args:
display_name: The user's display name (optional). Can be removed by explicitly passing
None.
``auth.DELETE_ATTRIBUTE``.
email: The user's primary email (optional).
email_verified: A boolean indicating whether or not the user's primary email is
verified (optional).
phone_number: The user's primary phone number (optional). Can be removed by explicitly
passing None.
photo_url: The user's photo URL (optional). Can be removed by explicitly passing None.
passing ``auth.DELETE_ATTRIBUTE``.
photo_url: The user's photo URL (optional). Can be removed by explicitly passing
``auth.DELETE_ATTRIBUTE``.
password: The user's raw, unhashed password. (optional).
disabled: A boolean indicating whether or not the user account is disabled (optional).
custom_claims: A dictionary or a JSON string contining the custom claims to be set on the
user account (optional).
user account (optional). To remove all custom claims, pass ``auth.DELETE_ATTRIBUTE``.
valid_since: An integer signifying the seconds since the epoch. This field is set by
``revoke_refresh_tokens`` and it is discouraged to set this field directly.

Expand Down
22 changes: 21 additions & 1 deletion tests/test_user_mgt.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,13 @@ def test_update_user_custom_claims(self, user_mgt_app):
request = json.loads(recorder[0].body.decode())
assert request == {'localId' : 'testuser', 'customAttributes' : json.dumps(claims)}

def test_update_user_delete_fields(self, user_mgt_app):
def test_delete_user_custom_claims(self, user_mgt_app):
user_mgt, recorder = _instrument_user_manager(user_mgt_app, 200, '{"localId":"testuser"}')
user_mgt.update_user('testuser', custom_claims=auth.DELETE_ATTRIBUTE)
request = json.loads(recorder[0].body.decode())
assert request == {'localId' : 'testuser', 'customAttributes' : json.dumps({})}

def test_update_user_delete_fields_with_none(self, user_mgt_app):
user_mgt, recorder = _instrument_user_manager(user_mgt_app, 200, '{"localId":"testuser"}')
user_mgt.update_user('testuser', display_name=None, photo_url=None, phone_number=None)
request = json.loads(recorder[0].body.decode())
Expand All @@ -391,6 +397,20 @@ def test_update_user_delete_fields(self, user_mgt_app):
'deleteProvider' : ['phone'],
}

def test_update_user_delete_fields(self, user_mgt_app):
user_mgt, recorder = _instrument_user_manager(user_mgt_app, 200, '{"localId":"testuser"}')
user_mgt.update_user(
'testuser',
display_name=auth.DELETE_ATTRIBUTE,
photo_url=auth.DELETE_ATTRIBUTE,
phone_number=auth.DELETE_ATTRIBUTE)
request = json.loads(recorder[0].body.decode())
assert request == {
'localId' : 'testuser',
'deleteAttribute' : ['DISPLAY_NAME', 'PHOTO_URL'],
'deleteProvider' : ['phone'],
}

def test_update_user_error(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 500, '{"error":"test"}')
with pytest.raises(auth.AuthError) as excinfo:
Expand Down