diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ceae9ffd..7dadefbda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/firebase_admin/_user_mgt.py b/firebase_admin/_user_mgt.py index 71e2055ad..24bb2bdb6 100644 --- a/firebase_admin/_user_mgt.py +++ b/firebase_admin/_user_mgt.py @@ -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): @@ -546,12 +553,12 @@ 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) @@ -559,13 +566,13 @@ def update_user(self, uid, display_name=_UNSPECIFIED, email=None, phone_number=_ 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 diff --git a/firebase_admin/auth.py b/firebase_admin/auth.py index 4f3d34b0b..0800d7c1e 100644 --- a/firebase_admin/auth.py +++ b/firebase_admin/auth.py @@ -37,6 +37,7 @@ __all__ = [ 'ActionCodeSettings', 'AuthError', + 'DELETE_ATTRIBUTE', 'ErrorInfo', 'ExportedUserRecord', 'ImportUserRecord', @@ -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 @@ -359,17 +361,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. diff --git a/tests/test_user_mgt.py b/tests/test_user_mgt.py index 6e033fae4..797e0ce59 100644 --- a/tests/test_user_mgt.py +++ b/tests/test_user_mgt.py @@ -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()) @@ -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: