Added delete option to database storage.
This commit is contained in:
parent
308604a33c
commit
963b5bc68b
1868 changed files with 192402 additions and 13278 deletions
846
venv/Lib/site-packages/firebase_admin/_user_mgt.py
Normal file
846
venv/Lib/site-packages/firebase_admin/_user_mgt.py
Normal file
|
@ -0,0 +1,846 @@
|
|||
# Copyright 2017 Google Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Firebase user management sub module."""
|
||||
|
||||
import base64
|
||||
from collections import defaultdict
|
||||
import json
|
||||
from urllib import parse
|
||||
|
||||
import requests
|
||||
|
||||
from firebase_admin import _auth_utils
|
||||
from firebase_admin import _rfc3339
|
||||
from firebase_admin import _user_identifier
|
||||
from firebase_admin import _user_import
|
||||
from firebase_admin._user_import import ErrorInfo
|
||||
|
||||
|
||||
MAX_LIST_USERS_RESULTS = 1000
|
||||
MAX_IMPORT_USERS_SIZE = 1000
|
||||
B64_REDACTED = base64.b64encode(b'REDACTED')
|
||||
|
||||
|
||||
class Sentinel:
|
||||
|
||||
def __init__(self, description):
|
||||
self.description = description
|
||||
|
||||
|
||||
DELETE_ATTRIBUTE = Sentinel('Value used to delete an attribute from a user profile')
|
||||
|
||||
|
||||
class UserMetadata:
|
||||
"""Contains additional metadata associated with a user account."""
|
||||
|
||||
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):
|
||||
""" Creation timestamp in milliseconds since the epoch.
|
||||
|
||||
Returns:
|
||||
integer: The user creation timestamp in milliseconds since the epoch.
|
||||
"""
|
||||
return self._creation_timestamp
|
||||
|
||||
@property
|
||||
def last_sign_in_timestamp(self):
|
||||
""" Last sign in timestamp in milliseconds since the epoch.
|
||||
|
||||
Returns:
|
||||
integer: The last sign in timestamp in milliseconds since the epoch.
|
||||
"""
|
||||
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.
|
||||
|
||||
Used to expose profile information returned by an identity provider.
|
||||
"""
|
||||
|
||||
@property
|
||||
def uid(self):
|
||||
"""Returns the user ID of this user."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
"""Returns the display name of this user."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def email(self):
|
||||
"""Returns the email address associated with this user."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def phone_number(self):
|
||||
"""Returns the phone number associated with this user."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def photo_url(self):
|
||||
"""Returns the photo URL of this user."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def provider_id(self):
|
||||
"""Returns the ID of the identity provider.
|
||||
|
||||
This can be a short domain name (e.g. google.com), or the identity of an OpenID
|
||||
identity provider.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class UserRecord(UserInfo):
|
||||
"""Contains metadata associated with a Firebase user account."""
|
||||
|
||||
def __init__(self, data):
|
||||
super(UserRecord, self).__init__()
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError('Invalid data argument: {0}. Must be a dictionary.'.format(data))
|
||||
if not data.get('localId'):
|
||||
raise ValueError('User ID must not be None or empty.')
|
||||
self._data = data
|
||||
|
||||
@property
|
||||
def uid(self):
|
||||
"""Returns the user ID of this user.
|
||||
|
||||
Returns:
|
||||
string: A user ID string. This value is never None or empty.
|
||||
"""
|
||||
return self._data.get('localId')
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
"""Returns the display name of this user.
|
||||
|
||||
Returns:
|
||||
string: A display name string or None.
|
||||
"""
|
||||
return self._data.get('displayName')
|
||||
|
||||
@property
|
||||
def email(self):
|
||||
"""Returns the email address associated with this user.
|
||||
|
||||
Returns:
|
||||
string: An email address string or None.
|
||||
"""
|
||||
return self._data.get('email')
|
||||
|
||||
@property
|
||||
def phone_number(self):
|
||||
"""Returns the phone number associated with this user.
|
||||
|
||||
Returns:
|
||||
string: A phone number string or None.
|
||||
"""
|
||||
return self._data.get('phoneNumber')
|
||||
|
||||
@property
|
||||
def photo_url(self):
|
||||
"""Returns the photo URL of this user.
|
||||
|
||||
Returns:
|
||||
string: A URL string or None.
|
||||
"""
|
||||
return self._data.get('photoUrl')
|
||||
|
||||
@property
|
||||
def provider_id(self):
|
||||
"""Returns the provider ID of this user.
|
||||
|
||||
Returns:
|
||||
string: A constant provider ID value.
|
||||
"""
|
||||
return 'firebase'
|
||||
|
||||
@property
|
||||
def email_verified(self):
|
||||
"""Returns whether the email address of this user has been verified.
|
||||
|
||||
Returns:
|
||||
bool: True if the email has been verified, and False otherwise.
|
||||
"""
|
||||
return bool(self._data.get('emailVerified'))
|
||||
|
||||
@property
|
||||
def disabled(self):
|
||||
"""Returns whether this user account is disabled.
|
||||
|
||||
Returns:
|
||||
bool: True if the user account is disabled, and False otherwise.
|
||||
"""
|
||||
return bool(self._data.get('disabled'))
|
||||
|
||||
@property
|
||||
def tokens_valid_after_timestamp(self):
|
||||
"""Returns the time, in milliseconds since the epoch, before which tokens are invalid.
|
||||
|
||||
Note: this is truncated to 1 second accuracy.
|
||||
|
||||
Returns:
|
||||
int: Timestamp in milliseconds since the epoch, truncated to the second.
|
||||
All tokens issued before that time are considered revoked.
|
||||
"""
|
||||
valid_since = self._data.get('validSince')
|
||||
if valid_since is not None:
|
||||
return 1000 * int(valid_since)
|
||||
return 0
|
||||
|
||||
@property
|
||||
def user_metadata(self):
|
||||
"""Returns additional metadata associated with this user.
|
||||
|
||||
Returns:
|
||||
UserMetadata: A UserMetadata instance. Does not return None.
|
||||
"""
|
||||
def _int_or_none(key):
|
||||
if key in self._data:
|
||||
return int(self._data[key])
|
||||
return None
|
||||
last_refresh_at_millis = None
|
||||
last_refresh_at_rfc3339 = self._data.get('lastRefreshAt', None)
|
||||
if last_refresh_at_rfc3339:
|
||||
last_refresh_at_millis = int(_rfc3339.parse_to_epoch(last_refresh_at_rfc3339) * 1000)
|
||||
return UserMetadata(
|
||||
_int_or_none('createdAt'), _int_or_none('lastLoginAt'), last_refresh_at_millis)
|
||||
|
||||
@property
|
||||
def provider_data(self):
|
||||
"""Returns a list of UserInfo instances.
|
||||
|
||||
Each object represents an identity from an identity provider that is linked to this user.
|
||||
|
||||
Returns:
|
||||
list: A list of UserInfo objects, which may be empty.
|
||||
"""
|
||||
providers = self._data.get('providerUserInfo', [])
|
||||
return [ProviderUserInfo(entry) for entry in providers]
|
||||
|
||||
@property
|
||||
def custom_claims(self):
|
||||
"""Returns any custom claims set on this user account.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary of claims or None.
|
||||
"""
|
||||
claims = self._data.get('customAttributes')
|
||||
if claims:
|
||||
parsed = json.loads(claims)
|
||||
if parsed != {}:
|
||||
return parsed
|
||||
return None
|
||||
|
||||
@property
|
||||
def tenant_id(self):
|
||||
"""Returns the tenant ID of this user.
|
||||
|
||||
Returns:
|
||||
string: A tenant ID string or None.
|
||||
"""
|
||||
return self._data.get('tenantId')
|
||||
|
||||
|
||||
class ExportedUserRecord(UserRecord):
|
||||
"""Contains metadata associated with a user including password hash and salt."""
|
||||
|
||||
@property
|
||||
def password_hash(self):
|
||||
"""The user's password hash as a base64-encoded string.
|
||||
|
||||
If the Firebase Auth hashing algorithm (SCRYPT) was used to create the user account, this
|
||||
is the base64-encoded password hash of the user. If a different hashing algorithm was
|
||||
used to create this user, as is typical when migrating from another Auth system, this
|
||||
is an empty string. If no password is set, or if the service account doesn't have permission
|
||||
to read the password, then this is ``None``.
|
||||
"""
|
||||
password_hash = self._data.get('passwordHash')
|
||||
|
||||
# If the password hash is redacted (probably due to missing permissions) then clear it out,
|
||||
# similar to how the salt is returned. (Otherwise, it *looks* like a b64-encoded hash is
|
||||
# present, which is confusing.)
|
||||
if password_hash == B64_REDACTED:
|
||||
return None
|
||||
return password_hash
|
||||
|
||||
@property
|
||||
def password_salt(self):
|
||||
"""The user's password salt as a base64-encoded string.
|
||||
|
||||
If the Firebase Auth hashing algorithm (SCRYPT) was used to create the user account, this
|
||||
is the base64-encoded password salt of the user. If a different hashing algorithm was
|
||||
used to create this user, as is typical when migrating from another Auth system, this is
|
||||
an empty string. If no password is set, or if the service account doesn't have permission to
|
||||
read the password, then this is ``None``.
|
||||
"""
|
||||
return self._data.get('salt')
|
||||
|
||||
|
||||
class GetUsersResult:
|
||||
"""Represents the result of the ``auth.get_users()`` API."""
|
||||
|
||||
def __init__(self, users, not_found):
|
||||
"""Constructs a `GetUsersResult` object.
|
||||
|
||||
Args:
|
||||
users: List of `UserRecord` instances.
|
||||
not_found: List of `UserIdentifier` instances.
|
||||
"""
|
||||
self._users = users
|
||||
self._not_found = not_found
|
||||
|
||||
@property
|
||||
def users(self):
|
||||
"""Set of `UserRecord` instances, corresponding to the set of users
|
||||
that were requested. Only users that were found are listed here. The
|
||||
result set is unordered.
|
||||
"""
|
||||
return self._users
|
||||
|
||||
@property
|
||||
def not_found(self):
|
||||
"""Set of `UserIdentifier` instances that were requested, but not
|
||||
found.
|
||||
"""
|
||||
return self._not_found
|
||||
|
||||
|
||||
class ListUsersPage:
|
||||
"""Represents a page of user records exported from a Firebase project.
|
||||
|
||||
Provides methods for traversing the user accounts included in this page, as well as retrieving
|
||||
subsequent pages of users. The iterator returned by ``iterate_all()`` can be used to iterate
|
||||
through all users in the Firebase project starting from this page.
|
||||
"""
|
||||
|
||||
def __init__(self, download, page_token, max_results):
|
||||
self._download = download
|
||||
self._max_results = max_results
|
||||
self._current = download(page_token, max_results)
|
||||
|
||||
@property
|
||||
def users(self):
|
||||
"""A list of ``ExportedUserRecord`` instances available in this page."""
|
||||
return [ExportedUserRecord(user) for user in self._current.get('users', [])]
|
||||
|
||||
@property
|
||||
def next_page_token(self):
|
||||
"""Page token string for the next page (empty string indicates no more pages)."""
|
||||
return self._current.get('nextPageToken', '')
|
||||
|
||||
@property
|
||||
def has_next_page(self):
|
||||
"""A boolean indicating whether more pages are available."""
|
||||
return bool(self.next_page_token)
|
||||
|
||||
def get_next_page(self):
|
||||
"""Retrieves the next page of user accounts, if available.
|
||||
|
||||
Returns:
|
||||
ListUsersPage: Next page of users, or None if this is the last page.
|
||||
"""
|
||||
if self.has_next_page:
|
||||
return ListUsersPage(self._download, self.next_page_token, self._max_results)
|
||||
return None
|
||||
|
||||
def iterate_all(self):
|
||||
"""Retrieves an iterator for user accounts.
|
||||
|
||||
Returned iterator will iterate through all the user accounts in the Firebase project
|
||||
starting from this page. The iterator will never buffer more than one page of users
|
||||
in memory at a time.
|
||||
|
||||
Returns:
|
||||
iterator: An iterator of ExportedUserRecord instances.
|
||||
"""
|
||||
return _UserIterator(self)
|
||||
|
||||
|
||||
class DeleteUsersResult:
|
||||
"""Represents the result of the ``auth.delete_users()`` API."""
|
||||
|
||||
def __init__(self, result, total):
|
||||
"""Constructs a `DeleteUsersResult` object.
|
||||
|
||||
Args:
|
||||
result: The proto response, wrapped in a
|
||||
`BatchDeleteAccountsResponse` instance.
|
||||
total: Total integer 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()` are
|
||||
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.ErrorInfo` instances describing the errors that
|
||||
were encountered during the deletion. Length of this list is equal to
|
||||
`failure_count`.
|
||||
"""
|
||||
return self._errors
|
||||
|
||||
|
||||
class BatchDeleteAccountsResponse:
|
||||
"""Represents the results of a `delete_users()` call."""
|
||||
|
||||
def __init__(self, errors=None):
|
||||
"""Constructs a `BatchDeleteAccountsResponse` instance, corresponding to
|
||||
the JSON representing the `BatchDeleteAccountsResponse` proto.
|
||||
|
||||
Args:
|
||||
errors: List of dictionaries, with each dictionary representing an
|
||||
`ErrorInfo` instance as returned by the server. `None` implies
|
||||
an empty list.
|
||||
"""
|
||||
self.errors = [ErrorInfo(err) for err in errors] if errors else []
|
||||
|
||||
|
||||
class ProviderUserInfo(UserInfo):
|
||||
"""Contains metadata regarding how a user is known by a particular identity provider."""
|
||||
|
||||
def __init__(self, data):
|
||||
super(ProviderUserInfo, self).__init__()
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError('Invalid data argument: {0}. Must be a dictionary.'.format(data))
|
||||
if not data.get('rawId'):
|
||||
raise ValueError('User ID must not be None or empty.')
|
||||
self._data = data
|
||||
|
||||
@property
|
||||
def uid(self):
|
||||
return self._data.get('rawId')
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return self._data.get('displayName')
|
||||
|
||||
@property
|
||||
def email(self):
|
||||
return self._data.get('email')
|
||||
|
||||
@property
|
||||
def phone_number(self):
|
||||
return self._data.get('phoneNumber')
|
||||
|
||||
@property
|
||||
def photo_url(self):
|
||||
return self._data.get('photoUrl')
|
||||
|
||||
@property
|
||||
def provider_id(self):
|
||||
return self._data.get('providerId')
|
||||
|
||||
|
||||
class ActionCodeSettings:
|
||||
"""Contains required continue/state URL with optional Android and iOS settings.
|
||||
Used when invoking the email action link generation APIs.
|
||||
"""
|
||||
|
||||
def __init__(self, url, handle_code_in_app=None, dynamic_link_domain=None, ios_bundle_id=None,
|
||||
android_package_name=None, android_install_app=None, android_minimum_version=None):
|
||||
self.url = url
|
||||
self.handle_code_in_app = handle_code_in_app
|
||||
self.dynamic_link_domain = dynamic_link_domain
|
||||
self.ios_bundle_id = ios_bundle_id
|
||||
self.android_package_name = android_package_name
|
||||
self.android_install_app = android_install_app
|
||||
self.android_minimum_version = android_minimum_version
|
||||
|
||||
|
||||
def encode_action_code_settings(settings):
|
||||
""" Validates the provided action code settings for email link generation and
|
||||
populates the REST api parameters.
|
||||
|
||||
settings - ``ActionCodeSettings`` object provided to be encoded
|
||||
returns - dict of parameters to be passed for link gereration.
|
||||
"""
|
||||
|
||||
parameters = {}
|
||||
# url
|
||||
if not settings.url:
|
||||
raise ValueError("Dynamic action links url is mandatory")
|
||||
|
||||
try:
|
||||
parsed = parse.urlparse(settings.url)
|
||||
if not parsed.netloc:
|
||||
raise ValueError('Malformed dynamic action links url: "{0}".'.format(settings.url))
|
||||
parameters['continueUrl'] = settings.url
|
||||
except Exception:
|
||||
raise ValueError('Malformed dynamic action links url: "{0}".'.format(settings.url))
|
||||
|
||||
# handle_code_in_app
|
||||
if settings.handle_code_in_app is not None:
|
||||
if not isinstance(settings.handle_code_in_app, bool):
|
||||
raise ValueError('Invalid value provided for handle_code_in_app: {0}'
|
||||
.format(settings.handle_code_in_app))
|
||||
parameters['canHandleCodeInApp'] = settings.handle_code_in_app
|
||||
|
||||
# dynamic_link_domain
|
||||
if settings.dynamic_link_domain is not None:
|
||||
if not isinstance(settings.dynamic_link_domain, str):
|
||||
raise ValueError('Invalid value provided for dynamic_link_domain: {0}'
|
||||
.format(settings.dynamic_link_domain))
|
||||
parameters['dynamicLinkDomain'] = settings.dynamic_link_domain
|
||||
|
||||
# ios_bundle_id
|
||||
if settings.ios_bundle_id is not None:
|
||||
if not isinstance(settings.ios_bundle_id, str):
|
||||
raise ValueError('Invalid value provided for ios_bundle_id: {0}'
|
||||
.format(settings.ios_bundle_id))
|
||||
parameters['iosBundleId'] = settings.ios_bundle_id
|
||||
|
||||
# android_* attributes
|
||||
if (settings.android_minimum_version or settings.android_install_app) \
|
||||
and not settings.android_package_name:
|
||||
raise ValueError("Android package name is required when specifying other Android settings")
|
||||
|
||||
if settings.android_package_name is not None:
|
||||
if not isinstance(settings.android_package_name, str):
|
||||
raise ValueError('Invalid value provided for android_package_name: {0}'
|
||||
.format(settings.android_package_name))
|
||||
parameters['androidPackageName'] = settings.android_package_name
|
||||
|
||||
if settings.android_minimum_version is not None:
|
||||
if not isinstance(settings.android_minimum_version, str):
|
||||
raise ValueError('Invalid value provided for android_minimum_version: {0}'
|
||||
.format(settings.android_minimum_version))
|
||||
parameters['androidMinimumVersion'] = settings.android_minimum_version
|
||||
|
||||
if settings.android_install_app is not None:
|
||||
if not isinstance(settings.android_install_app, bool):
|
||||
raise ValueError('Invalid value provided for android_install_app: {0}'
|
||||
.format(settings.android_install_app))
|
||||
parameters['androidInstallApp'] = settings.android_install_app
|
||||
|
||||
return parameters
|
||||
|
||||
|
||||
class UserManager:
|
||||
"""Provides methods for interacting with the Google Identity Toolkit."""
|
||||
|
||||
ID_TOOLKIT_URL = 'https://identitytoolkit.googleapis.com/v1'
|
||||
|
||||
def __init__(self, http_client, project_id, tenant_id=None):
|
||||
self.http_client = http_client
|
||||
self.base_url = '{0}/projects/{1}'.format(self.ID_TOOLKIT_URL, project_id)
|
||||
if tenant_id:
|
||||
self.base_url += '/tenants/{0}'.format(tenant_id)
|
||||
|
||||
def get_user(self, **kwargs):
|
||||
"""Gets the user data corresponding to the provided key."""
|
||||
if 'uid' in kwargs:
|
||||
key, key_type = kwargs.pop('uid'), 'user ID'
|
||||
payload = {'localId' : [_auth_utils.validate_uid(key, required=True)]}
|
||||
elif 'email' in kwargs:
|
||||
key, key_type = kwargs.pop('email'), 'email'
|
||||
payload = {'email' : [_auth_utils.validate_email(key, required=True)]}
|
||||
elif 'phone_number' in kwargs:
|
||||
key, key_type = kwargs.pop('phone_number'), 'phone number'
|
||||
payload = {'phoneNumber' : [_auth_utils.validate_phone(key, required=True)]}
|
||||
else:
|
||||
raise TypeError('Unsupported keyword arguments: {0}.'.format(kwargs))
|
||||
|
||||
body, http_resp = self._make_request('post', '/accounts:lookup', json=payload)
|
||||
if not body or not body.get('users'):
|
||||
raise _auth_utils.UserNotFoundError(
|
||||
'No user record found for the provided {0}: {1}.'.format(key_type, key),
|
||||
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 = defaultdict(list)
|
||||
for identifier in identifiers:
|
||||
if isinstance(identifier, _user_identifier.UidIdentifier):
|
||||
payload['localId'].append(identifier.uid)
|
||||
elif isinstance(identifier, _user_identifier.EmailIdentifier):
|
||||
payload['email'].append(identifier.email)
|
||||
elif isinstance(identifier, _user_identifier.PhoneIdentifier):
|
||||
payload['phoneNumber'].append(identifier.phone_number)
|
||||
elif isinstance(identifier, _user_identifier.ProviderIdentifier):
|
||||
payload['federatedUserId'].append({
|
||||
'providerId': identifier.provider_id,
|
||||
'rawId': identifier.provider_uid
|
||||
})
|
||||
else:
|
||||
raise ValueError(
|
||||
'Invalid entry in "identifiers" list. Unsupported type: {}'
|
||||
.format(type(identifier)))
|
||||
|
||||
body, http_resp = self._make_request(
|
||||
'post', '/accounts:lookup', json=payload)
|
||||
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:
|
||||
if not isinstance(page_token, str) or not page_token:
|
||||
raise ValueError('Page token must be a non-empty string.')
|
||||
if not isinstance(max_results, int):
|
||||
raise ValueError('Max results must be an integer.')
|
||||
if max_results < 1 or max_results > MAX_LIST_USERS_RESULTS:
|
||||
raise ValueError(
|
||||
'Max results must be a positive integer less than '
|
||||
'{0}.'.format(MAX_LIST_USERS_RESULTS))
|
||||
|
||||
payload = {'maxResults': max_results}
|
||||
if page_token:
|
||||
payload['nextPageToken'] = page_token
|
||||
body, _ = self._make_request('get', '/accounts:batchGet', params=payload)
|
||||
return body
|
||||
|
||||
def create_user(self, uid=None, display_name=None, email=None, phone_number=None,
|
||||
photo_url=None, password=None, disabled=None, email_verified=None):
|
||||
"""Creates a new user account with the specified properties."""
|
||||
payload = {
|
||||
'localId': _auth_utils.validate_uid(uid),
|
||||
'displayName': _auth_utils.validate_display_name(display_name),
|
||||
'email': _auth_utils.validate_email(email),
|
||||
'phoneNumber': _auth_utils.validate_phone(phone_number),
|
||||
'photoUrl': _auth_utils.validate_photo_url(photo_url),
|
||||
'password': _auth_utils.validate_password(password),
|
||||
'emailVerified': bool(email_verified) if email_verified is not None else None,
|
||||
'disabled': bool(disabled) if disabled is not None else None,
|
||||
}
|
||||
payload = {k: v for k, v in payload.items() if v is not None}
|
||||
body, http_resp = self._make_request('post', '/accounts', json=payload)
|
||||
if not body or not body.get('localId'):
|
||||
raise _auth_utils.UnexpectedResponseError(
|
||||
'Failed to create new user.', http_response=http_resp)
|
||||
return body.get('localId')
|
||||
|
||||
def update_user(self, uid, display_name=None, email=None, phone_number=None,
|
||||
photo_url=None, password=None, disabled=None, email_verified=None,
|
||||
valid_since=None, custom_claims=None):
|
||||
"""Updates an existing user account with the specified properties"""
|
||||
payload = {
|
||||
'localId': _auth_utils.validate_uid(uid, required=True),
|
||||
'email': _auth_utils.validate_email(email),
|
||||
'password': _auth_utils.validate_password(password),
|
||||
'validSince': _auth_utils.validate_timestamp(valid_since, 'valid_since'),
|
||||
'emailVerified': bool(email_verified) if email_verified is not None else None,
|
||||
'disableUser': bool(disabled) if disabled is not None else None,
|
||||
}
|
||||
|
||||
remove = []
|
||||
if display_name is not None:
|
||||
if display_name is DELETE_ATTRIBUTE:
|
||||
remove.append('DISPLAY_NAME')
|
||||
else:
|
||||
payload['displayName'] = _auth_utils.validate_display_name(display_name)
|
||||
if photo_url is not None:
|
||||
if 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 None:
|
||||
if phone_number is DELETE_ATTRIBUTE:
|
||||
payload['deleteProvider'] = ['phone']
|
||||
else:
|
||||
payload['phoneNumber'] = _auth_utils.validate_phone(phone_number)
|
||||
|
||||
if custom_claims is not None:
|
||||
if custom_claims is DELETE_ATTRIBUTE:
|
||||
custom_claims = {}
|
||||
json_claims = json.dumps(custom_claims) if isinstance(
|
||||
custom_claims, dict) else custom_claims
|
||||
payload['customAttributes'] = _auth_utils.validate_custom_claims(json_claims)
|
||||
|
||||
payload = {k: v for k, v in payload.items() if v is not None}
|
||||
body, http_resp = self._make_request('post', '/accounts:update', json=payload)
|
||||
if not body or not body.get('localId'):
|
||||
raise _auth_utils.UnexpectedResponseError(
|
||||
'Failed to update user: {0}.'.format(uid), http_response=http_resp)
|
||||
return body.get('localId')
|
||||
|
||||
def delete_user(self, uid):
|
||||
"""Deletes the user identified by the specified user ID."""
|
||||
_auth_utils.validate_uid(uid, required=True)
|
||||
body, http_resp = self._make_request('post', '/accounts:delete', json={'localId' : uid})
|
||||
if not body or not body.get('kind'):
|
||||
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) > 1000:
|
||||
raise ValueError("`uids` paramter must have <= 1000 entries.")
|
||||
for uid in uids:
|
||||
_auth_utils.validate_uid(uid, required=True)
|
||||
|
||||
body, http_resp = self._make_request('post', '/accounts:batchDelete',
|
||||
json={'localIds': uids, 'force': force_delete})
|
||||
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:
|
||||
if not users or len(users) > MAX_IMPORT_USERS_SIZE:
|
||||
raise ValueError(
|
||||
'Users must be a non-empty list with no more than {0} elements.'.format(
|
||||
MAX_IMPORT_USERS_SIZE))
|
||||
if any([not isinstance(u, _user_import.ImportUserRecord) for u in users]):
|
||||
raise ValueError('One or more user objects are invalid.')
|
||||
except TypeError:
|
||||
raise ValueError('users must be iterable')
|
||||
|
||||
payload = {'users': [u.to_dict() for u in users]}
|
||||
if any(['passwordHash' in u for u in payload['users']]):
|
||||
if not isinstance(hash_alg, _user_import.UserImportHash):
|
||||
raise ValueError('A UserImportHash is required to import users with passwords.')
|
||||
payload.update(hash_alg.to_dict())
|
||||
body, http_resp = self._make_request('post', '/accounts:batchCreate', json=payload)
|
||||
if not isinstance(body, dict):
|
||||
raise _auth_utils.UnexpectedResponseError(
|
||||
'Failed to import users.', http_response=http_resp)
|
||||
return body
|
||||
|
||||
def generate_email_action_link(self, action_type, email, action_code_settings=None):
|
||||
"""Fetches the email action links for types
|
||||
|
||||
Args:
|
||||
action_type: String. Valid values ['VERIFY_EMAIL', 'EMAIL_SIGNIN', 'PASSWORD_RESET']
|
||||
email: Email of the user for which the action is performed
|
||||
action_code_settings: ``ActionCodeSettings`` object or dict (optional). Defines whether
|
||||
the link is to be handled by a mobile app and the additional state information to be
|
||||
passed in the deep link, etc.
|
||||
Returns:
|
||||
link_url: action url to be emailed to the user
|
||||
|
||||
Raises:
|
||||
UnexpectedResponseError: If the backend server responds with an unexpected message
|
||||
FirebaseError: If an error occurs while generating the link
|
||||
ValueError: If the provided arguments are invalid
|
||||
"""
|
||||
payload = {
|
||||
'requestType': _auth_utils.validate_action_type(action_type),
|
||||
'email': _auth_utils.validate_email(email),
|
||||
'returnOobLink': True
|
||||
}
|
||||
|
||||
if action_code_settings:
|
||||
payload.update(encode_action_code_settings(action_code_settings))
|
||||
|
||||
body, http_resp = self._make_request('post', '/accounts:sendOobCode', json=payload)
|
||||
if not body or not body.get('oobLink'):
|
||||
raise _auth_utils.UnexpectedResponseError(
|
||||
'Failed to generate email action link.', http_response=http_resp)
|
||||
return body.get('oobLink')
|
||||
|
||||
def _make_request(self, method, path, **kwargs):
|
||||
url = '{0}{1}'.format(self.base_url, path)
|
||||
try:
|
||||
return self.http_client.body_and_response(method, url, **kwargs)
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise _auth_utils.handle_auth_backend_error(error)
|
||||
|
||||
|
||||
class _UserIterator(_auth_utils.PageIterator):
|
||||
|
||||
@property
|
||||
def items(self):
|
||||
return self._current_page.users
|
Loading…
Add table
Add a link
Reference in a new issue