-
Couldn't load subscription status.
- Fork 344
feat(auth): Add bulk get/delete methods #400
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
aa8207d
a83f66e
c5f57d0
0e282f7
070c1a8
466890b
598ffc9
1993007
0b18203
65d7d63
b21f23e
d4a8a3e
3c6b776
2167764
9ade7a8
278ee4a
7087016
5990d44
ba60c65
e9dafc1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| # Copyright 2019 Google Inc. | ||
hiranya911 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| """Classes to uniquely identify a user.""" | ||
|
|
||
| class UserIdentifier: | ||
| """Identifies a user to be looked up.""" | ||
|
|
||
|
|
||
| class UidIdentifier(UserIdentifier): | ||
| """Used for looking up an account by uid. | ||
| See ``auth.get_user()``. | ||
| """ | ||
|
|
||
| def __init__(self, uid): | ||
| """Constructs a new UidIdentifier. | ||
| Args: | ||
| uid: A user ID string. | ||
| """ | ||
| self.uid = uid | ||
|
|
||
|
|
||
| class EmailIdentifier(UserIdentifier): | ||
| """Used for looking up an account by email. | ||
| See ``auth.get_user()``. | ||
| """ | ||
|
|
||
| def __init__(self, email): | ||
| """Constructs a new EmailIdentifier. | ||
| Args: | ||
| email: A user email address string. | ||
| """ | ||
| self.email = email | ||
|
|
||
|
|
||
| class PhoneIdentifier(UserIdentifier): | ||
| """Used for looking up an account by phone number. | ||
| See ``auth.get_user()``. | ||
| """ | ||
|
|
||
| def __init__(self, phone_number): | ||
| """Constructs a new PhoneIdentifier. | ||
| Args: | ||
| phone_number: A phone number string. | ||
| """ | ||
| self.phone_number = phone_number | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,8 +19,10 @@ | |
| from urllib import parse | ||
|
|
||
| import requests | ||
| import iso8601 | ||
hiranya911 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| from firebase_admin import _auth_utils | ||
| from firebase_admin import _identifier | ||
| from firebase_admin import _user_import | ||
|
|
||
|
|
||
|
|
@@ -41,11 +43,14 @@ def __init__(self, description): | |
| class UserMetadata: | ||
| """Contains additional metadata associated with a user account.""" | ||
|
|
||
| def __init__(self, creation_timestamp=None, last_sign_in_timestamp=None): | ||
| def __init__(self, creation_timestamp=None, last_sign_in_timestamp=None, | ||
| last_refresh_timestamp=None): | ||
| self._creation_timestamp = _auth_utils.validate_timestamp( | ||
| creation_timestamp, 'creation_timestamp') | ||
| self._last_sign_in_timestamp = _auth_utils.validate_timestamp( | ||
| last_sign_in_timestamp, 'last_sign_in_timestamp') | ||
| self._last_refresh_timestamp = _auth_utils.validate_timestamp( | ||
| last_refresh_timestamp, 'last_refresh_timestamp') | ||
|
|
||
| @property | ||
| def creation_timestamp(self): | ||
|
|
@@ -65,6 +70,16 @@ def last_sign_in_timestamp(self): | |
| """ | ||
| return self._last_sign_in_timestamp | ||
|
|
||
| @property | ||
| def last_refresh_timestamp(self): | ||
| """The time at which the user was last active (ID token refreshed). | ||
|
|
||
| Returns: | ||
| integer: Milliseconds since epoch timestamp, or None if the user was | ||
|
||
| never active. | ||
| """ | ||
| return self._last_refresh_timestamp | ||
|
|
||
|
|
||
| class UserInfo: | ||
| """A collection of standard profile information for a user. | ||
|
|
@@ -216,7 +231,12 @@ def _int_or_none(key): | |
| if key in self._data: | ||
| return int(self._data[key]) | ||
| return None | ||
| return UserMetadata(_int_or_none('createdAt'), _int_or_none('lastLoginAt')) | ||
| last_refresh_at_millis = None | ||
| last_refresh_at_iso8601 = self._data.get('lastRefreshAt', None) | ||
| if last_refresh_at_iso8601 is not None: | ||
hiranya911 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| last_refresh_at_millis = iso8601.parse_date(last_refresh_at_iso8601).timestamp() * 1000 | ||
|
||
| return UserMetadata( | ||
| _int_or_none('createdAt'), _int_or_none('lastLoginAt'), last_refresh_at_millis) | ||
|
|
||
| @property | ||
| def provider_data(self): | ||
|
|
@@ -331,6 +351,85 @@ def iterate_all(self): | |
| return _UserIterator(self) | ||
|
|
||
|
|
||
| class DeleteUsersResult: | ||
| """Represents the result of the ``auth.delete_users()`` API.""" | ||
|
|
||
| def __init__(self, result, total): | ||
| """Constructs a DeleteUsersResult. | ||
|
||
|
|
||
| Args: | ||
| result: BatchDeleteAccountsResponse: The proto response, wrapped in a | ||
| BatchDeleteAccountsResponse instance. | ||
|
||
| total: integer: Total number of deletion attempts. | ||
| """ | ||
| errors = result.errors | ||
| self._success_count = total - len(errors) | ||
| self._failure_count = len(errors) | ||
| self._errors = errors | ||
|
|
||
| @property | ||
| def success_count(self): | ||
| """Returns the number of users that were deleted successfully (possibly | ||
| zero). | ||
|
|
||
| Users that did not exist prior to calling delete_users() will be | ||
|
||
| considered to be successfully deleted. | ||
| """ | ||
| return self._success_count | ||
|
|
||
| @property | ||
| def failure_count(self): | ||
| """Returns the number of users that failed to be deleted (possibly | ||
| zero). | ||
| """ | ||
| return self._failure_count | ||
|
|
||
| @property | ||
| def errors(self): | ||
| """A list of ``auth.BatchDeleteErrorInfo`` instances describing the | ||
| errors that were encountered during the deletion. Length of this list | ||
| is equal to `failure_count`. | ||
| """ | ||
| return self._errors | ||
|
|
||
|
|
||
| class BatchDeleteErrorInfo: | ||
hiranya911 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| """Represents an error that occurred while attempting to delete a batch of | ||
| users. | ||
| """ | ||
|
|
||
| def __init__(self, err): | ||
| """Constructs a BatchDeleteErrorInfo instance, corresponding to the | ||
| json representing the BatchDeleteErrorInfo proto. | ||
|
|
||
| Args: | ||
| err: A dictionary with 'index', 'local_id' and 'message' fields, | ||
| representing the BatchDeleteErrorInfo dictionary that's | ||
| returned by the server. | ||
| """ | ||
| self.index = err.get('index', 0) | ||
| self.local_id = err.get('local_id', "") | ||
| self.message = err.get('message', "") | ||
|
|
||
|
|
||
| class BatchDeleteAccountsResponse: | ||
| """Represents the results of a delete_users() call.""" | ||
|
||
|
|
||
| def __init__(self, errors=None): | ||
| """Constructs a BatchDeleteAccountsResponse instance, corresponseing to | ||
|
||
| the json representing the BatchDeleteAccountsResponse proto. | ||
|
||
|
|
||
| Args: | ||
| errors: List of dictionaries, with each dictionary representing a | ||
| BatchDeleteErrorInfo instance as returned by the server. None | ||
| implies an empty list. | ||
| """ | ||
| if errors: | ||
| self.errors = [BatchDeleteErrorInfo(err) for err in errors] | ||
hiranya911 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| else: | ||
| self.errors = [] | ||
|
|
||
|
|
||
| class ProviderUserInfo(UserInfo): | ||
| """Contains metadata regarding how a user is known by a particular identity provider.""" | ||
|
|
||
|
|
@@ -483,6 +582,53 @@ def get_user(self, **kwargs): | |
| http_response=http_resp) | ||
| return body['users'][0] | ||
|
|
||
| def get_users(self, identifiers): | ||
| """Looks up multiple users by their identifiers (uid, email, etc.) | ||
|
|
||
| Args: | ||
| identifiers: UserIdentifier[]: The identifiers indicating the user | ||
| to be looked up. Must have <= 100 entries. | ||
|
|
||
| Returns: | ||
| list[dict[string, string]]: List of dicts representing the json | ||
| UserInfo responses from the server. | ||
|
||
|
|
||
| Raises: | ||
| ValueError: If any of the identifiers are invalid or if more than | ||
| 100 identifiers are specified. | ||
| UnexpectedResponseError: If the backend server responds with an | ||
| unexpected message. | ||
| """ | ||
| if not identifiers: | ||
| return [] | ||
| if len(identifiers) > 100: | ||
| raise ValueError('`identifiers` parameter must have <= 100 entries.') | ||
|
|
||
| payload = {} | ||
| for identifier in identifiers: | ||
| if isinstance(identifier, _identifier.UidIdentifier): | ||
| _auth_utils.validate_uid(identifier.uid, required=True) | ||
|
||
| payload['localId'] = payload.get('localId', []) + [identifier.uid] | ||
hiranya911 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| elif isinstance(identifier, _identifier.EmailIdentifier): | ||
| _auth_utils.validate_email(identifier.email, required=True) | ||
| payload['email'] = payload.get('email', []) + [identifier.email] | ||
| elif isinstance(identifier, _identifier.PhoneIdentifier): | ||
| _auth_utils.validate_phone(identifier.phone_number, required=True) | ||
| payload['phoneNumber'] = payload.get('phoneNumber', []) + [identifier.phone_number] | ||
| else: | ||
| raise ValueError('Invalid argument `identifiers`. Unrecognized type.') | ||
hiranya911 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| try: | ||
| body, http_resp = self._client.body_and_response( | ||
| 'post', '/accounts:lookup', json=payload) | ||
| except requests.exceptions.RequestException as error: | ||
| raise _auth_utils.handle_auth_backend_error(error) | ||
| else: | ||
| if not http_resp.ok: | ||
| raise _auth_utils.UnexpectedResponseError( | ||
| 'Failed to get users.', http_response=http_resp) | ||
| return body.get('users', []) | ||
|
|
||
| def list_users(self, page_token=None, max_results=MAX_LIST_USERS_RESULTS): | ||
| """Retrieves a batch of users.""" | ||
| if page_token is not None: | ||
|
|
@@ -592,6 +738,47 @@ def delete_user(self, uid): | |
| raise _auth_utils.UnexpectedResponseError( | ||
| 'Failed to delete user: {0}.'.format(uid), http_response=http_resp) | ||
|
|
||
| def delete_users(self, uids, force_delete=False): | ||
| """Deletes the users identified by the specified user ids. | ||
|
|
||
| Args: | ||
| uids: A list of strings indicating the uids of the users to be deleted. | ||
| Must have <= 1000 entries. | ||
| force_delete: Optional parameter that indicates if users should be | ||
| deleted, even if they're not disabled. Defaults to False. | ||
|
|
||
|
|
||
| Returns: | ||
| BatchDeleteAccountsResponse: Server's proto response, wrapped in a | ||
| python object. | ||
|
|
||
| Raises: | ||
| ValueError: If any of the identifiers are invalid or if more than 1000 | ||
| identifiers are specified. | ||
| UnexpectedResponseError: If the backend server responds with an | ||
| unexpected message. | ||
| """ | ||
| if not uids: | ||
| return BatchDeleteAccountsResponse() | ||
|
|
||
| if len(uids) > 100: | ||
hiranya911 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| raise ValueError("`uids` paramter must have <= 100 entries.") | ||
| for uid in uids: | ||
| _auth_utils.validate_uid(uid, required=True) | ||
|
|
||
| try: | ||
| body, http_resp = self._client.body_and_response( | ||
| 'post', '/accounts:batchDelete', | ||
| json={'localIds': uids, 'force': force_delete}) | ||
| except requests.exceptions.RequestException as error: | ||
| raise _auth_utils.handle_auth_backend_error(error) | ||
| else: | ||
| if not isinstance(body, dict): | ||
| raise _auth_utils.UnexpectedResponseError( | ||
| 'Unexpected response from server while attempting to delete users.', | ||
| http_response=http_resp) | ||
| return BatchDeleteAccountsResponse(body.get('errors', [])) | ||
|
|
||
| def import_users(self, users, hash_alg=None): | ||
| """Imports the given list of users to Firebase Auth.""" | ||
| try: | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.