Added delete option to database storage.
This commit is contained in:
parent
308604a33c
commit
963b5bc68b
1868 changed files with 192402 additions and 13278 deletions
21
venv/Lib/site-packages/firebase_admin/__about__.py
Normal file
21
venv/Lib/site-packages/firebase_admin/__about__.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# 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.
|
||||
|
||||
"""About information (version, etc) for Firebase Admin SDK."""
|
||||
|
||||
__version__ = '4.4.0'
|
||||
__title__ = 'firebase_admin'
|
||||
__author__ = 'Firebase'
|
||||
__license__ = 'Apache License 2.0'
|
||||
__url__ = 'https://firebase.google.com/docs/admin/setup/'
|
309
venv/Lib/site-packages/firebase_admin/__init__.py
Normal file
309
venv/Lib/site-packages/firebase_admin/__init__.py
Normal file
|
@ -0,0 +1,309 @@
|
|||
# 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 Admin SDK for Python."""
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
|
||||
from firebase_admin import credentials
|
||||
from firebase_admin.__about__ import __version__
|
||||
|
||||
|
||||
_apps = {}
|
||||
_apps_lock = threading.RLock()
|
||||
_clock = datetime.datetime.utcnow
|
||||
|
||||
_DEFAULT_APP_NAME = '[DEFAULT]'
|
||||
_FIREBASE_CONFIG_ENV_VAR = 'FIREBASE_CONFIG'
|
||||
_CONFIG_VALID_KEYS = ['databaseAuthVariableOverride', 'databaseURL', 'httpTimeout', 'projectId',
|
||||
'storageBucket']
|
||||
|
||||
def initialize_app(credential=None, options=None, name=_DEFAULT_APP_NAME):
|
||||
"""Initializes and returns a new App instance.
|
||||
|
||||
Creates a new App instance using the specified options
|
||||
and the app name. If an instance already exists by the same
|
||||
app name a ValueError is raised.
|
||||
If options are not provided an attempt is made to load the options from the environment.
|
||||
This is done by looking up the ``FIREBASE_CONFIG`` environment variable. If the value of
|
||||
the variable starts with ``"{"``, it is parsed as a JSON object. Otherwise it is treated
|
||||
as a file name and the JSON content is read from the corresponding file.
|
||||
Use this function whenever a new App instance is required. Do not directly invoke the
|
||||
App constructor.
|
||||
|
||||
Args:
|
||||
credential: A credential object used to initialize the SDK (optional). If none is provided,
|
||||
Google Application Default Credentials are used.
|
||||
options: A dictionary of configuration options (optional). Supported options include
|
||||
``databaseURL``, ``storageBucket``, ``projectId``, ``databaseAuthVariableOverride``,
|
||||
``serviceAccountId`` and ``httpTimeout``. If ``httpTimeout`` is not set, the SDK
|
||||
uses a default timeout of 120 seconds.
|
||||
name: Name of the app (optional).
|
||||
Returns:
|
||||
App: A newly initialized instance of App.
|
||||
|
||||
Raises:
|
||||
ValueError: If the app name is already in use, or any of the
|
||||
provided arguments are invalid.
|
||||
"""
|
||||
if credential is None:
|
||||
credential = credentials.ApplicationDefault()
|
||||
app = App(name, credential, options)
|
||||
with _apps_lock:
|
||||
if app.name not in _apps:
|
||||
_apps[app.name] = app
|
||||
return app
|
||||
|
||||
if name == _DEFAULT_APP_NAME:
|
||||
raise ValueError((
|
||||
'The default Firebase app already exists. This means you called '
|
||||
'initialize_app() more than once without providing an app name as '
|
||||
'the second argument. In most cases you only need to call '
|
||||
'initialize_app() once. But if you do want to initialize multiple '
|
||||
'apps, pass a second argument to initialize_app() to give each app '
|
||||
'a unique name.'))
|
||||
|
||||
raise ValueError((
|
||||
'Firebase app named "{0}" already exists. This means you called '
|
||||
'initialize_app() more than once with the same app name as the '
|
||||
'second argument. Make sure you provide a unique name every time '
|
||||
'you call initialize_app().').format(name))
|
||||
|
||||
|
||||
def delete_app(app):
|
||||
"""Gracefully deletes an App instance.
|
||||
|
||||
Args:
|
||||
app: The app instance to be deleted.
|
||||
|
||||
Raises:
|
||||
ValueError: If the app is not initialized.
|
||||
"""
|
||||
if not isinstance(app, App):
|
||||
raise ValueError('Illegal app argument type: "{}". Argument must be of '
|
||||
'type App.'.format(type(app)))
|
||||
with _apps_lock:
|
||||
if _apps.get(app.name) is app:
|
||||
del _apps[app.name]
|
||||
app._cleanup() # pylint: disable=protected-access
|
||||
return
|
||||
if app.name == _DEFAULT_APP_NAME:
|
||||
raise ValueError(
|
||||
'The default Firebase app is not initialized. Make sure to initialize '
|
||||
'the default app by calling initialize_app().')
|
||||
|
||||
raise ValueError(
|
||||
('Firebase app named "{0}" is not initialized. Make sure to initialize '
|
||||
'the app by calling initialize_app() with your app name as the '
|
||||
'second argument.').format(app.name))
|
||||
|
||||
|
||||
def get_app(name=_DEFAULT_APP_NAME):
|
||||
"""Retrieves an App instance by name.
|
||||
|
||||
Args:
|
||||
name: Name of the App instance to retrieve (optional).
|
||||
|
||||
Returns:
|
||||
App: An App instance with the given name.
|
||||
|
||||
Raises:
|
||||
ValueError: If the specified name is not a string, or if the specified
|
||||
app does not exist.
|
||||
"""
|
||||
if not isinstance(name, str):
|
||||
raise ValueError('Illegal app name argument type: "{}". App name '
|
||||
'must be a string.'.format(type(name)))
|
||||
with _apps_lock:
|
||||
if name in _apps:
|
||||
return _apps[name]
|
||||
|
||||
if name == _DEFAULT_APP_NAME:
|
||||
raise ValueError(
|
||||
'The default Firebase app does not exist. Make sure to initialize '
|
||||
'the SDK by calling initialize_app().')
|
||||
|
||||
raise ValueError(
|
||||
('Firebase app named "{0}" does not exist. Make sure to initialize '
|
||||
'the SDK by calling initialize_app() with your app name as the '
|
||||
'second argument.').format(name))
|
||||
|
||||
|
||||
class _AppOptions:
|
||||
"""A collection of configuration options for an App."""
|
||||
|
||||
def __init__(self, options):
|
||||
if options is None:
|
||||
options = self._load_from_environment()
|
||||
|
||||
if not isinstance(options, dict):
|
||||
raise ValueError('Illegal Firebase app options type: {0}. Options '
|
||||
'must be a dictionary.'.format(type(options)))
|
||||
self._options = options
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""Returns the option identified by the provided key."""
|
||||
return self._options.get(key, default)
|
||||
|
||||
def _load_from_environment(self):
|
||||
"""Invoked when no options are passed to __init__, loads options from FIREBASE_CONFIG.
|
||||
|
||||
If the value of the FIREBASE_CONFIG environment variable starts with "{" an attempt is made
|
||||
to parse it as a JSON object, otherwise it is assumed to be pointing to a JSON file.
|
||||
"""
|
||||
|
||||
config_file = os.getenv(_FIREBASE_CONFIG_ENV_VAR)
|
||||
if not config_file:
|
||||
return {}
|
||||
if config_file.startswith('{'):
|
||||
json_str = config_file
|
||||
else:
|
||||
try:
|
||||
with open(config_file, 'r') as json_file:
|
||||
json_str = json_file.read()
|
||||
except Exception as err:
|
||||
raise ValueError('Unable to read file {}. {}'.format(config_file, err))
|
||||
try:
|
||||
json_data = json.loads(json_str)
|
||||
except Exception as err:
|
||||
raise ValueError('JSON string "{0}" is not valid json. {1}'.format(json_str, err))
|
||||
return {k: v for k, v in json_data.items() if k in _CONFIG_VALID_KEYS}
|
||||
|
||||
|
||||
class App:
|
||||
"""The entry point for Firebase Python SDK.
|
||||
|
||||
Represents a Firebase app, while holding the configuration and state
|
||||
common to all Firebase APIs.
|
||||
"""
|
||||
|
||||
def __init__(self, name, credential, options):
|
||||
"""Constructs a new App using the provided name and options.
|
||||
|
||||
Args:
|
||||
name: Name of the application.
|
||||
credential: A credential object.
|
||||
options: A dictionary of configuration options.
|
||||
|
||||
Raises:
|
||||
ValueError: If an argument is None or invalid.
|
||||
"""
|
||||
if not name or not isinstance(name, str):
|
||||
raise ValueError('Illegal Firebase app name "{0}" provided. App name must be a '
|
||||
'non-empty string.'.format(name))
|
||||
self._name = name
|
||||
|
||||
if not isinstance(credential, credentials.Base):
|
||||
raise ValueError('Illegal Firebase credential provided. App must be initialized '
|
||||
'with a valid credential instance.')
|
||||
self._credential = credential
|
||||
self._options = _AppOptions(options)
|
||||
self._lock = threading.RLock()
|
||||
self._services = {}
|
||||
|
||||
App._validate_project_id(self._options.get('projectId'))
|
||||
self._project_id_initialized = False
|
||||
|
||||
@classmethod
|
||||
def _validate_project_id(cls, project_id):
|
||||
if project_id is not None and not isinstance(project_id, str):
|
||||
raise ValueError(
|
||||
'Invalid project ID: "{0}". project ID must be a string.'.format(project_id))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def credential(self):
|
||||
return self._credential
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
return self._options
|
||||
|
||||
@property
|
||||
def project_id(self):
|
||||
if not self._project_id_initialized:
|
||||
self._project_id = self._lookup_project_id()
|
||||
self._project_id_initialized = True
|
||||
return self._project_id
|
||||
|
||||
def _lookup_project_id(self):
|
||||
"""Looks up the Firebase project ID associated with an App.
|
||||
|
||||
If a ``projectId`` is specified in app options, it is returned. Then tries to
|
||||
get the project ID from the credential used to initialize the app. If that also fails,
|
||||
attempts to look up the ``GOOGLE_CLOUD_PROJECT`` and ``GCLOUD_PROJECT`` environment
|
||||
variables.
|
||||
|
||||
Returns:
|
||||
str: A project ID string or None.
|
||||
"""
|
||||
project_id = self._options.get('projectId')
|
||||
if not project_id:
|
||||
try:
|
||||
project_id = self._credential.project_id
|
||||
except AttributeError:
|
||||
pass
|
||||
if not project_id:
|
||||
project_id = os.environ.get('GOOGLE_CLOUD_PROJECT',
|
||||
os.environ.get('GCLOUD_PROJECT'))
|
||||
App._validate_project_id(self._options.get('projectId'))
|
||||
return project_id
|
||||
|
||||
def _get_service(self, name, initializer):
|
||||
"""Returns the service instance identified by the given name.
|
||||
|
||||
Services are functional entities exposed by the Admin SDK (e.g. auth, database). Each
|
||||
service instance is associated with exactly one App. If the named service
|
||||
instance does not exist yet, _get_service() calls the provided initializer function to
|
||||
create the service instance. The created instance will be cached, so that subsequent
|
||||
calls would always fetch it from the cache.
|
||||
|
||||
Args:
|
||||
name: Name of the service to retrieve.
|
||||
initializer: A function that can be used to initialize a service for the first time.
|
||||
|
||||
Returns:
|
||||
object: The specified service instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the provided name is invalid, or if the App is already deleted.
|
||||
"""
|
||||
if not name or not isinstance(name, str):
|
||||
raise ValueError(
|
||||
'Illegal name argument: "{0}". Name must be a non-empty string.'.format(name))
|
||||
with self._lock:
|
||||
if self._services is None:
|
||||
raise ValueError(
|
||||
'Service requested from deleted Firebase App: "{0}".'.format(self._name))
|
||||
if name not in self._services:
|
||||
self._services[name] = initializer(self)
|
||||
return self._services[name]
|
||||
|
||||
def _cleanup(self):
|
||||
"""Cleans up any services associated with this App.
|
||||
|
||||
Checks whether each service contains a close() method, and calls it if available.
|
||||
This is to be called when an App is being deleted, thus ensuring graceful termination of
|
||||
any services started by the App.
|
||||
"""
|
||||
with self._lock:
|
||||
for service in self._services.values():
|
||||
if hasattr(service, 'close') and hasattr(service.close, '__call__'):
|
||||
service.close()
|
||||
self._services = None
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
703
venv/Lib/site-packages/firebase_admin/_auth_client.py
Normal file
703
venv/Lib/site-packages/firebase_admin/_auth_client.py
Normal file
|
@ -0,0 +1,703 @@
|
|||
# Copyright 2020 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 auth client sub module."""
|
||||
|
||||
import time
|
||||
|
||||
import firebase_admin
|
||||
from firebase_admin import _auth_providers
|
||||
from firebase_admin import _auth_utils
|
||||
from firebase_admin import _http_client
|
||||
from firebase_admin import _token_gen
|
||||
from firebase_admin import _user_identifier
|
||||
from firebase_admin import _user_import
|
||||
from firebase_admin import _user_mgt
|
||||
|
||||
|
||||
class Client:
|
||||
"""Firebase Authentication client scoped to a specific tenant."""
|
||||
|
||||
def __init__(self, app, tenant_id=None):
|
||||
if not app.project_id:
|
||||
raise ValueError("""A project ID is required to access the auth service.
|
||||
1. Use a service account credential, or
|
||||
2. set the project ID explicitly via Firebase App options, or
|
||||
3. set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.""")
|
||||
|
||||
credential = app.credential.get_credential()
|
||||
version_header = 'Python/Admin/{0}'.format(firebase_admin.__version__)
|
||||
http_client = _http_client.JsonHttpClient(
|
||||
credential=credential, headers={'X-Client-Version': version_header})
|
||||
|
||||
self._tenant_id = tenant_id
|
||||
self._token_generator = _token_gen.TokenGenerator(app, http_client)
|
||||
self._token_verifier = _token_gen.TokenVerifier(app)
|
||||
self._user_manager = _user_mgt.UserManager(http_client, app.project_id, tenant_id)
|
||||
self._provider_manager = _auth_providers.ProviderConfigClient(
|
||||
http_client, app.project_id, tenant_id)
|
||||
|
||||
@property
|
||||
def tenant_id(self):
|
||||
"""Tenant ID associated with this client."""
|
||||
return self._tenant_id
|
||||
|
||||
def create_custom_token(self, uid, developer_claims=None):
|
||||
"""Builds and signs a Firebase custom auth token.
|
||||
|
||||
Args:
|
||||
uid: ID of the user for whom the token is created.
|
||||
developer_claims: A dictionary of claims to be included in the token
|
||||
(optional).
|
||||
|
||||
Returns:
|
||||
bytes: A token minted from the input parameters.
|
||||
|
||||
Raises:
|
||||
ValueError: If input parameters are invalid.
|
||||
TokenSignError: If an error occurs while signing the token using the remote IAM service.
|
||||
"""
|
||||
return self._token_generator.create_custom_token(
|
||||
uid, developer_claims, tenant_id=self.tenant_id)
|
||||
|
||||
def verify_id_token(self, id_token, check_revoked=False):
|
||||
"""Verifies the signature and data for the provided JWT.
|
||||
|
||||
Accepts a signed token string, verifies that it is current, was issued
|
||||
to this project, and that it was correctly signed by Google.
|
||||
|
||||
Args:
|
||||
id_token: A string of the encoded JWT.
|
||||
check_revoked: Boolean, If true, checks whether the token has been revoked (optional).
|
||||
|
||||
Returns:
|
||||
dict: A dictionary of key-value pairs parsed from the decoded JWT.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``id_token`` is a not a string or is empty.
|
||||
InvalidIdTokenError: If ``id_token`` is not a valid Firebase ID token.
|
||||
ExpiredIdTokenError: If the specified ID token has expired.
|
||||
RevokedIdTokenError: If ``check_revoked`` is ``True`` and the ID token has been
|
||||
revoked.
|
||||
TenantIdMismatchError: If ``id_token`` belongs to a tenant that is different than
|
||||
this ``Client`` instance.
|
||||
CertificateFetchError: If an error occurs while fetching the public key certificates
|
||||
required to verify the ID token.
|
||||
"""
|
||||
if not isinstance(check_revoked, bool):
|
||||
# guard against accidental wrong assignment.
|
||||
raise ValueError('Illegal check_revoked argument. Argument must be of type '
|
||||
' bool, but given "{0}".'.format(type(check_revoked)))
|
||||
|
||||
verified_claims = self._token_verifier.verify_id_token(id_token)
|
||||
if self.tenant_id:
|
||||
token_tenant_id = verified_claims.get('firebase', {}).get('tenant')
|
||||
if self.tenant_id != token_tenant_id:
|
||||
raise _auth_utils.TenantIdMismatchError(
|
||||
'Invalid tenant ID: {0}'.format(token_tenant_id))
|
||||
|
||||
if check_revoked:
|
||||
self._check_jwt_revoked(verified_claims, _token_gen.RevokedIdTokenError, 'ID token')
|
||||
return verified_claims
|
||||
|
||||
def revoke_refresh_tokens(self, uid):
|
||||
"""Revokes all refresh tokens for an existing user.
|
||||
|
||||
This method updates the user's ``tokens_valid_after_timestamp`` to the current UTC
|
||||
in seconds since the epoch. It is important that the server on which this is called has its
|
||||
clock set correctly and synchronized.
|
||||
|
||||
While this revokes all sessions for a specified user and disables any new ID tokens for
|
||||
existing sessions from getting minted, existing ID tokens may remain active until their
|
||||
natural expiration (one hour). To verify that ID tokens are revoked, use
|
||||
``verify_id_token(idToken, check_revoked=True)``.
|
||||
|
||||
Args:
|
||||
uid: A user ID string.
|
||||
|
||||
Raises:
|
||||
ValueError: If the user ID is None, empty or malformed.
|
||||
FirebaseError: If an error occurs while revoking the refresh token.
|
||||
"""
|
||||
self._user_manager.update_user(uid, valid_since=int(time.time()))
|
||||
|
||||
def get_user(self, uid):
|
||||
"""Gets the user data corresponding to the specified user ID.
|
||||
|
||||
Args:
|
||||
uid: A user ID string.
|
||||
|
||||
Returns:
|
||||
UserRecord: A user record instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the user ID is None, empty or malformed.
|
||||
UserNotFoundError: If the specified user ID does not exist.
|
||||
FirebaseError: If an error occurs while retrieving the user.
|
||||
"""
|
||||
response = self._user_manager.get_user(uid=uid)
|
||||
return _user_mgt.UserRecord(response)
|
||||
|
||||
def get_user_by_email(self, email):
|
||||
"""Gets the user data corresponding to the specified user email.
|
||||
|
||||
Args:
|
||||
email: A user email address string.
|
||||
|
||||
Returns:
|
||||
UserRecord: A user record instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the email is None, empty or malformed.
|
||||
UserNotFoundError: If no user exists by the specified email address.
|
||||
FirebaseError: If an error occurs while retrieving the user.
|
||||
"""
|
||||
response = self._user_manager.get_user(email=email)
|
||||
return _user_mgt.UserRecord(response)
|
||||
|
||||
def get_user_by_phone_number(self, phone_number):
|
||||
"""Gets the user data corresponding to the specified phone number.
|
||||
|
||||
Args:
|
||||
phone_number: A phone number string.
|
||||
|
||||
Returns:
|
||||
UserRecord: A user record instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the phone number is ``None``, empty or malformed.
|
||||
UserNotFoundError: If no user exists by the specified phone number.
|
||||
FirebaseError: If an error occurs while retrieving the user.
|
||||
"""
|
||||
response = self._user_manager.get_user(phone_number=phone_number)
|
||||
return _user_mgt.UserRecord(response)
|
||||
|
||||
def get_users(self, identifiers):
|
||||
"""Gets the user data corresponding to the specified identifiers.
|
||||
|
||||
There are no ordering guarantees; in particular, the nth entry in the
|
||||
result list is not guaranteed to correspond to the nth entry in the input
|
||||
parameters list.
|
||||
|
||||
A maximum of 100 identifiers may be supplied. If more than 100
|
||||
identifiers are supplied, this method raises a `ValueError`.
|
||||
|
||||
Args:
|
||||
identifiers (list[Identifier]): A list of ``Identifier`` instances used
|
||||
to indicate which user records should be returned. Must have <= 100
|
||||
entries.
|
||||
|
||||
Returns:
|
||||
GetUsersResult: A ``GetUsersResult`` instance corresponding to the
|
||||
specified identifiers.
|
||||
|
||||
Raises:
|
||||
ValueError: If any of the identifiers are invalid or if more than 100
|
||||
identifiers are specified.
|
||||
"""
|
||||
response = self._user_manager.get_users(identifiers=identifiers)
|
||||
|
||||
def _matches(identifier, user_record):
|
||||
if isinstance(identifier, _user_identifier.UidIdentifier):
|
||||
return identifier.uid == user_record.uid
|
||||
if isinstance(identifier, _user_identifier.EmailIdentifier):
|
||||
return identifier.email == user_record.email
|
||||
if isinstance(identifier, _user_identifier.PhoneIdentifier):
|
||||
return identifier.phone_number == user_record.phone_number
|
||||
if isinstance(identifier, _user_identifier.ProviderIdentifier):
|
||||
return next((
|
||||
True
|
||||
for user_info in user_record.provider_data
|
||||
if identifier.provider_id == user_info.provider_id
|
||||
and identifier.provider_uid == user_info.uid
|
||||
), False)
|
||||
raise TypeError("Unexpected type: {}".format(type(identifier)))
|
||||
|
||||
def _is_user_found(identifier, user_records):
|
||||
return any(_matches(identifier, user_record) for user_record in user_records)
|
||||
|
||||
users = [_user_mgt.UserRecord(user) for user in response]
|
||||
not_found = [
|
||||
identifier for identifier in identifiers if not _is_user_found(identifier, users)]
|
||||
|
||||
return _user_mgt.GetUsersResult(users=users, not_found=not_found)
|
||||
|
||||
def list_users(self, page_token=None, max_results=_user_mgt.MAX_LIST_USERS_RESULTS):
|
||||
"""Retrieves a page of user accounts from a Firebase project.
|
||||
|
||||
The ``page_token`` argument governs the starting point of the page. The ``max_results``
|
||||
argument governs the maximum number of user accounts that may be included in the returned
|
||||
page. This function never returns ``None``. If there are no user accounts in the Firebase
|
||||
project, this returns an empty page.
|
||||
|
||||
Args:
|
||||
page_token: A non-empty page token string, which indicates the starting point of the
|
||||
page (optional). Defaults to ``None``, which will retrieve the first page of users.
|
||||
max_results: A positive integer indicating the maximum number of users to include in
|
||||
the returned page (optional). Defaults to 1000, which is also the maximum number
|
||||
allowed.
|
||||
|
||||
Returns:
|
||||
ListUsersPage: A page of user accounts.
|
||||
|
||||
Raises:
|
||||
ValueError: If max_results or page_token are invalid.
|
||||
FirebaseError: If an error occurs while retrieving the user accounts.
|
||||
"""
|
||||
def download(page_token, max_results):
|
||||
return self._user_manager.list_users(page_token, max_results)
|
||||
return _user_mgt.ListUsersPage(download, page_token, max_results)
|
||||
|
||||
def create_user(self, **kwargs): # pylint: disable=differing-param-doc
|
||||
"""Creates a new user account with the specified properties.
|
||||
|
||||
Args:
|
||||
kwargs: A series of keyword arguments (optional).
|
||||
|
||||
Keyword Args:
|
||||
uid: User ID to assign to the newly created user (optional).
|
||||
display_name: The user's display name (optional).
|
||||
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).
|
||||
photo_url: The user's photo URL (optional).
|
||||
password: The user's raw, unhashed password. (optional).
|
||||
disabled: A boolean indicating whether or not the user account is disabled (optional).
|
||||
|
||||
Returns:
|
||||
UserRecord: A UserRecord instance for the newly created user.
|
||||
|
||||
Raises:
|
||||
ValueError: If the specified user properties are invalid.
|
||||
FirebaseError: If an error occurs while creating the user account.
|
||||
"""
|
||||
uid = self._user_manager.create_user(**kwargs)
|
||||
return self.get_user(uid=uid)
|
||||
|
||||
def update_user(self, uid, **kwargs): # pylint: disable=differing-param-doc
|
||||
"""Updates an existing user account with the specified properties.
|
||||
|
||||
Args:
|
||||
uid: A user ID string.
|
||||
kwargs: A series of keyword arguments (optional).
|
||||
|
||||
Keyword Args:
|
||||
display_name: The user's display name (optional). Can be removed by explicitly passing
|
||||
``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 ``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). To remove all custom claims, pass
|
||||
``auth.DELETE_ATTRIBUTE``.
|
||||
valid_since: An integer signifying the seconds since the epoch (optional). This field
|
||||
is set by ``revoke_refresh_tokens`` and it is discouraged to set this field
|
||||
directly.
|
||||
|
||||
Returns:
|
||||
UserRecord: An updated UserRecord instance for the user.
|
||||
|
||||
Raises:
|
||||
ValueError: If the specified user ID or properties are invalid.
|
||||
FirebaseError: If an error occurs while updating the user account.
|
||||
"""
|
||||
self._user_manager.update_user(uid, **kwargs)
|
||||
return self.get_user(uid=uid)
|
||||
|
||||
def set_custom_user_claims(self, uid, custom_claims):
|
||||
"""Sets additional claims on an existing user account.
|
||||
|
||||
Custom claims set via this function can be used to define user roles and privilege levels.
|
||||
These claims propagate to all the devices where the user is already signed in (after token
|
||||
expiration or when token refresh is forced), and next time the user signs in. The claims
|
||||
can be accessed via the user's ID token JWT. If a reserved OIDC claim is specified (sub,
|
||||
iat, iss, etc), an error is thrown. Claims payload must also not be larger then 1000
|
||||
characters when serialized into a JSON string.
|
||||
|
||||
Args:
|
||||
uid: A user ID string.
|
||||
custom_claims: A dictionary or a JSON string of custom claims. Pass None to unset any
|
||||
claims set previously.
|
||||
|
||||
Raises:
|
||||
ValueError: If the specified user ID or the custom claims are invalid.
|
||||
FirebaseError: If an error occurs while updating the user account.
|
||||
"""
|
||||
if custom_claims is None:
|
||||
custom_claims = _user_mgt.DELETE_ATTRIBUTE
|
||||
self._user_manager.update_user(uid, custom_claims=custom_claims)
|
||||
|
||||
def delete_user(self, uid):
|
||||
"""Deletes the user identified by the specified user ID.
|
||||
|
||||
Args:
|
||||
uid: A user ID string.
|
||||
|
||||
Raises:
|
||||
ValueError: If the user ID is None, empty or malformed.
|
||||
FirebaseError: If an error occurs while deleting the user account.
|
||||
"""
|
||||
self._user_manager.delete_user(uid)
|
||||
|
||||
def delete_users(self, uids):
|
||||
"""Deletes the users specified by the given identifiers.
|
||||
|
||||
Deleting a non-existing user does not generate an error (the method is
|
||||
idempotent.) Non-existing users are considered to be successfully
|
||||
deleted and are therefore included in the
|
||||
`DeleteUserResult.success_count` value.
|
||||
|
||||
A maximum of 1000 identifiers may be supplied. If more than 1000
|
||||
identifiers are supplied, this method raises a `ValueError`.
|
||||
|
||||
Args:
|
||||
uids: A list of strings indicating the uids of the users to be deleted.
|
||||
Must have <= 1000 entries.
|
||||
|
||||
Returns:
|
||||
DeleteUsersResult: The total number of successful/failed deletions, as
|
||||
well as the array of errors that correspond to the failed
|
||||
deletions.
|
||||
|
||||
Raises:
|
||||
ValueError: If any of the identifiers are invalid or if more than 1000
|
||||
identifiers are specified.
|
||||
"""
|
||||
result = self._user_manager.delete_users(uids, force_delete=True)
|
||||
return _user_mgt.DeleteUsersResult(result, len(uids))
|
||||
|
||||
def import_users(self, users, hash_alg=None):
|
||||
"""Imports the specified list of users into Firebase Auth.
|
||||
|
||||
At most 1000 users can be imported at a time. This operation is optimized for bulk imports
|
||||
and ignores checks on identifier uniqueness, which could result in duplications. The
|
||||
``hash_alg`` parameter must be specified when importing users with passwords. Refer to the
|
||||
``UserImportHash`` class for supported hash algorithms.
|
||||
|
||||
Args:
|
||||
users: A list of ``ImportUserRecord`` instances to import. Length of the list must not
|
||||
exceed 1000.
|
||||
hash_alg: A ``UserImportHash`` object (optional). Required when importing users with
|
||||
passwords.
|
||||
|
||||
Returns:
|
||||
UserImportResult: An object summarizing the result of the import operation.
|
||||
|
||||
Raises:
|
||||
ValueError: If the provided arguments are invalid.
|
||||
FirebaseError: If an error occurs while importing users.
|
||||
"""
|
||||
result = self._user_manager.import_users(users, hash_alg)
|
||||
return _user_import.UserImportResult(result, len(users))
|
||||
|
||||
def generate_password_reset_link(self, email, action_code_settings=None):
|
||||
"""Generates the out-of-band email action link for password reset flows for the specified
|
||||
email address.
|
||||
|
||||
Args:
|
||||
email: The email of the user whose password is to be reset.
|
||||
action_code_settings: ``ActionCodeSettings`` instance (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.
|
||||
|
||||
Returns:
|
||||
link: The password reset link created by the API
|
||||
|
||||
Raises:
|
||||
ValueError: If the provided arguments are invalid
|
||||
FirebaseError: If an error occurs while generating the link
|
||||
"""
|
||||
return self._user_manager.generate_email_action_link(
|
||||
'PASSWORD_RESET', email, action_code_settings=action_code_settings)
|
||||
|
||||
def generate_email_verification_link(self, email, action_code_settings=None):
|
||||
"""Generates the out-of-band email action link for email verification flows for the
|
||||
specified email address.
|
||||
|
||||
Args:
|
||||
email: The email of the user to be verified.
|
||||
action_code_settings: ``ActionCodeSettings`` instance (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.
|
||||
|
||||
Returns:
|
||||
link: The email verification link created by the API
|
||||
|
||||
Raises:
|
||||
ValueError: If the provided arguments are invalid
|
||||
FirebaseError: If an error occurs while generating the link
|
||||
"""
|
||||
return self._user_manager.generate_email_action_link(
|
||||
'VERIFY_EMAIL', email, action_code_settings=action_code_settings)
|
||||
|
||||
def generate_sign_in_with_email_link(self, email, action_code_settings):
|
||||
"""Generates the out-of-band email action link for email link sign-in flows, using the
|
||||
action code settings provided.
|
||||
|
||||
Args:
|
||||
email: The email of the user signing in.
|
||||
action_code_settings: ``ActionCodeSettings`` instance. Defines whether
|
||||
the link is to be handled by a mobile app and the additional state information to be
|
||||
passed in the deep link.
|
||||
|
||||
Returns:
|
||||
link: The email sign-in link created by the API
|
||||
|
||||
Raises:
|
||||
ValueError: If the provided arguments are invalid
|
||||
FirebaseError: If an error occurs while generating the link
|
||||
"""
|
||||
return self._user_manager.generate_email_action_link(
|
||||
'EMAIL_SIGNIN', email, action_code_settings=action_code_settings)
|
||||
|
||||
def get_oidc_provider_config(self, provider_id):
|
||||
"""Returns the ``OIDCProviderConfig`` with the given ID.
|
||||
|
||||
Args:
|
||||
provider_id: Provider ID string.
|
||||
|
||||
Returns:
|
||||
SAMLProviderConfig: An OIDC provider config instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the provider ID is invalid, empty or does not have ``oidc.`` prefix.
|
||||
ConfigurationNotFoundError: If no OIDC provider is available with the given identifier.
|
||||
FirebaseError: If an error occurs while retrieving the OIDC provider.
|
||||
"""
|
||||
return self._provider_manager.get_oidc_provider_config(provider_id)
|
||||
|
||||
def create_oidc_provider_config(
|
||||
self, provider_id, client_id, issuer, display_name=None, enabled=None):
|
||||
"""Creates a new OIDC provider config from the given parameters.
|
||||
|
||||
OIDC provider support requires Google Cloud's Identity Platform (GCIP). To learn more about
|
||||
GCIP, including pricing and features, see https://cloud.google.com/identity-platform.
|
||||
|
||||
Args:
|
||||
provider_id: Provider ID string. Must have the prefix ``oidc.``.
|
||||
client_id: Client ID of the new config.
|
||||
issuer: Issuer of the new config. Must be a valid URL.
|
||||
display_name: The user-friendly display name to the current configuration (optional).
|
||||
This name is also used as the provider label in the Cloud Console.
|
||||
enabled: A boolean indicating whether the provider configuration is enabled or disabled
|
||||
(optional). A user cannot sign in using a disabled provider.
|
||||
|
||||
Returns:
|
||||
OIDCProviderConfig: The newly created OIDC provider config instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If any of the specified input parameters are invalid.
|
||||
FirebaseError: If an error occurs while creating the new OIDC provider config.
|
||||
"""
|
||||
return self._provider_manager.create_oidc_provider_config(
|
||||
provider_id, client_id=client_id, issuer=issuer, display_name=display_name,
|
||||
enabled=enabled)
|
||||
|
||||
def update_oidc_provider_config(
|
||||
self, provider_id, client_id=None, issuer=None, display_name=None, enabled=None):
|
||||
"""Updates an existing OIDC provider config with the given parameters.
|
||||
|
||||
Args:
|
||||
provider_id: Provider ID string. Must have the prefix ``oidc.``.
|
||||
client_id: Client ID of the new config (optional).
|
||||
issuer: Issuer of the new config (optional). Must be a valid URL.
|
||||
display_name: The user-friendly display name to the current configuration (optional).
|
||||
Pass ``auth.DELETE_ATTRIBUTE`` to delete the current display name.
|
||||
enabled: A boolean indicating whether the provider configuration is enabled or disabled
|
||||
(optional).
|
||||
|
||||
Returns:
|
||||
OIDCProviderConfig: The updated OIDC provider config instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If any of the specified input parameters are invalid.
|
||||
FirebaseError: If an error occurs while updating the OIDC provider config.
|
||||
"""
|
||||
return self._provider_manager.update_oidc_provider_config(
|
||||
provider_id, client_id=client_id, issuer=issuer, display_name=display_name,
|
||||
enabled=enabled)
|
||||
|
||||
def delete_oidc_provider_config(self, provider_id):
|
||||
"""Deletes the ``OIDCProviderConfig`` with the given ID.
|
||||
|
||||
Args:
|
||||
provider_id: Provider ID string.
|
||||
|
||||
Raises:
|
||||
ValueError: If the provider ID is invalid, empty or does not have ``oidc.`` prefix.
|
||||
ConfigurationNotFoundError: If no OIDC provider is available with the given identifier.
|
||||
FirebaseError: If an error occurs while deleting the OIDC provider.
|
||||
"""
|
||||
self._provider_manager.delete_oidc_provider_config(provider_id)
|
||||
|
||||
def list_oidc_provider_configs(
|
||||
self, page_token=None, max_results=_auth_providers.MAX_LIST_CONFIGS_RESULTS):
|
||||
"""Retrieves a page of OIDC provider configs from a Firebase project.
|
||||
|
||||
The ``page_token`` argument governs the starting point of the page. The ``max_results``
|
||||
argument governs the maximum number of configs that may be included in the returned
|
||||
page. This function never returns ``None``. If there are no OIDC configs in the Firebase
|
||||
project, this returns an empty page.
|
||||
|
||||
Args:
|
||||
page_token: A non-empty page token string, which indicates the starting point of the
|
||||
page (optional). Defaults to ``None``, which will retrieve the first page of users.
|
||||
max_results: A positive integer indicating the maximum number of users to include in
|
||||
the returned page (optional). Defaults to 100, which is also the maximum number
|
||||
allowed.
|
||||
|
||||
Returns:
|
||||
ListProviderConfigsPage: A page of OIDC provider config instances.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``max_results`` or ``page_token`` are invalid.
|
||||
FirebaseError: If an error occurs while retrieving the OIDC provider configs.
|
||||
"""
|
||||
return self._provider_manager.list_oidc_provider_configs(page_token, max_results)
|
||||
|
||||
def get_saml_provider_config(self, provider_id):
|
||||
"""Returns the ``SAMLProviderConfig`` with the given ID.
|
||||
|
||||
Args:
|
||||
provider_id: Provider ID string.
|
||||
|
||||
Returns:
|
||||
SAMLProviderConfig: A SAML provider config instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the provider ID is invalid, empty or does not have ``saml.`` prefix.
|
||||
ConfigurationNotFoundError: If no SAML provider is available with the given identifier.
|
||||
FirebaseError: If an error occurs while retrieving the SAML provider.
|
||||
"""
|
||||
return self._provider_manager.get_saml_provider_config(provider_id)
|
||||
|
||||
def create_saml_provider_config(
|
||||
self, provider_id, idp_entity_id, sso_url, x509_certificates, rp_entity_id,
|
||||
callback_url, display_name=None, enabled=None):
|
||||
"""Creates a new SAML provider config from the given parameters.
|
||||
|
||||
SAML provider support requires Google Cloud's Identity Platform (GCIP). To learn more about
|
||||
GCIP, including pricing and features, see https://cloud.google.com/identity-platform.
|
||||
|
||||
Args:
|
||||
provider_id: Provider ID string. Must have the prefix ``saml.``.
|
||||
idp_entity_id: The SAML IdP entity identifier.
|
||||
sso_url: The SAML IdP SSO URL. Must be a valid URL.
|
||||
x509_certificates: The list of SAML IdP X.509 certificates issued by CA for this
|
||||
provider. Multiple certificates are accepted to prevent outages during IdP key
|
||||
rotation (for example ADFS rotates every 10 days). When the Auth server receives a
|
||||
SAML response, it will match the SAML response with the certificate on record.
|
||||
Otherwise the response is rejected. Developers are expected to manage the
|
||||
certificate updates as keys are rotated.
|
||||
rp_entity_id: The SAML relying party (service provider) entity ID. This is defined by
|
||||
the developer but needs to be provided to the SAML IdP.
|
||||
callback_url: Callback URL string. This is fixed and must always be the same as the
|
||||
OAuth redirect URL provisioned by Firebase Auth, unless a custom authDomain is
|
||||
used.
|
||||
display_name: The user-friendly display name to the current configuration (optional).
|
||||
This name is also used as the provider label in the Cloud Console.
|
||||
enabled: A boolean indicating whether the provider configuration is enabled or disabled
|
||||
(optional). A user cannot sign in using a disabled provider.
|
||||
|
||||
Returns:
|
||||
SAMLProviderConfig: The newly created SAML provider config instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If any of the specified input parameters are invalid.
|
||||
FirebaseError: If an error occurs while creating the new SAML provider config.
|
||||
"""
|
||||
return self._provider_manager.create_saml_provider_config(
|
||||
provider_id, idp_entity_id=idp_entity_id, sso_url=sso_url,
|
||||
x509_certificates=x509_certificates, rp_entity_id=rp_entity_id,
|
||||
callback_url=callback_url, display_name=display_name, enabled=enabled)
|
||||
|
||||
def update_saml_provider_config(
|
||||
self, provider_id, idp_entity_id=None, sso_url=None, x509_certificates=None,
|
||||
rp_entity_id=None, callback_url=None, display_name=None, enabled=None):
|
||||
"""Updates an existing SAML provider config with the given parameters.
|
||||
|
||||
Args:
|
||||
provider_id: Provider ID string. Must have the prefix ``saml.``.
|
||||
idp_entity_id: The SAML IdP entity identifier (optional).
|
||||
sso_url: The SAML IdP SSO URL. Must be a valid URL (optional).
|
||||
x509_certificates: The list of SAML IdP X.509 certificates issued by CA for this
|
||||
provider (optional).
|
||||
rp_entity_id: The SAML relying party entity ID (optional).
|
||||
callback_url: Callback URL string (optional).
|
||||
display_name: The user-friendly display name of the current configuration (optional).
|
||||
Pass ``auth.DELETE_ATTRIBUTE`` to delete the current display name.
|
||||
enabled: A boolean indicating whether the provider configuration is enabled or disabled
|
||||
(optional).
|
||||
|
||||
Returns:
|
||||
SAMLProviderConfig: The updated SAML provider config instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If any of the specified input parameters are invalid.
|
||||
FirebaseError: If an error occurs while updating the SAML provider config.
|
||||
"""
|
||||
return self._provider_manager.update_saml_provider_config(
|
||||
provider_id, idp_entity_id=idp_entity_id, sso_url=sso_url,
|
||||
x509_certificates=x509_certificates, rp_entity_id=rp_entity_id,
|
||||
callback_url=callback_url, display_name=display_name, enabled=enabled)
|
||||
|
||||
def delete_saml_provider_config(self, provider_id):
|
||||
"""Deletes the ``SAMLProviderConfig`` with the given ID.
|
||||
|
||||
Args:
|
||||
provider_id: Provider ID string.
|
||||
|
||||
Raises:
|
||||
ValueError: If the provider ID is invalid, empty or does not have ``saml.`` prefix.
|
||||
ConfigurationNotFoundError: If no SAML provider is available with the given identifier.
|
||||
FirebaseError: If an error occurs while deleting the SAML provider.
|
||||
"""
|
||||
self._provider_manager.delete_saml_provider_config(provider_id)
|
||||
|
||||
def list_saml_provider_configs(
|
||||
self, page_token=None, max_results=_auth_providers.MAX_LIST_CONFIGS_RESULTS):
|
||||
"""Retrieves a page of SAML provider configs from a Firebase project.
|
||||
|
||||
The ``page_token`` argument governs the starting point of the page. The ``max_results``
|
||||
argument governs the maximum number of configs that may be included in the returned
|
||||
page. This function never returns ``None``. If there are no SAML configs in the Firebase
|
||||
project, this returns an empty page.
|
||||
|
||||
Args:
|
||||
page_token: A non-empty page token string, which indicates the starting point of the
|
||||
page (optional). Defaults to ``None``, which will retrieve the first page of users.
|
||||
max_results: A positive integer indicating the maximum number of users to include in
|
||||
the returned page (optional). Defaults to 100, which is also the maximum number
|
||||
allowed.
|
||||
|
||||
Returns:
|
||||
ListProviderConfigsPage: A page of SAML provider config instances.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``max_results`` or ``page_token`` are invalid.
|
||||
FirebaseError: If an error occurs while retrieving the SAML provider configs.
|
||||
"""
|
||||
return self._provider_manager.list_saml_provider_configs(page_token, max_results)
|
||||
|
||||
def _check_jwt_revoked(self, verified_claims, exc_type, label):
|
||||
user = self.get_user(verified_claims.get('uid'))
|
||||
if verified_claims.get('iat') * 1000 < user.tokens_valid_after_timestamp:
|
||||
raise exc_type('The Firebase {0} has been revoked.'.format(label))
|
390
venv/Lib/site-packages/firebase_admin/_auth_providers.py
Normal file
390
venv/Lib/site-packages/firebase_admin/_auth_providers.py
Normal file
|
@ -0,0 +1,390 @@
|
|||
# Copyright 2020 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 auth providers management sub module."""
|
||||
|
||||
from urllib import parse
|
||||
|
||||
import requests
|
||||
|
||||
from firebase_admin import _auth_utils
|
||||
from firebase_admin import _user_mgt
|
||||
|
||||
|
||||
MAX_LIST_CONFIGS_RESULTS = 100
|
||||
|
||||
|
||||
class ProviderConfig:
|
||||
"""Parent type for all authentication provider config types."""
|
||||
|
||||
def __init__(self, data):
|
||||
self._data = data
|
||||
|
||||
@property
|
||||
def provider_id(self):
|
||||
name = self._data['name']
|
||||
return name.split('/')[-1]
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return self._data.get('displayName')
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return self._data.get('enabled', False)
|
||||
|
||||
|
||||
class OIDCProviderConfig(ProviderConfig):
|
||||
"""Represents the OIDC auth provider configuration.
|
||||
|
||||
See https://openid.net/specs/openid-connect-core-1_0-final.html.
|
||||
"""
|
||||
|
||||
@property
|
||||
def issuer(self):
|
||||
return self._data['issuer']
|
||||
|
||||
@property
|
||||
def client_id(self):
|
||||
return self._data['clientId']
|
||||
|
||||
|
||||
class SAMLProviderConfig(ProviderConfig):
|
||||
"""Represents he SAML auth provider configuration.
|
||||
|
||||
See http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html.
|
||||
"""
|
||||
|
||||
@property
|
||||
def idp_entity_id(self):
|
||||
return self._data.get('idpConfig', {})['idpEntityId']
|
||||
|
||||
@property
|
||||
def sso_url(self):
|
||||
return self._data.get('idpConfig', {})['ssoUrl']
|
||||
|
||||
@property
|
||||
def x509_certificates(self):
|
||||
certs = self._data.get('idpConfig', {})['idpCertificates']
|
||||
return [c['x509Certificate'] for c in certs]
|
||||
|
||||
@property
|
||||
def callback_url(self):
|
||||
return self._data.get('spConfig', {})['callbackUri']
|
||||
|
||||
@property
|
||||
def rp_entity_id(self):
|
||||
return self._data.get('spConfig', {})['spEntityId']
|
||||
|
||||
|
||||
class ListProviderConfigsPage:
|
||||
"""Represents a page of AuthProviderConfig instances retrieved from a Firebase project.
|
||||
|
||||
Provides methods for traversing the provider configs included in this page, as well as
|
||||
retrieving subsequent pages. The iterator returned by ``iterate_all()`` can be used to iterate
|
||||
through all provider configs 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 provider_configs(self):
|
||||
"""A list of ``AuthProviderConfig`` instances available in this page."""
|
||||
raise NotImplementedError
|
||||
|
||||
@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 provider configs, if available.
|
||||
|
||||
Returns:
|
||||
ListProviderConfigsPage: Next page of provider configs, or None if this is the last
|
||||
page.
|
||||
"""
|
||||
if self.has_next_page:
|
||||
return self.__class__(self._download, self.next_page_token, self._max_results)
|
||||
return None
|
||||
|
||||
def iterate_all(self):
|
||||
"""Retrieves an iterator for provider configs.
|
||||
|
||||
Returned iterator will iterate through all the provider configs in the Firebase project
|
||||
starting from this page. The iterator will never buffer more than one page of configs
|
||||
in memory at a time.
|
||||
|
||||
Returns:
|
||||
iterator: An iterator of AuthProviderConfig instances.
|
||||
"""
|
||||
return _ProviderConfigIterator(self)
|
||||
|
||||
|
||||
class _ListOIDCProviderConfigsPage(ListProviderConfigsPage):
|
||||
|
||||
@property
|
||||
def provider_configs(self):
|
||||
return [OIDCProviderConfig(data) for data in self._current.get('oauthIdpConfigs', [])]
|
||||
|
||||
|
||||
class _ListSAMLProviderConfigsPage(ListProviderConfigsPage):
|
||||
|
||||
@property
|
||||
def provider_configs(self):
|
||||
return [SAMLProviderConfig(data) for data in self._current.get('inboundSamlConfigs', [])]
|
||||
|
||||
|
||||
class _ProviderConfigIterator(_auth_utils.PageIterator):
|
||||
|
||||
@property
|
||||
def items(self):
|
||||
return self._current_page.provider_configs
|
||||
|
||||
|
||||
class ProviderConfigClient:
|
||||
"""Client for managing Auth provider configurations."""
|
||||
|
||||
PROVIDER_CONFIG_URL = 'https://identitytoolkit.googleapis.com/v2beta1'
|
||||
|
||||
def __init__(self, http_client, project_id, tenant_id=None):
|
||||
self.http_client = http_client
|
||||
self.base_url = '{0}/projects/{1}'.format(self.PROVIDER_CONFIG_URL, project_id)
|
||||
if tenant_id:
|
||||
self.base_url += '/tenants/{0}'.format(tenant_id)
|
||||
|
||||
def get_oidc_provider_config(self, provider_id):
|
||||
_validate_oidc_provider_id(provider_id)
|
||||
body = self._make_request('get', '/oauthIdpConfigs/{0}'.format(provider_id))
|
||||
return OIDCProviderConfig(body)
|
||||
|
||||
def create_oidc_provider_config(
|
||||
self, provider_id, client_id, issuer, display_name=None, enabled=None):
|
||||
"""Creates a new OIDC provider config from the given parameters."""
|
||||
_validate_oidc_provider_id(provider_id)
|
||||
req = {
|
||||
'clientId': _validate_non_empty_string(client_id, 'client_id'),
|
||||
'issuer': _validate_url(issuer, 'issuer'),
|
||||
}
|
||||
if display_name is not None:
|
||||
req['displayName'] = _auth_utils.validate_string(display_name, 'display_name')
|
||||
if enabled is not None:
|
||||
req['enabled'] = _auth_utils.validate_boolean(enabled, 'enabled')
|
||||
|
||||
params = 'oauthIdpConfigId={0}'.format(provider_id)
|
||||
body = self._make_request('post', '/oauthIdpConfigs', json=req, params=params)
|
||||
return OIDCProviderConfig(body)
|
||||
|
||||
def update_oidc_provider_config(
|
||||
self, provider_id, client_id=None, issuer=None, display_name=None, enabled=None):
|
||||
"""Updates an existing OIDC provider config with the given parameters."""
|
||||
_validate_oidc_provider_id(provider_id)
|
||||
req = {}
|
||||
if display_name is not None:
|
||||
if display_name == _user_mgt.DELETE_ATTRIBUTE:
|
||||
req['displayName'] = None
|
||||
else:
|
||||
req['displayName'] = _auth_utils.validate_string(display_name, 'display_name')
|
||||
if enabled is not None:
|
||||
req['enabled'] = _auth_utils.validate_boolean(enabled, 'enabled')
|
||||
if client_id:
|
||||
req['clientId'] = _validate_non_empty_string(client_id, 'client_id')
|
||||
if issuer:
|
||||
req['issuer'] = _validate_url(issuer, 'issuer')
|
||||
|
||||
if not req:
|
||||
raise ValueError('At least one parameter must be specified for update.')
|
||||
|
||||
update_mask = _auth_utils.build_update_mask(req)
|
||||
params = 'updateMask={0}'.format(','.join(update_mask))
|
||||
url = '/oauthIdpConfigs/{0}'.format(provider_id)
|
||||
body = self._make_request('patch', url, json=req, params=params)
|
||||
return OIDCProviderConfig(body)
|
||||
|
||||
def delete_oidc_provider_config(self, provider_id):
|
||||
_validate_oidc_provider_id(provider_id)
|
||||
self._make_request('delete', '/oauthIdpConfigs/{0}'.format(provider_id))
|
||||
|
||||
def list_oidc_provider_configs(self, page_token=None, max_results=MAX_LIST_CONFIGS_RESULTS):
|
||||
return _ListOIDCProviderConfigsPage(
|
||||
self._fetch_oidc_provider_configs, page_token, max_results)
|
||||
|
||||
def _fetch_oidc_provider_configs(self, page_token=None, max_results=MAX_LIST_CONFIGS_RESULTS):
|
||||
return self._fetch_provider_configs('/oauthIdpConfigs', page_token, max_results)
|
||||
|
||||
def get_saml_provider_config(self, provider_id):
|
||||
_validate_saml_provider_id(provider_id)
|
||||
body = self._make_request('get', '/inboundSamlConfigs/{0}'.format(provider_id))
|
||||
return SAMLProviderConfig(body)
|
||||
|
||||
def create_saml_provider_config(
|
||||
self, provider_id, idp_entity_id, sso_url, x509_certificates,
|
||||
rp_entity_id, callback_url, display_name=None, enabled=None):
|
||||
"""Creates a new SAML provider config from the given parameters."""
|
||||
_validate_saml_provider_id(provider_id)
|
||||
req = {
|
||||
'idpConfig': {
|
||||
'idpEntityId': _validate_non_empty_string(idp_entity_id, 'idp_entity_id'),
|
||||
'ssoUrl': _validate_url(sso_url, 'sso_url'),
|
||||
'idpCertificates': _validate_x509_certificates(x509_certificates),
|
||||
},
|
||||
'spConfig': {
|
||||
'spEntityId': _validate_non_empty_string(rp_entity_id, 'rp_entity_id'),
|
||||
'callbackUri': _validate_url(callback_url, 'callback_url'),
|
||||
},
|
||||
}
|
||||
if display_name is not None:
|
||||
req['displayName'] = _auth_utils.validate_string(display_name, 'display_name')
|
||||
if enabled is not None:
|
||||
req['enabled'] = _auth_utils.validate_boolean(enabled, 'enabled')
|
||||
|
||||
params = 'inboundSamlConfigId={0}'.format(provider_id)
|
||||
body = self._make_request('post', '/inboundSamlConfigs', json=req, params=params)
|
||||
return SAMLProviderConfig(body)
|
||||
|
||||
def update_saml_provider_config(
|
||||
self, provider_id, idp_entity_id=None, sso_url=None, x509_certificates=None,
|
||||
rp_entity_id=None, callback_url=None, display_name=None, enabled=None):
|
||||
"""Updates an existing SAML provider config with the given parameters."""
|
||||
_validate_saml_provider_id(provider_id)
|
||||
idp_config = {}
|
||||
if idp_entity_id is not None:
|
||||
idp_config['idpEntityId'] = _validate_non_empty_string(idp_entity_id, 'idp_entity_id')
|
||||
if sso_url is not None:
|
||||
idp_config['ssoUrl'] = _validate_url(sso_url, 'sso_url')
|
||||
if x509_certificates is not None:
|
||||
idp_config['idpCertificates'] = _validate_x509_certificates(x509_certificates)
|
||||
|
||||
sp_config = {}
|
||||
if rp_entity_id is not None:
|
||||
sp_config['spEntityId'] = _validate_non_empty_string(rp_entity_id, 'rp_entity_id')
|
||||
if callback_url is not None:
|
||||
sp_config['callbackUri'] = _validate_url(callback_url, 'callback_url')
|
||||
|
||||
req = {}
|
||||
if display_name is not None:
|
||||
if display_name == _user_mgt.DELETE_ATTRIBUTE:
|
||||
req['displayName'] = None
|
||||
else:
|
||||
req['displayName'] = _auth_utils.validate_string(display_name, 'display_name')
|
||||
if enabled is not None:
|
||||
req['enabled'] = _auth_utils.validate_boolean(enabled, 'enabled')
|
||||
if idp_config:
|
||||
req['idpConfig'] = idp_config
|
||||
if sp_config:
|
||||
req['spConfig'] = sp_config
|
||||
|
||||
if not req:
|
||||
raise ValueError('At least one parameter must be specified for update.')
|
||||
|
||||
update_mask = _auth_utils.build_update_mask(req)
|
||||
params = 'updateMask={0}'.format(','.join(update_mask))
|
||||
url = '/inboundSamlConfigs/{0}'.format(provider_id)
|
||||
body = self._make_request('patch', url, json=req, params=params)
|
||||
return SAMLProviderConfig(body)
|
||||
|
||||
def delete_saml_provider_config(self, provider_id):
|
||||
_validate_saml_provider_id(provider_id)
|
||||
self._make_request('delete', '/inboundSamlConfigs/{0}'.format(provider_id))
|
||||
|
||||
def list_saml_provider_configs(self, page_token=None, max_results=MAX_LIST_CONFIGS_RESULTS):
|
||||
return _ListSAMLProviderConfigsPage(
|
||||
self._fetch_saml_provider_configs, page_token, max_results)
|
||||
|
||||
def _fetch_saml_provider_configs(self, page_token=None, max_results=MAX_LIST_CONFIGS_RESULTS):
|
||||
return self._fetch_provider_configs('/inboundSamlConfigs', page_token, max_results)
|
||||
|
||||
def _fetch_provider_configs(self, path, page_token=None, max_results=MAX_LIST_CONFIGS_RESULTS):
|
||||
"""Fetches a page of auth provider configs"""
|
||||
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_CONFIGS_RESULTS:
|
||||
raise ValueError(
|
||||
'Max results must be a positive integer less than or equal to '
|
||||
'{0}.'.format(MAX_LIST_CONFIGS_RESULTS))
|
||||
|
||||
params = 'pageSize={0}'.format(max_results)
|
||||
if page_token:
|
||||
params += '&pageToken={0}'.format(page_token)
|
||||
return self._make_request('get', path, params=params)
|
||||
|
||||
def _make_request(self, method, path, **kwargs):
|
||||
url = '{0}{1}'.format(self.base_url, path)
|
||||
try:
|
||||
return self.http_client.body(method, url, **kwargs)
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise _auth_utils.handle_auth_backend_error(error)
|
||||
|
||||
|
||||
def _validate_oidc_provider_id(provider_id):
|
||||
if not isinstance(provider_id, str):
|
||||
raise ValueError(
|
||||
'Invalid OIDC provider ID: {0}. Provider ID must be a non-empty string.'.format(
|
||||
provider_id))
|
||||
if not provider_id.startswith('oidc.'):
|
||||
raise ValueError('Invalid OIDC provider ID: {0}.'.format(provider_id))
|
||||
return provider_id
|
||||
|
||||
|
||||
def _validate_saml_provider_id(provider_id):
|
||||
if not isinstance(provider_id, str):
|
||||
raise ValueError(
|
||||
'Invalid SAML provider ID: {0}. Provider ID must be a non-empty string.'.format(
|
||||
provider_id))
|
||||
if not provider_id.startswith('saml.'):
|
||||
raise ValueError('Invalid SAML provider ID: {0}.'.format(provider_id))
|
||||
return provider_id
|
||||
|
||||
|
||||
def _validate_non_empty_string(value, label):
|
||||
"""Validates that the given value is a non-empty string."""
|
||||
if not isinstance(value, str):
|
||||
raise ValueError('Invalid type for {0}: {1}.'.format(label, value))
|
||||
if not value:
|
||||
raise ValueError('{0} must not be empty.'.format(label))
|
||||
return value
|
||||
|
||||
|
||||
def _validate_url(url, label):
|
||||
"""Validates that the given value is a well-formed URL string."""
|
||||
if not isinstance(url, str) or not url:
|
||||
raise ValueError(
|
||||
'Invalid photo URL: "{0}". {1} must be a non-empty '
|
||||
'string.'.format(url, label))
|
||||
try:
|
||||
parsed = parse.urlparse(url)
|
||||
if not parsed.netloc:
|
||||
raise ValueError('Malformed {0}: "{1}".'.format(label, url))
|
||||
return url
|
||||
except Exception:
|
||||
raise ValueError('Malformed {0}: "{1}".'.format(label, url))
|
||||
|
||||
|
||||
def _validate_x509_certificates(x509_certificates):
|
||||
if not isinstance(x509_certificates, list) or not x509_certificates:
|
||||
raise ValueError('x509_certificates must be a non-empty list.')
|
||||
if not all([isinstance(cert, str) and cert for cert in x509_certificates]):
|
||||
raise ValueError('x509_certificates must only contain non-empty strings.')
|
||||
return [{'x509Certificate': cert} for cert in x509_certificates]
|
422
venv/Lib/site-packages/firebase_admin/_auth_utils.py
Normal file
422
venv/Lib/site-packages/firebase_admin/_auth_utils.py
Normal file
|
@ -0,0 +1,422 @@
|
|||
# Copyright 2018 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 auth utils."""
|
||||
|
||||
import json
|
||||
import re
|
||||
from urllib import parse
|
||||
|
||||
from firebase_admin import exceptions
|
||||
from firebase_admin import _utils
|
||||
|
||||
|
||||
MAX_CLAIMS_PAYLOAD_SIZE = 1000
|
||||
RESERVED_CLAIMS = set([
|
||||
'acr', 'amr', 'at_hash', 'aud', 'auth_time', 'azp', 'cnf', 'c_hash', 'exp', 'iat',
|
||||
'iss', 'jti', 'nbf', 'nonce', 'sub', 'firebase',
|
||||
])
|
||||
VALID_EMAIL_ACTION_TYPES = set(['VERIFY_EMAIL', 'EMAIL_SIGNIN', 'PASSWORD_RESET'])
|
||||
|
||||
|
||||
class PageIterator:
|
||||
"""An iterator that allows iterating over a sequence of items, one at a time.
|
||||
|
||||
This implementation loads a page of items into memory, and iterates on them. When the whole
|
||||
page has been traversed, it loads another page. This class never keeps more than one page
|
||||
of entries in memory.
|
||||
"""
|
||||
|
||||
def __init__(self, current_page):
|
||||
if not current_page:
|
||||
raise ValueError('Current page must not be None.')
|
||||
self._current_page = current_page
|
||||
self._index = 0
|
||||
|
||||
def next(self):
|
||||
if self._index == len(self.items):
|
||||
if self._current_page.has_next_page:
|
||||
self._current_page = self._current_page.get_next_page()
|
||||
self._index = 0
|
||||
if self._index < len(self.items):
|
||||
result = self.items[self._index]
|
||||
self._index += 1
|
||||
return result
|
||||
raise StopIteration
|
||||
|
||||
@property
|
||||
def items(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def __next__(self):
|
||||
return self.next()
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
|
||||
def validate_uid(uid, required=False):
|
||||
if uid is None and not required:
|
||||
return None
|
||||
if not isinstance(uid, str) or not uid or len(uid) > 128:
|
||||
raise ValueError(
|
||||
'Invalid uid: "{0}". The uid must be a non-empty string with no more than 128 '
|
||||
'characters.'.format(uid))
|
||||
return uid
|
||||
|
||||
def validate_email(email, required=False):
|
||||
if email is None and not required:
|
||||
return None
|
||||
if not isinstance(email, str) or not email:
|
||||
raise ValueError(
|
||||
'Invalid email: "{0}". Email must be a non-empty string.'.format(email))
|
||||
parts = email.split('@')
|
||||
if len(parts) != 2 or not parts[0] or not parts[1]:
|
||||
raise ValueError('Malformed email address string: "{0}".'.format(email))
|
||||
return email
|
||||
|
||||
def validate_phone(phone, required=False):
|
||||
"""Validates the specified phone number.
|
||||
|
||||
Phone number vlidation is very lax here. Backend will enforce E.164 spec compliance, and
|
||||
normalize accordingly. Here we check if the number starts with + sign, and contains at
|
||||
least one alphanumeric character.
|
||||
"""
|
||||
if phone is None and not required:
|
||||
return None
|
||||
if not isinstance(phone, str) or not phone:
|
||||
raise ValueError('Invalid phone number: "{0}". Phone number must be a non-empty '
|
||||
'string.'.format(phone))
|
||||
if not phone.startswith('+') or not re.search('[a-zA-Z0-9]', phone):
|
||||
raise ValueError('Invalid phone number: "{0}". Phone number must be a valid, E.164 '
|
||||
'compliant identifier.'.format(phone))
|
||||
return phone
|
||||
|
||||
def validate_password(password, required=False):
|
||||
if password is None and not required:
|
||||
return None
|
||||
if not isinstance(password, str) or len(password) < 6:
|
||||
raise ValueError(
|
||||
'Invalid password string. Password must be a string at least 6 characters long.')
|
||||
return password
|
||||
|
||||
def validate_bytes(value, label, required=False):
|
||||
if value is None and not required:
|
||||
return None
|
||||
if not isinstance(value, bytes) or not value:
|
||||
raise ValueError('{0} must be a non-empty byte sequence.'.format(label))
|
||||
return value
|
||||
|
||||
def validate_display_name(display_name, required=False):
|
||||
if display_name is None and not required:
|
||||
return None
|
||||
if not isinstance(display_name, str) or not display_name:
|
||||
raise ValueError(
|
||||
'Invalid display name: "{0}". Display name must be a non-empty '
|
||||
'string.'.format(display_name))
|
||||
return display_name
|
||||
|
||||
def validate_provider_id(provider_id, required=True):
|
||||
if provider_id is None and not required:
|
||||
return None
|
||||
if not isinstance(provider_id, str) or not provider_id:
|
||||
raise ValueError(
|
||||
'Invalid provider ID: "{0}". Provider ID must be a non-empty '
|
||||
'string.'.format(provider_id))
|
||||
return provider_id
|
||||
|
||||
def validate_provider_uid(provider_uid, required=True):
|
||||
if provider_uid is None and not required:
|
||||
return None
|
||||
if not isinstance(provider_uid, str) or not provider_uid:
|
||||
raise ValueError(
|
||||
'Invalid provider UID: "{0}". Provider UID must be a non-empty '
|
||||
'string.'.format(provider_uid))
|
||||
return provider_uid
|
||||
|
||||
def validate_photo_url(photo_url, required=False):
|
||||
"""Parses and validates the given URL string."""
|
||||
if photo_url is None and not required:
|
||||
return None
|
||||
if not isinstance(photo_url, str) or not photo_url:
|
||||
raise ValueError(
|
||||
'Invalid photo URL: "{0}". Photo URL must be a non-empty '
|
||||
'string.'.format(photo_url))
|
||||
try:
|
||||
parsed = parse.urlparse(photo_url)
|
||||
if not parsed.netloc:
|
||||
raise ValueError('Malformed photo URL: "{0}".'.format(photo_url))
|
||||
return photo_url
|
||||
except Exception:
|
||||
raise ValueError('Malformed photo URL: "{0}".'.format(photo_url))
|
||||
|
||||
def validate_timestamp(timestamp, label, required=False):
|
||||
"""Validates the given timestamp value. Timestamps must be positive integers."""
|
||||
if timestamp is None and not required:
|
||||
return None
|
||||
if isinstance(timestamp, bool):
|
||||
raise ValueError('Boolean value specified as timestamp.')
|
||||
try:
|
||||
timestamp_int = int(timestamp)
|
||||
except TypeError:
|
||||
raise ValueError('Invalid type for timestamp value: {0}.'.format(timestamp))
|
||||
else:
|
||||
if timestamp_int != timestamp:
|
||||
raise ValueError('{0} must be a numeric value and a whole number.'.format(label))
|
||||
if timestamp_int <= 0:
|
||||
raise ValueError('{0} timestamp must be a positive interger.'.format(label))
|
||||
return timestamp_int
|
||||
|
||||
def validate_int(value, label, low=None, high=None):
|
||||
"""Validates that the given value represents an integer.
|
||||
|
||||
There are several ways to represent an integer in Python (e.g. 2, 2L, 2.0). This method allows
|
||||
for all such representations except for booleans. Booleans also behave like integers, but
|
||||
always translate to 1 and 0. Passing a boolean to an API that expects integers is most likely
|
||||
a developer error.
|
||||
"""
|
||||
if value is None or isinstance(value, bool):
|
||||
raise ValueError('Invalid type for integer value: {0}.'.format(value))
|
||||
try:
|
||||
val_int = int(value)
|
||||
except TypeError:
|
||||
raise ValueError('Invalid type for integer value: {0}.'.format(value))
|
||||
else:
|
||||
if val_int != value:
|
||||
# This will be True for non-numeric values like '2' and non-whole numbers like 2.5.
|
||||
raise ValueError('{0} must be a numeric value and a whole number.'.format(label))
|
||||
if low is not None and val_int < low:
|
||||
raise ValueError('{0} must not be smaller than {1}.'.format(label, low))
|
||||
if high is not None and val_int > high:
|
||||
raise ValueError('{0} must not be larger than {1}.'.format(label, high))
|
||||
return val_int
|
||||
|
||||
def validate_string(value, label):
|
||||
"""Validates that the given value is a string."""
|
||||
if not isinstance(value, str):
|
||||
raise ValueError('Invalid type for {0}: {1}.'.format(label, value))
|
||||
return value
|
||||
|
||||
def validate_boolean(value, label):
|
||||
"""Validates that the given value is a boolean."""
|
||||
if not isinstance(value, bool):
|
||||
raise ValueError('Invalid type for {0}: {1}.'.format(label, value))
|
||||
return value
|
||||
|
||||
def validate_custom_claims(custom_claims, required=False):
|
||||
"""Validates the specified custom claims.
|
||||
|
||||
Custom claims must be specified as a JSON string. The string must not exceed 1000
|
||||
characters, and the parsed JSON payload must not contain reserved JWT claims.
|
||||
"""
|
||||
if custom_claims is None and not required:
|
||||
return None
|
||||
claims_str = str(custom_claims)
|
||||
if len(claims_str) > MAX_CLAIMS_PAYLOAD_SIZE:
|
||||
raise ValueError(
|
||||
'Custom claims payload must not exceed {0} characters.'.format(
|
||||
MAX_CLAIMS_PAYLOAD_SIZE))
|
||||
try:
|
||||
parsed = json.loads(claims_str)
|
||||
except Exception:
|
||||
raise ValueError('Failed to parse custom claims string as JSON.')
|
||||
|
||||
if not isinstance(parsed, dict):
|
||||
raise ValueError('Custom claims must be parseable as a JSON object.')
|
||||
invalid_claims = RESERVED_CLAIMS.intersection(set(parsed.keys()))
|
||||
if len(invalid_claims) > 1:
|
||||
joined = ', '.join(sorted(invalid_claims))
|
||||
raise ValueError('Claims "{0}" are reserved, and must not be set.'.format(joined))
|
||||
if len(invalid_claims) == 1:
|
||||
raise ValueError(
|
||||
'Claim "{0}" is reserved, and must not be set.'.format(invalid_claims.pop()))
|
||||
return claims_str
|
||||
|
||||
def validate_action_type(action_type):
|
||||
if action_type not in VALID_EMAIL_ACTION_TYPES:
|
||||
raise ValueError('Invalid action type provided action_type: {0}. \
|
||||
Valid values are {1}'.format(action_type, ', '.join(VALID_EMAIL_ACTION_TYPES)))
|
||||
return action_type
|
||||
|
||||
def build_update_mask(params):
|
||||
"""Creates an update mask list from the given dictionary."""
|
||||
mask = []
|
||||
for key, value in params.items():
|
||||
if isinstance(value, dict):
|
||||
child_mask = build_update_mask(value)
|
||||
for child in child_mask:
|
||||
mask.append('{0}.{1}'.format(key, child))
|
||||
else:
|
||||
mask.append(key)
|
||||
|
||||
return sorted(mask)
|
||||
|
||||
|
||||
class UidAlreadyExistsError(exceptions.AlreadyExistsError):
|
||||
"""The user with the provided uid already exists."""
|
||||
|
||||
default_message = 'The user with the provided uid already exists'
|
||||
|
||||
def __init__(self, message, cause, http_response):
|
||||
exceptions.AlreadyExistsError.__init__(self, message, cause, http_response)
|
||||
|
||||
|
||||
class EmailAlreadyExistsError(exceptions.AlreadyExistsError):
|
||||
"""The user with the provided email already exists."""
|
||||
|
||||
default_message = 'The user with the provided email already exists'
|
||||
|
||||
def __init__(self, message, cause, http_response):
|
||||
exceptions.AlreadyExistsError.__init__(self, message, cause, http_response)
|
||||
|
||||
|
||||
class InsufficientPermissionError(exceptions.PermissionDeniedError):
|
||||
"""The credential used to initialize the SDK lacks required permissions."""
|
||||
|
||||
default_message = ('The credential used to initialize the SDK has insufficient '
|
||||
'permissions to perform the requested operation. See '
|
||||
'https://firebase.google.com/docs/admin/setup for details '
|
||||
'on how to initialize the Admin SDK with appropriate permissions')
|
||||
|
||||
def __init__(self, message, cause, http_response):
|
||||
exceptions.PermissionDeniedError.__init__(self, message, cause, http_response)
|
||||
|
||||
|
||||
class InvalidDynamicLinkDomainError(exceptions.InvalidArgumentError):
|
||||
"""Dynamic link domain in ActionCodeSettings is not authorized."""
|
||||
|
||||
default_message = 'Dynamic link domain specified in ActionCodeSettings is not authorized'
|
||||
|
||||
def __init__(self, message, cause, http_response):
|
||||
exceptions.InvalidArgumentError.__init__(self, message, cause, http_response)
|
||||
|
||||
|
||||
class InvalidIdTokenError(exceptions.InvalidArgumentError):
|
||||
"""The provided ID token is not a valid Firebase ID token."""
|
||||
|
||||
default_message = 'The provided ID token is invalid'
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
exceptions.InvalidArgumentError.__init__(self, message, cause, http_response)
|
||||
|
||||
|
||||
class PhoneNumberAlreadyExistsError(exceptions.AlreadyExistsError):
|
||||
"""The user with the provided phone number already exists."""
|
||||
|
||||
default_message = 'The user with the provided phone number already exists'
|
||||
|
||||
def __init__(self, message, cause, http_response):
|
||||
exceptions.AlreadyExistsError.__init__(self, message, cause, http_response)
|
||||
|
||||
|
||||
class UnexpectedResponseError(exceptions.UnknownError):
|
||||
"""Backend service responded with an unexpected or malformed response."""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
exceptions.UnknownError.__init__(self, message, cause, http_response)
|
||||
|
||||
|
||||
class UserNotFoundError(exceptions.NotFoundError):
|
||||
"""No user record found for the specified identifier."""
|
||||
|
||||
default_message = 'No user record found for the given identifier'
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
exceptions.NotFoundError.__init__(self, message, cause, http_response)
|
||||
|
||||
|
||||
class TenantNotFoundError(exceptions.NotFoundError):
|
||||
"""No tenant found for the specified identifier."""
|
||||
|
||||
default_message = 'No tenant found for the given identifier'
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
exceptions.NotFoundError.__init__(self, message, cause, http_response)
|
||||
|
||||
|
||||
class TenantIdMismatchError(exceptions.InvalidArgumentError):
|
||||
"""Missing or invalid tenant ID field in the given JWT."""
|
||||
|
||||
def __init__(self, message):
|
||||
exceptions.InvalidArgumentError.__init__(self, message)
|
||||
|
||||
|
||||
class ConfigurationNotFoundError(exceptions.NotFoundError):
|
||||
"""No auth provider found for the specified identifier."""
|
||||
|
||||
default_message = 'No auth provider found for the given identifier'
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
exceptions.NotFoundError.__init__(self, message, cause, http_response)
|
||||
|
||||
|
||||
_CODE_TO_EXC_TYPE = {
|
||||
'CONFIGURATION_NOT_FOUND': ConfigurationNotFoundError,
|
||||
'DUPLICATE_EMAIL': EmailAlreadyExistsError,
|
||||
'DUPLICATE_LOCAL_ID': UidAlreadyExistsError,
|
||||
'EMAIL_EXISTS': EmailAlreadyExistsError,
|
||||
'INSUFFICIENT_PERMISSION': InsufficientPermissionError,
|
||||
'INVALID_DYNAMIC_LINK_DOMAIN': InvalidDynamicLinkDomainError,
|
||||
'INVALID_ID_TOKEN': InvalidIdTokenError,
|
||||
'PHONE_NUMBER_EXISTS': PhoneNumberAlreadyExistsError,
|
||||
'TENANT_NOT_FOUND': TenantNotFoundError,
|
||||
'USER_NOT_FOUND': UserNotFoundError,
|
||||
}
|
||||
|
||||
|
||||
def handle_auth_backend_error(error):
|
||||
"""Converts a requests error received from the Firebase Auth service into a FirebaseError."""
|
||||
if error.response is None:
|
||||
return _utils.handle_requests_error(error)
|
||||
|
||||
code, custom_message = _parse_error_body(error.response)
|
||||
if not code:
|
||||
msg = 'Unexpected error response: {0}'.format(error.response.content.decode())
|
||||
return _utils.handle_requests_error(error, message=msg)
|
||||
|
||||
exc_type = _CODE_TO_EXC_TYPE.get(code)
|
||||
msg = _build_error_message(code, exc_type, custom_message)
|
||||
if not exc_type:
|
||||
return _utils.handle_requests_error(error, message=msg)
|
||||
|
||||
return exc_type(msg, cause=error, http_response=error.response)
|
||||
|
||||
|
||||
def _parse_error_body(response):
|
||||
"""Parses the given error response to extract Auth error code and message."""
|
||||
error_dict = {}
|
||||
try:
|
||||
parsed_body = response.json()
|
||||
if isinstance(parsed_body, dict):
|
||||
error_dict = parsed_body.get('error', {})
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Auth error response format: {"error": {"message": "AUTH_ERROR_CODE: Optional text"}}
|
||||
code = error_dict.get('message') if isinstance(error_dict, dict) else None
|
||||
custom_message = None
|
||||
if code:
|
||||
separator = code.find(':')
|
||||
if separator != -1:
|
||||
custom_message = code[separator + 1:].strip()
|
||||
code = code[:separator]
|
||||
|
||||
return code, custom_message
|
||||
|
||||
|
||||
def _build_error_message(code, exc_type, custom_message):
|
||||
default_message = exc_type.default_message if (
|
||||
exc_type and hasattr(exc_type, 'default_message')) else 'Error while calling Auth service'
|
||||
ext = ' {0}'.format(custom_message) if custom_message else ''
|
||||
return '{0} ({1}).{2}'.format(default_message, code, ext)
|
148
venv/Lib/site-packages/firebase_admin/_http_client.py
Normal file
148
venv/Lib/site-packages/firebase_admin/_http_client.py
Normal file
|
@ -0,0 +1,148 @@
|
|||
# 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.
|
||||
|
||||
"""Internal HTTP client module.
|
||||
|
||||
This module provides utilities for making HTTP calls using the requests library.
|
||||
"""
|
||||
|
||||
from google.auth import transport
|
||||
import requests
|
||||
from requests.packages.urllib3.util import retry # pylint: disable=import-error
|
||||
|
||||
|
||||
_ANY_METHOD = None
|
||||
|
||||
# Default retry configuration: Retries once on low-level connection and socket read errors.
|
||||
# Retries up to 4 times on HTTP 500 and 503 errors, with exponential backoff. Returns the
|
||||
# last response upon exhausting all retries.
|
||||
DEFAULT_RETRY_CONFIG = retry.Retry(
|
||||
connect=1, read=1, status=4, status_forcelist=[500, 503], method_whitelist=_ANY_METHOD,
|
||||
raise_on_status=False, backoff_factor=0.5)
|
||||
|
||||
|
||||
DEFAULT_TIMEOUT_SECONDS = 120
|
||||
|
||||
|
||||
class HttpClient:
|
||||
"""Base HTTP client used to make HTTP calls.
|
||||
|
||||
HttpClient maintains an HTTP session, and handles request authentication and retries if
|
||||
necessary.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, credential=None, session=None, base_url='', headers=None,
|
||||
retries=DEFAULT_RETRY_CONFIG, timeout=DEFAULT_TIMEOUT_SECONDS):
|
||||
"""Creates a new HttpClient instance from the provided arguments.
|
||||
|
||||
If a credential is provided, initializes a new HTTP session authorized with it. If neither
|
||||
a credential nor a session is provided, initializes a new unauthorized session.
|
||||
|
||||
Args:
|
||||
credential: A Google credential that can be used to authenticate requests (optional).
|
||||
session: A custom HTTP session (optional).
|
||||
base_url: A URL prefix to be added to all outgoing requests (optional).
|
||||
headers: A map of headers to be added to all outgoing requests (optional).
|
||||
retries: A urllib retry configuration. Default settings would retry once for low-level
|
||||
connection and socket read errors, and up to 4 times for HTTP 500 and 503 errors.
|
||||
Pass a False value to disable retries (optional).
|
||||
timeout: HTTP timeout in seconds. Defaults to 120 seconds when not specified. Set to
|
||||
None to disable timeouts (optional).
|
||||
"""
|
||||
if credential:
|
||||
self._session = transport.requests.AuthorizedSession(credential)
|
||||
elif session:
|
||||
self._session = session
|
||||
else:
|
||||
self._session = requests.Session() # pylint: disable=redefined-variable-type
|
||||
|
||||
if headers:
|
||||
self._session.headers.update(headers)
|
||||
if retries:
|
||||
self._session.mount('http://', requests.adapters.HTTPAdapter(max_retries=retries))
|
||||
self._session.mount('https://', requests.adapters.HTTPAdapter(max_retries=retries))
|
||||
self._base_url = base_url
|
||||
self._timeout = timeout
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
return self._session
|
||||
|
||||
@property
|
||||
def base_url(self):
|
||||
return self._base_url
|
||||
|
||||
@property
|
||||
def timeout(self):
|
||||
return self._timeout
|
||||
|
||||
def parse_body(self, resp):
|
||||
raise NotImplementedError
|
||||
|
||||
def request(self, method, url, **kwargs):
|
||||
"""Makes an HTTP call using the Python requests library.
|
||||
|
||||
This is the sole entry point to the requests library. All other helper methods in this
|
||||
class call this method to send HTTP requests out. Refer to
|
||||
http://docs.python-requests.org/en/master/api/ for more information on supported options
|
||||
and features.
|
||||
|
||||
Args:
|
||||
method: HTTP method name as a string (e.g. get, post).
|
||||
url: URL of the remote endpoint.
|
||||
kwargs: An additional set of keyword arguments to be passed into the requests API
|
||||
(e.g. json, params, timeout).
|
||||
|
||||
Returns:
|
||||
Response: An HTTP response object.
|
||||
|
||||
Raises:
|
||||
RequestException: Any requests exceptions encountered while making the HTTP call.
|
||||
"""
|
||||
if 'timeout' not in kwargs:
|
||||
kwargs['timeout'] = self.timeout
|
||||
resp = self._session.request(method, self.base_url + url, **kwargs)
|
||||
resp.raise_for_status()
|
||||
return resp
|
||||
|
||||
def headers(self, method, url, **kwargs):
|
||||
resp = self.request(method, url, **kwargs)
|
||||
return resp.headers
|
||||
|
||||
def body_and_response(self, method, url, **kwargs):
|
||||
resp = self.request(method, url, **kwargs)
|
||||
return self.parse_body(resp), resp
|
||||
|
||||
def body(self, method, url, **kwargs):
|
||||
resp = self.request(method, url, **kwargs)
|
||||
return self.parse_body(resp)
|
||||
|
||||
def headers_and_body(self, method, url, **kwargs):
|
||||
resp = self.request(method, url, **kwargs)
|
||||
return resp.headers, self.parse_body(resp)
|
||||
|
||||
def close(self):
|
||||
self._session.close()
|
||||
self._session = None
|
||||
|
||||
|
||||
class JsonHttpClient(HttpClient):
|
||||
"""An HTTP client that parses response messages as JSON."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
HttpClient.__init__(self, **kwargs)
|
||||
|
||||
def parse_body(self, resp):
|
||||
return resp.json()
|
696
venv/Lib/site-packages/firebase_admin/_messaging_encoder.py
Normal file
696
venv/Lib/site-packages/firebase_admin/_messaging_encoder.py
Normal file
|
@ -0,0 +1,696 @@
|
|||
# Copyright 2019 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.
|
||||
|
||||
"""Encoding and validation utils for the messaging (FCM) module."""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import math
|
||||
import numbers
|
||||
import re
|
||||
|
||||
import firebase_admin._messaging_utils as _messaging_utils
|
||||
|
||||
|
||||
class Message:
|
||||
"""A message that can be sent via Firebase Cloud Messaging.
|
||||
|
||||
Contains payload information as well as recipient information. In particular, the message must
|
||||
contain exactly one of token, topic or condition fields.
|
||||
|
||||
Args:
|
||||
data: A dictionary of data fields (optional). All keys and values in the dictionary must be
|
||||
strings.
|
||||
notification: An instance of ``messaging.Notification`` (optional).
|
||||
android: An instance of ``messaging.AndroidConfig`` (optional).
|
||||
webpush: An instance of ``messaging.WebpushConfig`` (optional).
|
||||
apns: An instance of ``messaging.ApnsConfig`` (optional).
|
||||
fcm_options: An instance of ``messaging.FCMOptions`` (optional).
|
||||
token: The registration token of the device to which the message should be sent (optional).
|
||||
topic: Name of the FCM topic to which the message should be sent (optional). Topic name
|
||||
may contain the ``/topics/`` prefix.
|
||||
condition: The FCM condition to which the message should be sent (optional).
|
||||
"""
|
||||
|
||||
def __init__(self, data=None, notification=None, android=None, webpush=None, apns=None,
|
||||
fcm_options=None, token=None, topic=None, condition=None):
|
||||
self.data = data
|
||||
self.notification = notification
|
||||
self.android = android
|
||||
self.webpush = webpush
|
||||
self.apns = apns
|
||||
self.fcm_options = fcm_options
|
||||
self.token = token
|
||||
self.topic = topic
|
||||
self.condition = condition
|
||||
|
||||
def __str__(self):
|
||||
return json.dumps(self, cls=MessageEncoder, sort_keys=True)
|
||||
|
||||
|
||||
class MulticastMessage:
|
||||
"""A message that can be sent to multiple tokens via Firebase Cloud Messaging.
|
||||
|
||||
Args:
|
||||
tokens: A list of registration tokens of targeted devices.
|
||||
data: A dictionary of data fields (optional). All keys and values in the dictionary must be
|
||||
strings.
|
||||
notification: An instance of ``messaging.Notification`` (optional).
|
||||
android: An instance of ``messaging.AndroidConfig`` (optional).
|
||||
webpush: An instance of ``messaging.WebpushConfig`` (optional).
|
||||
apns: An instance of ``messaging.ApnsConfig`` (optional).
|
||||
fcm_options: An instance of ``messaging.FCMOptions`` (optional).
|
||||
"""
|
||||
def __init__(self, tokens, data=None, notification=None, android=None, webpush=None, apns=None,
|
||||
fcm_options=None):
|
||||
_Validators.check_string_list('MulticastMessage.tokens', tokens)
|
||||
if len(tokens) > 500:
|
||||
raise ValueError('MulticastMessage.tokens must not contain more than 500 tokens.')
|
||||
self.tokens = tokens
|
||||
self.data = data
|
||||
self.notification = notification
|
||||
self.android = android
|
||||
self.webpush = webpush
|
||||
self.apns = apns
|
||||
self.fcm_options = fcm_options
|
||||
|
||||
|
||||
class _Validators:
|
||||
"""A collection of data validation utilities.
|
||||
|
||||
Methods provided in this class raise ``ValueErrors`` if any validations fail.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def check_string(cls, label, value, non_empty=False):
|
||||
"""Checks if the given value is a string."""
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, str):
|
||||
if non_empty:
|
||||
raise ValueError('{0} must be a non-empty string.'.format(label))
|
||||
raise ValueError('{0} must be a string.'.format(label))
|
||||
if non_empty and not value:
|
||||
raise ValueError('{0} must be a non-empty string.'.format(label))
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def check_number(cls, label, value):
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, numbers.Number):
|
||||
raise ValueError('{0} must be a number.'.format(label))
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def check_string_dict(cls, label, value):
|
||||
"""Checks if the given value is a dictionary comprised only of string keys and values."""
|
||||
if value is None or value == {}:
|
||||
return None
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError('{0} must be a dictionary.'.format(label))
|
||||
non_str = [k for k in value if not isinstance(k, str)]
|
||||
if non_str:
|
||||
raise ValueError('{0} must not contain non-string keys.'.format(label))
|
||||
non_str = [v for v in value.values() if not isinstance(v, str)]
|
||||
if non_str:
|
||||
raise ValueError('{0} must not contain non-string values.'.format(label))
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def check_string_list(cls, label, value):
|
||||
"""Checks if the given value is a list comprised only of strings."""
|
||||
if value is None or value == []:
|
||||
return None
|
||||
if not isinstance(value, list):
|
||||
raise ValueError('{0} must be a list of strings.'.format(label))
|
||||
non_str = [k for k in value if not isinstance(k, str)]
|
||||
if non_str:
|
||||
raise ValueError('{0} must not contain non-string values.'.format(label))
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def check_number_list(cls, label, value):
|
||||
"""Checks if the given value is a list comprised only of numbers."""
|
||||
if value is None or value == []:
|
||||
return None
|
||||
if not isinstance(value, list):
|
||||
raise ValueError('{0} must be a list of numbers.'.format(label))
|
||||
non_number = [k for k in value if not isinstance(k, numbers.Number)]
|
||||
if non_number:
|
||||
raise ValueError('{0} must not contain non-number values.'.format(label))
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def check_analytics_label(cls, label, value):
|
||||
"""Checks if the given value is a valid analytics label."""
|
||||
value = _Validators.check_string(label, value)
|
||||
if value is not None and not re.match(r'^[a-zA-Z0-9-_.~%]{1,50}$', value):
|
||||
raise ValueError('Malformed {}.'.format(label))
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def check_datetime(cls, label, value):
|
||||
"""Checks if the given value is a datetime."""
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, datetime.datetime):
|
||||
raise ValueError('{0} must be a datetime.'.format(label))
|
||||
return value
|
||||
|
||||
|
||||
class MessageEncoder(json.JSONEncoder):
|
||||
"""A custom ``JSONEncoder`` implementation for serializing Message instances into JSON."""
|
||||
|
||||
@classmethod
|
||||
def remove_null_values(cls, dict_value):
|
||||
return {k: v for k, v in dict_value.items() if v not in [None, [], {}]}
|
||||
|
||||
@classmethod
|
||||
def encode_android(cls, android):
|
||||
"""Encodes an ``AndroidConfig`` instance into JSON."""
|
||||
if android is None:
|
||||
return None
|
||||
if not isinstance(android, _messaging_utils.AndroidConfig):
|
||||
raise ValueError('Message.android must be an instance of AndroidConfig class.')
|
||||
result = {
|
||||
'collapse_key': _Validators.check_string(
|
||||
'AndroidConfig.collapse_key', android.collapse_key),
|
||||
'data': _Validators.check_string_dict(
|
||||
'AndroidConfig.data', android.data),
|
||||
'notification': cls.encode_android_notification(android.notification),
|
||||
'priority': _Validators.check_string(
|
||||
'AndroidConfig.priority', android.priority, non_empty=True),
|
||||
'restricted_package_name': _Validators.check_string(
|
||||
'AndroidConfig.restricted_package_name', android.restricted_package_name),
|
||||
'ttl': cls.encode_ttl(android.ttl),
|
||||
'fcm_options': cls.encode_android_fcm_options(android.fcm_options),
|
||||
}
|
||||
result = cls.remove_null_values(result)
|
||||
priority = result.get('priority')
|
||||
if priority and priority not in ('high', 'normal'):
|
||||
raise ValueError('AndroidConfig.priority must be "high" or "normal".')
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def encode_android_fcm_options(cls, fcm_options):
|
||||
"""Encodes an ``AndroidFCMOptions`` instance into JSON."""
|
||||
if fcm_options is None:
|
||||
return None
|
||||
if not isinstance(fcm_options, _messaging_utils.AndroidFCMOptions):
|
||||
raise ValueError('AndroidConfig.fcm_options must be an instance of '
|
||||
'AndroidFCMOptions class.')
|
||||
result = {
|
||||
'analytics_label': _Validators.check_analytics_label(
|
||||
'AndroidFCMOptions.analytics_label', fcm_options.analytics_label),
|
||||
}
|
||||
result = cls.remove_null_values(result)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def encode_ttl(cls, ttl):
|
||||
"""Encodes an ``AndroidConfig`` ``TTL`` duration into a string."""
|
||||
if ttl is None:
|
||||
return None
|
||||
if isinstance(ttl, numbers.Number):
|
||||
ttl = datetime.timedelta(seconds=ttl)
|
||||
if not isinstance(ttl, datetime.timedelta):
|
||||
raise ValueError('AndroidConfig.ttl must be a duration in seconds or an instance of '
|
||||
'datetime.timedelta.')
|
||||
total_seconds = ttl.total_seconds()
|
||||
if total_seconds < 0:
|
||||
raise ValueError('AndroidConfig.ttl must not be negative.')
|
||||
seconds = int(math.floor(total_seconds))
|
||||
nanos = int((total_seconds - seconds) * 1e9)
|
||||
if nanos:
|
||||
return '{0}.{1}s'.format(seconds, str(nanos).zfill(9))
|
||||
return '{0}s'.format(seconds)
|
||||
|
||||
@classmethod
|
||||
def encode_milliseconds(cls, label, msec):
|
||||
"""Encodes a duration in milliseconds into a string."""
|
||||
if msec is None:
|
||||
return None
|
||||
if isinstance(msec, numbers.Number):
|
||||
msec = datetime.timedelta(milliseconds=msec)
|
||||
if not isinstance(msec, datetime.timedelta):
|
||||
raise ValueError('{0} must be a duration in milliseconds or an instance of '
|
||||
'datetime.timedelta.'.format(label))
|
||||
total_seconds = msec.total_seconds()
|
||||
if total_seconds < 0:
|
||||
raise ValueError('{0} must not be negative.'.format(label))
|
||||
seconds = int(math.floor(total_seconds))
|
||||
nanos = int((total_seconds - seconds) * 1e9)
|
||||
if nanos:
|
||||
return '{0}.{1}s'.format(seconds, str(nanos).zfill(9))
|
||||
return '{0}s'.format(seconds)
|
||||
|
||||
@classmethod
|
||||
def encode_android_notification(cls, notification):
|
||||
"""Encodes an ``AndroidNotification`` instance into JSON."""
|
||||
if notification is None:
|
||||
return None
|
||||
if not isinstance(notification, _messaging_utils.AndroidNotification):
|
||||
raise ValueError('AndroidConfig.notification must be an instance of '
|
||||
'AndroidNotification class.')
|
||||
result = {
|
||||
'body': _Validators.check_string(
|
||||
'AndroidNotification.body', notification.body),
|
||||
'body_loc_args': _Validators.check_string_list(
|
||||
'AndroidNotification.body_loc_args', notification.body_loc_args),
|
||||
'body_loc_key': _Validators.check_string(
|
||||
'AndroidNotification.body_loc_key', notification.body_loc_key),
|
||||
'click_action': _Validators.check_string(
|
||||
'AndroidNotification.click_action', notification.click_action),
|
||||
'color': _Validators.check_string(
|
||||
'AndroidNotification.color', notification.color, non_empty=True),
|
||||
'icon': _Validators.check_string(
|
||||
'AndroidNotification.icon', notification.icon),
|
||||
'sound': _Validators.check_string(
|
||||
'AndroidNotification.sound', notification.sound),
|
||||
'tag': _Validators.check_string(
|
||||
'AndroidNotification.tag', notification.tag),
|
||||
'title': _Validators.check_string(
|
||||
'AndroidNotification.title', notification.title),
|
||||
'title_loc_args': _Validators.check_string_list(
|
||||
'AndroidNotification.title_loc_args', notification.title_loc_args),
|
||||
'title_loc_key': _Validators.check_string(
|
||||
'AndroidNotification.title_loc_key', notification.title_loc_key),
|
||||
'channel_id': _Validators.check_string(
|
||||
'AndroidNotification.channel_id', notification.channel_id),
|
||||
'image': _Validators.check_string(
|
||||
'image', notification.image),
|
||||
'ticker': _Validators.check_string(
|
||||
'AndroidNotification.ticker', notification.ticker),
|
||||
'sticky': notification.sticky,
|
||||
'event_time': _Validators.check_datetime(
|
||||
'AndroidNotification.event_timestamp', notification.event_timestamp),
|
||||
'local_only': notification.local_only,
|
||||
'notification_priority': _Validators.check_string(
|
||||
'AndroidNotification.priority', notification.priority, non_empty=True),
|
||||
'vibrate_timings': _Validators.check_number_list(
|
||||
'AndroidNotification.vibrate_timings_millis', notification.vibrate_timings_millis),
|
||||
'default_vibrate_timings': notification.default_vibrate_timings,
|
||||
'default_sound': notification.default_sound,
|
||||
'default_light_settings': notification.default_light_settings,
|
||||
'light_settings': cls.encode_light_settings(notification.light_settings),
|
||||
'visibility': _Validators.check_string(
|
||||
'AndroidNotification.visibility', notification.visibility, non_empty=True),
|
||||
'notification_count': _Validators.check_number(
|
||||
'AndroidNotification.notification_count', notification.notification_count)
|
||||
}
|
||||
result = cls.remove_null_values(result)
|
||||
color = result.get('color')
|
||||
if color and not re.match(r'^#[0-9a-fA-F]{6}$', color):
|
||||
raise ValueError(
|
||||
'AndroidNotification.color must be in the form #RRGGBB.')
|
||||
if result.get('body_loc_args') and not result.get('body_loc_key'):
|
||||
raise ValueError(
|
||||
'AndroidNotification.body_loc_key is required when specifying body_loc_args.')
|
||||
if result.get('title_loc_args') and not result.get('title_loc_key'):
|
||||
raise ValueError(
|
||||
'AndroidNotification.title_loc_key is required when specifying title_loc_args.')
|
||||
|
||||
event_time = result.get('event_time')
|
||||
if event_time:
|
||||
# if the datetime instance is not naive (tzinfo is present), convert to UTC
|
||||
# otherwise (tzinfo is None) assume the datetime instance is already in UTC
|
||||
if event_time.tzinfo is not None:
|
||||
event_time = event_time.astimezone(datetime.timezone.utc)
|
||||
result['event_time'] = event_time.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
|
||||
|
||||
priority = result.get('notification_priority')
|
||||
if priority:
|
||||
if priority not in ('min', 'low', 'default', 'high', 'max'):
|
||||
raise ValueError('AndroidNotification.priority must be "default", "min", "low", '
|
||||
'"high" or "max".')
|
||||
result['notification_priority'] = 'PRIORITY_' + priority.upper()
|
||||
|
||||
visibility = result.get('visibility')
|
||||
if visibility:
|
||||
if visibility not in ('private', 'public', 'secret'):
|
||||
raise ValueError(
|
||||
'AndroidNotification.visibility must be "private", "public" or "secret".')
|
||||
result['visibility'] = visibility.upper()
|
||||
|
||||
vibrate_timings_millis = result.get('vibrate_timings')
|
||||
if vibrate_timings_millis:
|
||||
vibrate_timing_strings = []
|
||||
for msec in vibrate_timings_millis:
|
||||
formated_string = cls.encode_milliseconds(
|
||||
'AndroidNotification.vibrate_timings_millis', msec)
|
||||
vibrate_timing_strings.append(formated_string)
|
||||
result['vibrate_timings'] = vibrate_timing_strings
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def encode_light_settings(cls, light_settings):
|
||||
"""Encodes a ``LightSettings`` instance into JSON."""
|
||||
if light_settings is None:
|
||||
return None
|
||||
if not isinstance(light_settings, _messaging_utils.LightSettings):
|
||||
raise ValueError(
|
||||
'AndroidNotification.light_settings must be an instance of LightSettings class.')
|
||||
result = {
|
||||
'color': _Validators.check_string(
|
||||
'LightSettings.color', light_settings.color, non_empty=True),
|
||||
'light_on_duration': cls.encode_milliseconds(
|
||||
'LightSettings.light_on_duration_millis', light_settings.light_on_duration_millis),
|
||||
'light_off_duration': cls.encode_milliseconds(
|
||||
'LightSettings.light_off_duration_millis',
|
||||
light_settings.light_off_duration_millis),
|
||||
}
|
||||
result = cls.remove_null_values(result)
|
||||
light_on_duration = result.get('light_on_duration')
|
||||
if not light_on_duration:
|
||||
raise ValueError(
|
||||
'LightSettings.light_on_duration_millis is required.')
|
||||
|
||||
light_off_duration = result.get('light_off_duration')
|
||||
if not light_off_duration:
|
||||
raise ValueError(
|
||||
'LightSettings.light_off_duration_millis is required.')
|
||||
|
||||
color = result.get('color')
|
||||
if not color:
|
||||
raise ValueError('LightSettings.color is required.')
|
||||
if not re.match(r'^#[0-9a-fA-F]{6}$', color) and not re.match(r'^#[0-9a-fA-F]{8}$', color):
|
||||
raise ValueError(
|
||||
'LightSettings.color must be in the form #RRGGBB or #RRGGBBAA.')
|
||||
if len(color) == 7:
|
||||
color = (color+'FF')
|
||||
rgba = [int(color[i:i + 2], 16) / 255.0 for i in (1, 3, 5, 7)]
|
||||
result['color'] = {'red': rgba[0], 'green': rgba[1],
|
||||
'blue': rgba[2], 'alpha': rgba[3]}
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def encode_webpush(cls, webpush):
|
||||
"""Encodes a ``WebpushConfig`` instance into JSON."""
|
||||
if webpush is None:
|
||||
return None
|
||||
if not isinstance(webpush, _messaging_utils.WebpushConfig):
|
||||
raise ValueError('Message.webpush must be an instance of WebpushConfig class.')
|
||||
result = {
|
||||
'data': _Validators.check_string_dict(
|
||||
'WebpushConfig.data', webpush.data),
|
||||
'headers': _Validators.check_string_dict(
|
||||
'WebpushConfig.headers', webpush.headers),
|
||||
'notification': cls.encode_webpush_notification(webpush.notification),
|
||||
'fcm_options': cls.encode_webpush_fcm_options(webpush.fcm_options),
|
||||
}
|
||||
return cls.remove_null_values(result)
|
||||
|
||||
@classmethod
|
||||
def encode_webpush_notification(cls, notification):
|
||||
"""Encodes a ``WebpushNotification`` instance into JSON."""
|
||||
if notification is None:
|
||||
return None
|
||||
if not isinstance(notification, _messaging_utils.WebpushNotification):
|
||||
raise ValueError('WebpushConfig.notification must be an instance of '
|
||||
'WebpushNotification class.')
|
||||
result = {
|
||||
'actions': cls.encode_webpush_notification_actions(notification.actions),
|
||||
'badge': _Validators.check_string(
|
||||
'WebpushNotification.badge', notification.badge),
|
||||
'body': _Validators.check_string(
|
||||
'WebpushNotification.body', notification.body),
|
||||
'data': notification.data,
|
||||
'dir': _Validators.check_string(
|
||||
'WebpushNotification.direction', notification.direction),
|
||||
'icon': _Validators.check_string(
|
||||
'WebpushNotification.icon', notification.icon),
|
||||
'image': _Validators.check_string(
|
||||
'WebpushNotification.image', notification.image),
|
||||
'lang': _Validators.check_string(
|
||||
'WebpushNotification.language', notification.language),
|
||||
'renotify': notification.renotify,
|
||||
'requireInteraction': notification.require_interaction,
|
||||
'silent': notification.silent,
|
||||
'tag': _Validators.check_string(
|
||||
'WebpushNotification.tag', notification.tag),
|
||||
'timestamp': _Validators.check_number(
|
||||
'WebpushNotification.timestamp_millis', notification.timestamp_millis),
|
||||
'title': _Validators.check_string(
|
||||
'WebpushNotification.title', notification.title),
|
||||
'vibrate': notification.vibrate,
|
||||
}
|
||||
direction = result.get('dir')
|
||||
if direction and direction not in ('auto', 'ltr', 'rtl'):
|
||||
raise ValueError('WebpushNotification.direction must be "auto", "ltr" or "rtl".')
|
||||
if notification.custom_data is not None:
|
||||
if not isinstance(notification.custom_data, dict):
|
||||
raise ValueError('WebpushNotification.custom_data must be a dict.')
|
||||
for key, value in notification.custom_data.items():
|
||||
if key in result:
|
||||
raise ValueError(
|
||||
'Multiple specifications for {0} in WebpushNotification.'.format(key))
|
||||
result[key] = value
|
||||
return cls.remove_null_values(result)
|
||||
|
||||
@classmethod
|
||||
def encode_webpush_notification_actions(cls, actions):
|
||||
"""Encodes a list of ``WebpushNotificationActions`` into JSON."""
|
||||
if actions is None:
|
||||
return None
|
||||
if not isinstance(actions, list):
|
||||
raise ValueError('WebpushConfig.notification.actions must be a list of '
|
||||
'WebpushNotificationAction instances.')
|
||||
results = []
|
||||
for action in actions:
|
||||
if not isinstance(action, _messaging_utils.WebpushNotificationAction):
|
||||
raise ValueError('WebpushConfig.notification.actions must be a list of '
|
||||
'WebpushNotificationAction instances.')
|
||||
result = {
|
||||
'action': _Validators.check_string(
|
||||
'WebpushNotificationAction.action', action.action),
|
||||
'title': _Validators.check_string(
|
||||
'WebpushNotificationAction.title', action.title),
|
||||
'icon': _Validators.check_string(
|
||||
'WebpushNotificationAction.icon', action.icon),
|
||||
}
|
||||
results.append(cls.remove_null_values(result))
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def encode_webpush_fcm_options(cls, options):
|
||||
"""Encodes a ``WebpushFCMOptions`` instance into JSON."""
|
||||
if options is None:
|
||||
return None
|
||||
result = {
|
||||
'link': _Validators.check_string('WebpushConfig.fcm_options.link', options.link),
|
||||
}
|
||||
result = cls.remove_null_values(result)
|
||||
link = result.get('link')
|
||||
if link is not None and not link.startswith('https://'):
|
||||
raise ValueError('WebpushFCMOptions.link must be a HTTPS URL.')
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def encode_apns(cls, apns):
|
||||
"""Encodes an ``APNSConfig`` instance into JSON."""
|
||||
if apns is None:
|
||||
return None
|
||||
if not isinstance(apns, _messaging_utils.APNSConfig):
|
||||
raise ValueError('Message.apns must be an instance of APNSConfig class.')
|
||||
result = {
|
||||
'headers': _Validators.check_string_dict(
|
||||
'APNSConfig.headers', apns.headers),
|
||||
'payload': cls.encode_apns_payload(apns.payload),
|
||||
'fcm_options': cls.encode_apns_fcm_options(apns.fcm_options),
|
||||
}
|
||||
return cls.remove_null_values(result)
|
||||
|
||||
@classmethod
|
||||
def encode_apns_payload(cls, payload):
|
||||
"""Encodes an ``APNSPayload`` instance into JSON."""
|
||||
if payload is None:
|
||||
return None
|
||||
if not isinstance(payload, _messaging_utils.APNSPayload):
|
||||
raise ValueError('APNSConfig.payload must be an instance of APNSPayload class.')
|
||||
result = {
|
||||
'aps': cls.encode_aps(payload.aps)
|
||||
}
|
||||
for key, value in payload.custom_data.items():
|
||||
result[key] = value
|
||||
return cls.remove_null_values(result)
|
||||
|
||||
@classmethod
|
||||
def encode_apns_fcm_options(cls, fcm_options):
|
||||
"""Encodes an ``APNSFCMOptions`` instance into JSON."""
|
||||
if fcm_options is None:
|
||||
return None
|
||||
if not isinstance(fcm_options, _messaging_utils.APNSFCMOptions):
|
||||
raise ValueError('APNSConfig.fcm_options must be an instance of APNSFCMOptions class.')
|
||||
result = {
|
||||
'analytics_label': _Validators.check_analytics_label(
|
||||
'APNSFCMOptions.analytics_label', fcm_options.analytics_label),
|
||||
'image': _Validators.check_string('APNSFCMOptions.image', fcm_options.image)
|
||||
}
|
||||
result = cls.remove_null_values(result)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def encode_aps(cls, aps):
|
||||
"""Encodes an ``Aps`` instance into JSON."""
|
||||
if not isinstance(aps, _messaging_utils.Aps):
|
||||
raise ValueError('APNSPayload.aps must be an instance of Aps class.')
|
||||
result = {
|
||||
'alert': cls.encode_aps_alert(aps.alert),
|
||||
'badge': _Validators.check_number('Aps.badge', aps.badge),
|
||||
'sound': cls.encode_aps_sound(aps.sound),
|
||||
'category': _Validators.check_string('Aps.category', aps.category),
|
||||
'thread-id': _Validators.check_string('Aps.thread_id', aps.thread_id),
|
||||
}
|
||||
if aps.content_available is True:
|
||||
result['content-available'] = 1
|
||||
if aps.mutable_content is True:
|
||||
result['mutable-content'] = 1
|
||||
if aps.custom_data is not None:
|
||||
if not isinstance(aps.custom_data, dict):
|
||||
raise ValueError('Aps.custom_data must be a dict.')
|
||||
for key, val in aps.custom_data.items():
|
||||
_Validators.check_string('Aps.custom_data key', key)
|
||||
if key in result:
|
||||
raise ValueError('Multiple specifications for {0} in Aps.'.format(key))
|
||||
result[key] = val
|
||||
return cls.remove_null_values(result)
|
||||
|
||||
@classmethod
|
||||
def encode_aps_sound(cls, sound):
|
||||
"""Encodes an APNs sound configuration into JSON."""
|
||||
if sound is None:
|
||||
return None
|
||||
if sound and isinstance(sound, str):
|
||||
return sound
|
||||
if not isinstance(sound, _messaging_utils.CriticalSound):
|
||||
raise ValueError(
|
||||
'Aps.sound must be a non-empty string or an instance of CriticalSound class.')
|
||||
result = {
|
||||
'name': _Validators.check_string('CriticalSound.name', sound.name, non_empty=True),
|
||||
'volume': _Validators.check_number('CriticalSound.volume', sound.volume),
|
||||
}
|
||||
if sound.critical:
|
||||
result['critical'] = 1
|
||||
if not result['name']:
|
||||
raise ValueError('CriticalSond.name must be a non-empty string.')
|
||||
volume = result['volume']
|
||||
if volume is not None and (volume < 0 or volume > 1):
|
||||
raise ValueError('CriticalSound.volume must be in the interval [0,1].')
|
||||
return cls.remove_null_values(result)
|
||||
|
||||
@classmethod
|
||||
def encode_aps_alert(cls, alert):
|
||||
"""Encodes an ``ApsAlert`` instance into JSON."""
|
||||
if alert is None:
|
||||
return None
|
||||
if isinstance(alert, str):
|
||||
return alert
|
||||
if not isinstance(alert, _messaging_utils.ApsAlert):
|
||||
raise ValueError('Aps.alert must be a string or an instance of ApsAlert class.')
|
||||
result = {
|
||||
'title': _Validators.check_string('ApsAlert.title', alert.title),
|
||||
'subtitle': _Validators.check_string('ApsAlert.subtitle', alert.subtitle),
|
||||
'body': _Validators.check_string('ApsAlert.body', alert.body),
|
||||
'title-loc-key': _Validators.check_string(
|
||||
'ApsAlert.title_loc_key', alert.title_loc_key),
|
||||
'title-loc-args': _Validators.check_string_list(
|
||||
'ApsAlert.title_loc_args', alert.title_loc_args),
|
||||
'loc-key': _Validators.check_string(
|
||||
'ApsAlert.loc_key', alert.loc_key),
|
||||
'loc-args': _Validators.check_string_list(
|
||||
'ApsAlert.loc_args', alert.loc_args),
|
||||
'action-loc-key': _Validators.check_string(
|
||||
'ApsAlert.action_loc_key', alert.action_loc_key),
|
||||
'launch-image': _Validators.check_string(
|
||||
'ApsAlert.launch_image', alert.launch_image),
|
||||
}
|
||||
if result.get('loc-args') and not result.get('loc-key'):
|
||||
raise ValueError(
|
||||
'ApsAlert.loc_key is required when specifying loc_args.')
|
||||
if result.get('title-loc-args') and not result.get('title-loc-key'):
|
||||
raise ValueError(
|
||||
'ApsAlert.title_loc_key is required when specifying title_loc_args.')
|
||||
if alert.custom_data is not None:
|
||||
if not isinstance(alert.custom_data, dict):
|
||||
raise ValueError('ApsAlert.custom_data must be a dict.')
|
||||
for key, val in alert.custom_data.items():
|
||||
_Validators.check_string('ApsAlert.custom_data key', key)
|
||||
# allow specifying key override because Apple could update API so that key
|
||||
# could have unexpected value type
|
||||
result[key] = val
|
||||
return cls.remove_null_values(result)
|
||||
|
||||
@classmethod
|
||||
def encode_notification(cls, notification):
|
||||
"""Encodes a ``Notification`` instance into JSON."""
|
||||
if notification is None:
|
||||
return None
|
||||
if not isinstance(notification, _messaging_utils.Notification):
|
||||
raise ValueError('Message.notification must be an instance of Notification class.')
|
||||
result = {
|
||||
'body': _Validators.check_string('Notification.body', notification.body),
|
||||
'title': _Validators.check_string('Notification.title', notification.title),
|
||||
'image': _Validators.check_string('Notification.image', notification.image)
|
||||
}
|
||||
return cls.remove_null_values(result)
|
||||
|
||||
@classmethod
|
||||
def sanitize_topic_name(cls, topic):
|
||||
"""Removes the /topics/ prefix from the topic name, if present."""
|
||||
if not topic:
|
||||
return None
|
||||
prefix = '/topics/'
|
||||
if topic.startswith(prefix):
|
||||
topic = topic[len(prefix):]
|
||||
# Checks for illegal characters and empty string.
|
||||
if not re.match(r'^[a-zA-Z0-9-_\.~%]+$', topic):
|
||||
raise ValueError('Malformed topic name.')
|
||||
return topic
|
||||
|
||||
def default(self, o): # pylint: disable=method-hidden
|
||||
if not isinstance(o, Message):
|
||||
return json.JSONEncoder.default(self, o)
|
||||
result = {
|
||||
'android': MessageEncoder.encode_android(o.android),
|
||||
'apns': MessageEncoder.encode_apns(o.apns),
|
||||
'condition': _Validators.check_string(
|
||||
'Message.condition', o.condition, non_empty=True),
|
||||
'data': _Validators.check_string_dict('Message.data', o.data),
|
||||
'notification': MessageEncoder.encode_notification(o.notification),
|
||||
'token': _Validators.check_string('Message.token', o.token, non_empty=True),
|
||||
'topic': _Validators.check_string('Message.topic', o.topic, non_empty=True),
|
||||
'webpush': MessageEncoder.encode_webpush(o.webpush),
|
||||
'fcm_options': MessageEncoder.encode_fcm_options(o.fcm_options),
|
||||
}
|
||||
result['topic'] = MessageEncoder.sanitize_topic_name(result.get('topic'))
|
||||
result = MessageEncoder.remove_null_values(result)
|
||||
target_count = sum([t in result for t in ['token', 'topic', 'condition']])
|
||||
if target_count != 1:
|
||||
raise ValueError('Exactly one of token, topic or condition must be specified.')
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def encode_fcm_options(cls, fcm_options):
|
||||
"""Encodes an ``FCMOptions`` instance into JSON."""
|
||||
if fcm_options is None:
|
||||
return None
|
||||
if not isinstance(fcm_options, _messaging_utils.FCMOptions):
|
||||
raise ValueError('Message.fcm_options must be an instance of FCMOptions class.')
|
||||
result = {
|
||||
'analytics_label': _Validators.check_analytics_label(
|
||||
'FCMOptions.analytics_label', fcm_options.analytics_label),
|
||||
}
|
||||
result = cls.remove_null_values(result)
|
||||
return result
|
494
venv/Lib/site-packages/firebase_admin/_messaging_utils.py
Normal file
494
venv/Lib/site-packages/firebase_admin/_messaging_utils.py
Normal file
|
@ -0,0 +1,494 @@
|
|||
# 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.
|
||||
|
||||
"""Types and utilities used by the messaging (FCM) module."""
|
||||
|
||||
from firebase_admin import exceptions
|
||||
|
||||
|
||||
class Notification:
|
||||
"""A notification that can be included in a message.
|
||||
|
||||
Args:
|
||||
title: Title of the notification (optional).
|
||||
body: Body of the notification (optional).
|
||||
image: Image url of the notification (optional)
|
||||
"""
|
||||
|
||||
def __init__(self, title=None, body=None, image=None):
|
||||
self.title = title
|
||||
self.body = body
|
||||
self.image = image
|
||||
|
||||
|
||||
class AndroidConfig:
|
||||
"""Android-specific options that can be included in a message.
|
||||
|
||||
Args:
|
||||
collapse_key: Collapse key string for the message (optional). This is an identifier for a
|
||||
group of messages that can be collapsed, so that only the last message is sent when
|
||||
delivery can be resumed. A maximum of 4 different collapse keys may be active at a
|
||||
given time.
|
||||
priority: Priority of the message (optional). Must be one of ``high`` or ``normal``.
|
||||
ttl: The time-to-live duration of the message (optional). This can be specified
|
||||
as a numeric seconds value or a ``datetime.timedelta`` instance.
|
||||
restricted_package_name: The package name of the application where the registration tokens
|
||||
must match in order to receive the message (optional).
|
||||
data: A dictionary of data fields (optional). All keys and values in the dictionary must be
|
||||
strings. When specified, overrides any data fields set via ``Message.data``.
|
||||
notification: A ``messaging.AndroidNotification`` to be included in the message (optional).
|
||||
fcm_options: A ``messaging.AndroidFCMOptions`` to be included in the message (optional).
|
||||
"""
|
||||
|
||||
def __init__(self, collapse_key=None, priority=None, ttl=None, restricted_package_name=None,
|
||||
data=None, notification=None, fcm_options=None):
|
||||
self.collapse_key = collapse_key
|
||||
self.priority = priority
|
||||
self.ttl = ttl
|
||||
self.restricted_package_name = restricted_package_name
|
||||
self.data = data
|
||||
self.notification = notification
|
||||
self.fcm_options = fcm_options
|
||||
|
||||
|
||||
class AndroidNotification:
|
||||
"""Android-specific notification parameters.
|
||||
|
||||
Args:
|
||||
title: Title of the notification (optional). If specified, overrides the title set via
|
||||
``messaging.Notification``.
|
||||
body: Body of the notification (optional). If specified, overrides the body set via
|
||||
``messaging.Notification``.
|
||||
icon: Icon of the notification (optional).
|
||||
color: Color of the notification icon expressed in ``#rrggbb`` form (optional).
|
||||
sound: Sound to be played when the device receives the notification (optional). This is
|
||||
usually the file name of the sound resource.
|
||||
tag: Tag of the notification (optional). This is an identifier used to replace existing
|
||||
notifications in the notification drawer. If not specified, each request creates a new
|
||||
notification.
|
||||
click_action: The action associated with a user click on the notification (optional). If
|
||||
specified, an activity with a matching intent filter is launched when a user clicks on
|
||||
the notification.
|
||||
body_loc_key: Key of the body string in the app's string resources to use to localize the
|
||||
body text (optional).
|
||||
body_loc_args: A list of resource keys that will be used in place of the format specifiers
|
||||
in ``body_loc_key`` (optional).
|
||||
title_loc_key: Key of the title string in the app's string resources to use to localize the
|
||||
title text (optional).
|
||||
title_loc_args: A list of resource keys that will be used in place of the format specifiers
|
||||
in ``title_loc_key`` (optional).
|
||||
channel_id: channel_id of the notification (optional).
|
||||
image: Image url of the notification (optional).
|
||||
ticker: Sets the ``ticker`` text, which is sent to accessibility services. Prior to API
|
||||
level 21 (Lollipop), sets the text that is displayed in the status bar when the
|
||||
notification first arrives (optional).
|
||||
sticky: When set to ``False`` or unset, the notification is automatically dismissed when the
|
||||
user clicks it in the panel. When set to ``True``, the notification persists even when
|
||||
the user clicks it (optional).
|
||||
event_timestamp: For notifications that inform users about events with an absolute time
|
||||
reference, sets the time that the event in the notification occurred as a
|
||||
``datetime.datetime`` instance. If the ``datetime.datetime`` instance is naive, it
|
||||
defaults to be in the UTC timezone. Notifications in the panel are sorted by this time
|
||||
(optional).
|
||||
local_only: Sets whether or not this notification is relevant only to the current device.
|
||||
Some notifications can be bridged to other devices for remote display, such as a Wear OS
|
||||
watch. This hint can be set to recommend this notification not be bridged (optional).
|
||||
See Wear OS guides:
|
||||
https://developer.android.com/training/wearables/notifications/bridger#existing-method-of-preventing-bridging
|
||||
priority: Sets the relative priority for this notification. Low-priority notifications may
|
||||
be hidden from the user in certain situations. Note this priority differs from
|
||||
``AndroidMessagePriority``. This priority is processed by the client after the message
|
||||
has been delivered. Whereas ``AndroidMessagePriority`` is an FCM concept that controls
|
||||
when the message is delivered (optional). Must be one of ``default``, ``min``, ``low``,
|
||||
``high``, ``max`` or ``normal``.
|
||||
vibrate_timings_millis: Sets the vibration pattern to use. Pass in an array of milliseconds
|
||||
to turn the vibrator on or off. The first value indicates the duration to wait before
|
||||
turning the vibrator on. The next value indicates the duration to keep the vibrator on.
|
||||
Subsequent values alternate between duration to turn the vibrator off and to turn the
|
||||
vibrator on. If ``vibrate_timings`` is set and ``default_vibrate_timings`` is set to
|
||||
``True``, the default value is used instead of the user-specified ``vibrate_timings``.
|
||||
default_vibrate_timings: If set to ``True``, use the Android framework's default vibrate
|
||||
pattern for the notification (optional). Default values are specified in ``config.xml``
|
||||
https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml.
|
||||
If ``default_vibrate_timings`` is set to ``True`` and ``vibrate_timings`` is also set,
|
||||
the default value is used instead of the user-specified ``vibrate_timings``.
|
||||
default_sound: If set to ``True``, use the Android framework's default sound for the
|
||||
notification (optional). Default values are specified in ``config.xml``
|
||||
https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml
|
||||
light_settings: Settings to control the notification's LED blinking rate and color if LED is
|
||||
available on the device. The total blinking time is controlled by the OS (optional).
|
||||
default_light_settings: If set to ``True``, use the Android framework's default LED light
|
||||
settings for the notification. Default values are specified in ``config.xml``
|
||||
https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml.
|
||||
If ``default_light_settings`` is set to ``True`` and ``light_settings`` is also set, the
|
||||
user-specified ``light_settings`` is used instead of the default value.
|
||||
visibility: Sets the visibility of the notification. Must be either ``private``, ``public``,
|
||||
or ``secret``. If unspecified, default to ``private``.
|
||||
notification_count: Sets the number of items this notification represents. May be displayed
|
||||
as a badge count for Launchers that support badging. See ``NotificationBadge``
|
||||
https://developer.android.com/training/notify-user/badges. For example, this might be
|
||||
useful if you're using just one notification to represent multiple new messages but you
|
||||
want the count here to represent the number of total new messages. If zero or
|
||||
unspecified, systems that support badging use the default, which is to increment a
|
||||
number displayed on the long-press menu each time a new notification arrives.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, title=None, body=None, icon=None, color=None, sound=None, tag=None,
|
||||
click_action=None, body_loc_key=None, body_loc_args=None, title_loc_key=None,
|
||||
title_loc_args=None, channel_id=None, image=None, ticker=None, sticky=None,
|
||||
event_timestamp=None, local_only=None, priority=None, vibrate_timings_millis=None,
|
||||
default_vibrate_timings=None, default_sound=None, light_settings=None,
|
||||
default_light_settings=None, visibility=None, notification_count=None):
|
||||
self.title = title
|
||||
self.body = body
|
||||
self.icon = icon
|
||||
self.color = color
|
||||
self.sound = sound
|
||||
self.tag = tag
|
||||
self.click_action = click_action
|
||||
self.body_loc_key = body_loc_key
|
||||
self.body_loc_args = body_loc_args
|
||||
self.title_loc_key = title_loc_key
|
||||
self.title_loc_args = title_loc_args
|
||||
self.channel_id = channel_id
|
||||
self.image = image
|
||||
self.ticker = ticker
|
||||
self.sticky = sticky
|
||||
self.event_timestamp = event_timestamp
|
||||
self.local_only = local_only
|
||||
self.priority = priority
|
||||
self.vibrate_timings_millis = vibrate_timings_millis
|
||||
self.default_vibrate_timings = default_vibrate_timings
|
||||
self.default_sound = default_sound
|
||||
self.light_settings = light_settings
|
||||
self.default_light_settings = default_light_settings
|
||||
self.visibility = visibility
|
||||
self.notification_count = notification_count
|
||||
|
||||
|
||||
class LightSettings:
|
||||
"""Represents settings to control notification LED that can be included in a
|
||||
``messaging.AndroidNotification``.
|
||||
|
||||
Args:
|
||||
color: Sets the color of the LED in ``#rrggbb`` or ``#rrggbbaa`` format.
|
||||
light_on_duration_millis: Along with ``light_off_duration``, defines the blink rate of LED
|
||||
flashes.
|
||||
light_off_duration_millis: Along with ``light_on_duration``, defines the blink rate of LED
|
||||
flashes.
|
||||
"""
|
||||
def __init__(self, color, light_on_duration_millis,
|
||||
light_off_duration_millis):
|
||||
self.color = color
|
||||
self.light_on_duration_millis = light_on_duration_millis
|
||||
self.light_off_duration_millis = light_off_duration_millis
|
||||
|
||||
|
||||
class AndroidFCMOptions:
|
||||
"""Options for features provided by the FCM SDK for Android.
|
||||
|
||||
Args:
|
||||
analytics_label: contains additional options for features provided by the FCM Android SDK
|
||||
(optional).
|
||||
"""
|
||||
|
||||
def __init__(self, analytics_label=None):
|
||||
self.analytics_label = analytics_label
|
||||
|
||||
|
||||
class WebpushConfig:
|
||||
"""Webpush-specific options that can be included in a message.
|
||||
|
||||
Args:
|
||||
headers: A dictionary of headers (optional). Refer `Webpush Specification`_ for supported
|
||||
headers.
|
||||
data: A dictionary of data fields (optional). All keys and values in the dictionary must be
|
||||
strings. When specified, overrides any data fields set via ``Message.data``.
|
||||
notification: A ``messaging.WebpushNotification`` to be included in the message (optional).
|
||||
fcm_options: A ``messaging.WebpushFCMOptions`` instance to be included in the message
|
||||
(optional).
|
||||
|
||||
.. _Webpush Specification: https://tools.ietf.org/html/rfc8030#section-5
|
||||
"""
|
||||
|
||||
def __init__(self, headers=None, data=None, notification=None, fcm_options=None):
|
||||
self.headers = headers
|
||||
self.data = data
|
||||
self.notification = notification
|
||||
self.fcm_options = fcm_options
|
||||
|
||||
|
||||
class WebpushNotificationAction:
|
||||
"""An action available to the users when the notification is presented.
|
||||
|
||||
Args:
|
||||
action: Action string.
|
||||
title: Title string.
|
||||
icon: Icon URL for the action (optional).
|
||||
"""
|
||||
|
||||
def __init__(self, action, title, icon=None):
|
||||
self.action = action
|
||||
self.title = title
|
||||
self.icon = icon
|
||||
|
||||
|
||||
class WebpushNotification:
|
||||
"""Webpush-specific notification parameters.
|
||||
|
||||
Refer to the `Notification Reference`_ for more information.
|
||||
|
||||
Args:
|
||||
title: Title of the notification (optional). If specified, overrides the title set via
|
||||
``messaging.Notification``.
|
||||
body: Body of the notification (optional). If specified, overrides the body set via
|
||||
``messaging.Notification``.
|
||||
icon: Icon URL of the notification (optional).
|
||||
actions: A list of ``messaging.WebpushNotificationAction`` instances (optional).
|
||||
badge: URL of the image used to represent the notification when there is
|
||||
not enough space to display the notification itself (optional).
|
||||
data: Any arbitrary JSON data that should be associated with the notification (optional).
|
||||
direction: The direction in which to display the notification (optional). Must be either
|
||||
'auto', 'ltr' or 'rtl'.
|
||||
image: The URL of an image to be displayed in the notification (optional).
|
||||
language: Notification language (optional).
|
||||
renotify: A boolean indicating whether the user should be notified after a new
|
||||
notification replaces an old one (optional).
|
||||
require_interaction: A boolean indicating whether a notification should remain active
|
||||
until the user clicks or dismisses it, rather than closing automatically (optional).
|
||||
silent: ``True`` to indicate that the notification should be silent (optional).
|
||||
tag: An identifying tag on the notification (optional).
|
||||
timestamp_millis: A timestamp value in milliseconds on the notification (optional).
|
||||
vibrate: A vibration pattern for the device's vibration hardware to emit when the
|
||||
notification fires (optional). The pattern is specified as an integer array.
|
||||
custom_data: A dict of custom key-value pairs to be included in the notification
|
||||
(optional)
|
||||
|
||||
.. _Notification Reference: https://developer.mozilla.org/en-US/docs/Web/API\
|
||||
/notification/Notification
|
||||
"""
|
||||
|
||||
def __init__(self, title=None, body=None, icon=None, actions=None, badge=None, data=None,
|
||||
direction=None, image=None, language=None, renotify=None,
|
||||
require_interaction=None, silent=None, tag=None, timestamp_millis=None,
|
||||
vibrate=None, custom_data=None):
|
||||
self.title = title
|
||||
self.body = body
|
||||
self.icon = icon
|
||||
self.actions = actions
|
||||
self.badge = badge
|
||||
self.data = data
|
||||
self.direction = direction
|
||||
self.image = image
|
||||
self.language = language
|
||||
self.renotify = renotify
|
||||
self.require_interaction = require_interaction
|
||||
self.silent = silent
|
||||
self.tag = tag
|
||||
self.timestamp_millis = timestamp_millis
|
||||
self.vibrate = vibrate
|
||||
self.custom_data = custom_data
|
||||
|
||||
|
||||
class WebpushFCMOptions:
|
||||
"""Options for features provided by the FCM SDK for Web.
|
||||
|
||||
Args:
|
||||
link: The link to open when the user clicks on the notification. Must be an HTTPS URL
|
||||
(optional).
|
||||
"""
|
||||
|
||||
def __init__(self, link=None):
|
||||
self.link = link
|
||||
|
||||
|
||||
class APNSConfig:
|
||||
"""APNS-specific options that can be included in a message.
|
||||
|
||||
Refer to `APNS Documentation`_ for more information.
|
||||
|
||||
Args:
|
||||
headers: A dictionary of headers (optional).
|
||||
payload: A ``messaging.APNSPayload`` to be included in the message (optional).
|
||||
fcm_options: A ``messaging.APNSFCMOptions`` instance to be included in the message
|
||||
(optional).
|
||||
|
||||
.. _APNS Documentation: https://developer.apple.com/library/content/documentation\
|
||||
/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html
|
||||
"""
|
||||
|
||||
def __init__(self, headers=None, payload=None, fcm_options=None):
|
||||
self.headers = headers
|
||||
self.payload = payload
|
||||
self.fcm_options = fcm_options
|
||||
|
||||
|
||||
class APNSPayload:
|
||||
"""Payload of an APNS message.
|
||||
|
||||
Args:
|
||||
aps: A ``messaging.Aps`` instance to be included in the payload.
|
||||
kwargs: Arbitrary keyword arguments to be included as custom fields in the payload
|
||||
(optional).
|
||||
"""
|
||||
|
||||
def __init__(self, aps, **kwargs):
|
||||
self.aps = aps
|
||||
self.custom_data = kwargs
|
||||
|
||||
|
||||
class Aps:
|
||||
"""Aps dictionary to be included in an APNS payload.
|
||||
|
||||
Args:
|
||||
alert: A string or a ``messaging.ApsAlert`` instance (optional).
|
||||
badge: A number representing the badge to be displayed with the message (optional).
|
||||
sound: Name of the sound file to be played with the message or a
|
||||
``messaging.CriticalSound`` instance (optional).
|
||||
content_available: A boolean indicating whether to configure a background update
|
||||
notification (optional).
|
||||
category: String identifier representing the message type (optional).
|
||||
thread_id: An app-specific string identifier for grouping messages (optional).
|
||||
mutable_content: A boolean indicating whether to support mutating notifications at
|
||||
the client using app extensions (optional).
|
||||
custom_data: A dict of custom key-value pairs to be included in the Aps dictionary
|
||||
(optional).
|
||||
"""
|
||||
|
||||
def __init__(self, alert=None, badge=None, sound=None, content_available=None, category=None,
|
||||
thread_id=None, mutable_content=None, custom_data=None):
|
||||
self.alert = alert
|
||||
self.badge = badge
|
||||
self.sound = sound
|
||||
self.content_available = content_available
|
||||
self.category = category
|
||||
self.thread_id = thread_id
|
||||
self.mutable_content = mutable_content
|
||||
self.custom_data = custom_data
|
||||
|
||||
|
||||
class CriticalSound:
|
||||
"""Critical alert sound configuration that can be included in ``messaging.Aps``.
|
||||
|
||||
Args:
|
||||
name: The name of a sound file in your app's main bundle or in the ``Library/Sounds``
|
||||
folder of your app's container directory. Specify the string ``default`` to play the
|
||||
system sound.
|
||||
critical: Set to ``True`` to set the critical alert flag on the sound configuration
|
||||
(optional).
|
||||
volume: The volume for the critical alert's sound. Must be a value between 0.0 (silent)
|
||||
and 1.0 (full volume) (optional).
|
||||
"""
|
||||
|
||||
def __init__(self, name, critical=None, volume=None):
|
||||
self.name = name
|
||||
self.critical = critical
|
||||
self.volume = volume
|
||||
|
||||
|
||||
class ApsAlert:
|
||||
"""An alert that can be included in ``messaging.Aps``.
|
||||
|
||||
Args:
|
||||
title: Title of the alert (optional). If specified, overrides the title set via
|
||||
``messaging.Notification``.
|
||||
subtitle: Subtitle of the alert (optional).
|
||||
body: Body of the alert (optional). If specified, overrides the body set via
|
||||
``messaging.Notification``.
|
||||
loc_key: Key of the body string in the app's string resources to use to localize the
|
||||
body text (optional).
|
||||
loc_args: A list of resource keys that will be used in place of the format specifiers
|
||||
in ``loc_key`` (optional).
|
||||
title_loc_key: Key of the title string in the app's string resources to use to localize the
|
||||
title text (optional).
|
||||
title_loc_args: A list of resource keys that will be used in place of the format specifiers
|
||||
in ``title_loc_key`` (optional).
|
||||
action_loc_key: Key of the text in the app's string resources to use to localize the
|
||||
action button text (optional).
|
||||
launch_image: Image for the notification action (optional).
|
||||
custom_data: A dict of custom key-value pairs to be included in the ApsAlert dictionary
|
||||
(optional)
|
||||
"""
|
||||
|
||||
def __init__(self, title=None, subtitle=None, body=None, loc_key=None, loc_args=None,
|
||||
title_loc_key=None, title_loc_args=None, action_loc_key=None, launch_image=None,
|
||||
custom_data=None):
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.body = body
|
||||
self.loc_key = loc_key
|
||||
self.loc_args = loc_args
|
||||
self.title_loc_key = title_loc_key
|
||||
self.title_loc_args = title_loc_args
|
||||
self.action_loc_key = action_loc_key
|
||||
self.launch_image = launch_image
|
||||
self.custom_data = custom_data
|
||||
|
||||
|
||||
class APNSFCMOptions:
|
||||
"""Options for features provided by the FCM SDK for iOS.
|
||||
|
||||
Args:
|
||||
analytics_label: contains additional options for features provided by the FCM iOS SDK
|
||||
(optional).
|
||||
image: contains the URL of an image that is going to be displayed in a notification
|
||||
(optional).
|
||||
"""
|
||||
|
||||
def __init__(self, analytics_label=None, image=None):
|
||||
self.analytics_label = analytics_label
|
||||
self.image = image
|
||||
|
||||
|
||||
class FCMOptions:
|
||||
"""Options for features provided by SDK.
|
||||
|
||||
Args:
|
||||
analytics_label: contains additional options to use across all platforms (optional).
|
||||
"""
|
||||
|
||||
def __init__(self, analytics_label=None):
|
||||
self.analytics_label = analytics_label
|
||||
|
||||
|
||||
class ThirdPartyAuthError(exceptions.UnauthenticatedError):
|
||||
"""APNs certificate or web push auth key was invalid or missing."""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
exceptions.UnauthenticatedError.__init__(self, message, cause, http_response)
|
||||
|
||||
|
||||
class QuotaExceededError(exceptions.ResourceExhaustedError):
|
||||
"""Sending limit exceeded for the message target."""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
exceptions.ResourceExhaustedError.__init__(self, message, cause, http_response)
|
||||
|
||||
|
||||
class SenderIdMismatchError(exceptions.PermissionDeniedError):
|
||||
"""The authenticated sender ID is different from the sender ID for the registration token."""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
exceptions.PermissionDeniedError.__init__(self, message, cause, http_response)
|
||||
|
||||
|
||||
class UnregisteredError(exceptions.NotFoundError):
|
||||
"""App instance was unregistered from FCM.
|
||||
|
||||
This usually means that the token used is no longer valid and a new one must be used."""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
exceptions.NotFoundError.__init__(self, message, cause, http_response)
|
87
venv/Lib/site-packages/firebase_admin/_rfc3339.py
Normal file
87
venv/Lib/site-packages/firebase_admin/_rfc3339.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
# Copyright 2020 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.
|
||||
|
||||
"""Parse RFC3339 date strings"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
import re
|
||||
|
||||
def parse_to_epoch(datestr):
|
||||
"""Parse an RFC3339 date string and return the number of seconds since the
|
||||
epoch (as a float).
|
||||
|
||||
In particular, this method is meant to parse the strings returned by the
|
||||
JSON mapping of protobuf google.protobuf.timestamp.Timestamp instances:
|
||||
https://github.com/protocolbuffers/protobuf/blob/4cf5bfee9546101d98754d23ff378ff718ba8438/src/google/protobuf/timestamp.proto#L99
|
||||
|
||||
This method has microsecond precision; nanoseconds will be truncated.
|
||||
|
||||
Args:
|
||||
datestr: A string in RFC3339 format.
|
||||
Returns:
|
||||
Float: The number of seconds since the Unix epoch.
|
||||
Raises:
|
||||
ValueError: Raised if the `datestr` is not a valid RFC3339 date string.
|
||||
"""
|
||||
return _parse_to_datetime(datestr).timestamp()
|
||||
|
||||
|
||||
def _parse_to_datetime(datestr):
|
||||
"""Parse an RFC3339 date string and return a python datetime instance.
|
||||
|
||||
Args:
|
||||
datestr: A string in RFC3339 format.
|
||||
Returns:
|
||||
datetime: The corresponding `datetime` (with timezone information).
|
||||
Raises:
|
||||
ValueError: Raised if the `datestr` is not a valid RFC3339 date string.
|
||||
"""
|
||||
# If more than 6 digits appear in the fractional seconds position, truncate
|
||||
# to just the most significant 6. (i.e. we only have microsecond precision;
|
||||
# nanos are truncated.)
|
||||
datestr_modified = re.sub(r'(\.\d{6})\d*', r'\1', datestr)
|
||||
|
||||
# This format is the one we actually expect to occur from our backend. The
|
||||
# others are only present because the spec says we *should* accept them.
|
||||
try:
|
||||
return datetime.strptime(
|
||||
datestr_modified, '%Y-%m-%dT%H:%M:%S.%fZ'
|
||||
).replace(tzinfo=timezone.utc)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return datetime.strptime(
|
||||
datestr_modified, '%Y-%m-%dT%H:%M:%SZ'
|
||||
).replace(tzinfo=timezone.utc)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Note: %z parses timezone offsets, but requires the timezone offset *not*
|
||||
# include a separating ':'. As of python 3.7, this was relaxed.
|
||||
# TODO(rsgowman): Once python3.7 becomes our floor, we can drop the regex
|
||||
# replacement.
|
||||
datestr_modified = re.sub(r'(\d\d):(\d\d)$', r'\1\2', datestr_modified)
|
||||
|
||||
try:
|
||||
return datetime.strptime(datestr_modified, '%Y-%m-%dT%H:%M:%S.%f%z')
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return datetime.strptime(datestr_modified, '%Y-%m-%dT%H:%M:%S%z')
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
raise ValueError('time data {0} does not match RFC3339 format'.format(datestr))
|
208
venv/Lib/site-packages/firebase_admin/_sseclient.py
Normal file
208
venv/Lib/site-packages/firebase_admin/_sseclient.py
Normal file
|
@ -0,0 +1,208 @@
|
|||
# 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.
|
||||
|
||||
"""SSEClient module to stream realtime updates from the Firebase Database.
|
||||
|
||||
Based on a similar implementation from Pyrebase.
|
||||
"""
|
||||
|
||||
import re
|
||||
import time
|
||||
import warnings
|
||||
|
||||
from google.auth import transport
|
||||
import requests
|
||||
|
||||
|
||||
# Technically, we should support streams that mix line endings. This regex,
|
||||
# however, assumes that a system will provide consistent line endings.
|
||||
end_of_field = re.compile(r'\r\n\r\n|\r\r|\n\n')
|
||||
|
||||
|
||||
class KeepAuthSession(transport.requests.AuthorizedSession):
|
||||
"""A session that does not drop authentication on redirects between domains."""
|
||||
|
||||
def __init__(self, credential):
|
||||
super(KeepAuthSession, self).__init__(credential)
|
||||
|
||||
def rebuild_auth(self, prepared_request, response):
|
||||
pass
|
||||
|
||||
|
||||
class _EventBuffer:
|
||||
"""A helper class for buffering and parsing raw SSE data."""
|
||||
|
||||
def __init__(self):
|
||||
self._buffer = []
|
||||
self._tail = ''
|
||||
|
||||
def append(self, char):
|
||||
self._buffer.append(char)
|
||||
self._tail += char
|
||||
self._tail = self._tail[-4:]
|
||||
|
||||
def truncate(self):
|
||||
head, sep, _ = self.buffer_string.rpartition('\n')
|
||||
rem = head + sep
|
||||
self._buffer = list(rem)
|
||||
self._tail = rem[-4:]
|
||||
|
||||
@property
|
||||
def is_end_of_field(self):
|
||||
last_two_chars = self._tail[-2:]
|
||||
return last_two_chars == '\n\n' or last_two_chars == '\r\r' or self._tail == '\r\n\r\n'
|
||||
|
||||
@property
|
||||
def buffer_string(self):
|
||||
return ''.join(self._buffer)
|
||||
|
||||
|
||||
class SSEClient:
|
||||
"""SSE client implementation."""
|
||||
|
||||
def __init__(self, url, session, retry=3000, **kwargs):
|
||||
"""Initializes the SSEClient.
|
||||
|
||||
Args:
|
||||
url: The remote url to connect to.
|
||||
session: The requests session.
|
||||
retry: The retry interval in milliseconds (optional).
|
||||
**kwargs: Extra kwargs that will be sent to ``requests.get()`` (optional).
|
||||
"""
|
||||
self.url = url
|
||||
self.session = session
|
||||
self.retry = retry
|
||||
self.requests_kwargs = kwargs
|
||||
self.should_connect = True
|
||||
self.last_id = None
|
||||
self.buf = u'' # Keep data here as it streams in
|
||||
|
||||
headers = self.requests_kwargs.get('headers', {})
|
||||
# The SSE spec requires making requests with Cache-Control: no-cache
|
||||
headers['Cache-Control'] = 'no-cache'
|
||||
# The 'Accept' header is not required, but explicit > implicit
|
||||
headers['Accept'] = 'text/event-stream'
|
||||
self.requests_kwargs['headers'] = headers
|
||||
self._connect()
|
||||
|
||||
def close(self):
|
||||
"""Closes the SSEClient instance."""
|
||||
self.should_connect = False
|
||||
self.retry = 0
|
||||
self.resp.close()
|
||||
|
||||
def _connect(self):
|
||||
"""Connects to the server using requests."""
|
||||
if self.should_connect:
|
||||
if self.last_id:
|
||||
self.requests_kwargs['headers']['Last-Event-ID'] = self.last_id
|
||||
self.resp = self.session.get(self.url, stream=True, **self.requests_kwargs)
|
||||
self.resp_iterator = self.resp.iter_content(decode_unicode=True)
|
||||
self.resp.raise_for_status()
|
||||
else:
|
||||
raise StopIteration()
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
if not re.search(end_of_field, self.buf):
|
||||
temp_buffer = _EventBuffer()
|
||||
while not temp_buffer.is_end_of_field:
|
||||
try:
|
||||
nextchar = next(self.resp_iterator)
|
||||
temp_buffer.append(nextchar)
|
||||
except (StopIteration, requests.RequestException):
|
||||
time.sleep(self.retry / 1000.0)
|
||||
self._connect()
|
||||
# The SSE spec only supports resuming from a whole message, so
|
||||
# if we have half a message we should throw it out.
|
||||
temp_buffer.truncate()
|
||||
continue
|
||||
self.buf = temp_buffer.buffer_string
|
||||
|
||||
split = re.split(end_of_field, self.buf)
|
||||
head = split[0]
|
||||
self.buf = '\n\n'.join(split[1:])
|
||||
event = Event.parse(head)
|
||||
|
||||
if event.data == 'credential is no longer valid':
|
||||
self._connect()
|
||||
return None
|
||||
if event.data == 'null':
|
||||
return None
|
||||
|
||||
# If the server requests a specific retry delay, we need to honor it.
|
||||
if event.retry:
|
||||
self.retry = event.retry
|
||||
|
||||
# last_id should only be set if included in the message. It's not
|
||||
# forgotten if a message omits it.
|
||||
if event.event_id:
|
||||
self.last_id = event.event_id
|
||||
return event
|
||||
|
||||
def next(self):
|
||||
return self.__next__()
|
||||
|
||||
|
||||
class Event:
|
||||
"""Event represents the events fired by SSE."""
|
||||
|
||||
sse_line_pattern = re.compile('(?P<name>[^:]*):?( ?(?P<value>.*))?')
|
||||
|
||||
def __init__(self, data='', event_type='message', event_id=None, retry=None):
|
||||
self.data = data
|
||||
self.event_type = event_type
|
||||
self.event_id = event_id
|
||||
self.retry = retry
|
||||
|
||||
@classmethod
|
||||
def parse(cls, raw):
|
||||
"""Given a possibly-multiline string representing an SSE message, parses it
|
||||
and returns an Event object.
|
||||
|
||||
Args:
|
||||
raw: the raw data to parse.
|
||||
|
||||
Returns:
|
||||
Event: A new ``Event`` with the parameters initialized.
|
||||
"""
|
||||
event = cls()
|
||||
for line in raw.split('\n'):
|
||||
match = cls.sse_line_pattern.match(line)
|
||||
if match is None:
|
||||
# Malformed line. Discard but warn.
|
||||
warnings.warn('Invalid SSE line: "%s"' % line, SyntaxWarning)
|
||||
continue
|
||||
|
||||
name = match.groupdict()['name']
|
||||
value = match.groupdict()['value']
|
||||
if name == '':
|
||||
# line began with a ":", so is a comment. Ignore
|
||||
continue
|
||||
if name == 'data':
|
||||
# If we already have some data, then join to it with a newline.
|
||||
# Else this is it.
|
||||
if event.data:
|
||||
event.data = '%s\n%s' % (event.data, value)
|
||||
else:
|
||||
event.data = value
|
||||
elif name == 'event':
|
||||
event.event_type = value
|
||||
elif name == 'id':
|
||||
event.event_id = value
|
||||
elif name == 'retry':
|
||||
event.retry = int(value)
|
||||
return event
|
401
venv/Lib/site-packages/firebase_admin/_token_gen.py
Normal file
401
venv/Lib/site-packages/firebase_admin/_token_gen.py
Normal file
|
@ -0,0 +1,401 @@
|
|||
# Copyright 2018 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 token minting and validation sub module."""
|
||||
|
||||
import datetime
|
||||
import time
|
||||
|
||||
import cachecontrol
|
||||
import requests
|
||||
from google.auth import credentials
|
||||
from google.auth import iam
|
||||
from google.auth import jwt
|
||||
from google.auth import transport
|
||||
import google.auth.exceptions
|
||||
import google.oauth2.id_token
|
||||
import google.oauth2.service_account
|
||||
|
||||
from firebase_admin import exceptions
|
||||
from firebase_admin import _auth_utils
|
||||
|
||||
|
||||
# ID token constants
|
||||
ID_TOKEN_ISSUER_PREFIX = 'https://securetoken.google.com/'
|
||||
ID_TOKEN_CERT_URI = ('https://www.googleapis.com/robot/v1/metadata/x509/'
|
||||
'securetoken@system.gserviceaccount.com')
|
||||
|
||||
# Session cookie constants
|
||||
COOKIE_ISSUER_PREFIX = 'https://session.firebase.google.com/'
|
||||
COOKIE_CERT_URI = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys'
|
||||
MIN_SESSION_COOKIE_DURATION_SECONDS = int(datetime.timedelta(minutes=5).total_seconds())
|
||||
MAX_SESSION_COOKIE_DURATION_SECONDS = int(datetime.timedelta(days=14).total_seconds())
|
||||
|
||||
# Custom token constants
|
||||
MAX_TOKEN_LIFETIME_SECONDS = int(datetime.timedelta(hours=1).total_seconds())
|
||||
FIREBASE_AUDIENCE = ('https://identitytoolkit.googleapis.com/google.'
|
||||
'identity.identitytoolkit.v1.IdentityToolkit')
|
||||
RESERVED_CLAIMS = set([
|
||||
'acr', 'amr', 'at_hash', 'aud', 'auth_time', 'azp', 'cnf', 'c_hash',
|
||||
'exp', 'firebase', 'iat', 'iss', 'jti', 'nbf', 'nonce', 'sub'
|
||||
])
|
||||
METADATA_SERVICE_URL = ('http://metadata.google.internal/computeMetadata/v1/instance/'
|
||||
'service-accounts/default/email')
|
||||
|
||||
|
||||
class _SigningProvider:
|
||||
"""Stores a reference to a google.auth.crypto.Signer."""
|
||||
|
||||
def __init__(self, signer, signer_email):
|
||||
self._signer = signer
|
||||
self._signer_email = signer_email
|
||||
|
||||
@property
|
||||
def signer(self):
|
||||
return self._signer
|
||||
|
||||
@property
|
||||
def signer_email(self):
|
||||
return self._signer_email
|
||||
|
||||
@classmethod
|
||||
def from_credential(cls, google_cred):
|
||||
return _SigningProvider(google_cred.signer, google_cred.signer_email)
|
||||
|
||||
@classmethod
|
||||
def from_iam(cls, request, google_cred, service_account):
|
||||
signer = iam.Signer(request, google_cred, service_account)
|
||||
return _SigningProvider(signer, service_account)
|
||||
|
||||
|
||||
class TokenGenerator:
|
||||
"""Generates custom tokens and session cookies."""
|
||||
|
||||
ID_TOOLKIT_URL = 'https://identitytoolkit.googleapis.com/v1'
|
||||
|
||||
def __init__(self, app, http_client):
|
||||
self.app = app
|
||||
self.http_client = http_client
|
||||
self.request = transport.requests.Request()
|
||||
self.base_url = '{0}/projects/{1}'.format(self.ID_TOOLKIT_URL, app.project_id)
|
||||
self._signing_provider = None
|
||||
|
||||
def _init_signing_provider(self):
|
||||
"""Initializes a signing provider by following the go/firebase-admin-sign protocol."""
|
||||
# If the SDK was initialized with a service account, use it to sign bytes.
|
||||
google_cred = self.app.credential.get_credential()
|
||||
if isinstance(google_cred, google.oauth2.service_account.Credentials):
|
||||
return _SigningProvider.from_credential(google_cred)
|
||||
|
||||
# If the SDK was initialized with a service account email, use it with the IAM service
|
||||
# to sign bytes.
|
||||
service_account = self.app.options.get('serviceAccountId')
|
||||
if service_account:
|
||||
return _SigningProvider.from_iam(self.request, google_cred, service_account)
|
||||
|
||||
# If the SDK was initialized with some other credential type that supports signing
|
||||
# (e.g. GAE credentials), use it to sign bytes.
|
||||
if isinstance(google_cred, credentials.Signing):
|
||||
return _SigningProvider.from_credential(google_cred)
|
||||
|
||||
# Attempt to discover a service account email from the local Metadata service. Use it
|
||||
# with the IAM service to sign bytes.
|
||||
resp = self.request(url=METADATA_SERVICE_URL, headers={'Metadata-Flavor': 'Google'})
|
||||
if resp.status != 200:
|
||||
raise ValueError(
|
||||
'Failed to contact the local metadata service: {0}.'.format(resp.data.decode()))
|
||||
service_account = resp.data.decode()
|
||||
return _SigningProvider.from_iam(self.request, google_cred, service_account)
|
||||
|
||||
@property
|
||||
def signing_provider(self):
|
||||
"""Initializes and returns the SigningProvider instance to be used."""
|
||||
if not self._signing_provider:
|
||||
try:
|
||||
self._signing_provider = self._init_signing_provider()
|
||||
except Exception as error:
|
||||
url = 'https://firebase.google.com/docs/auth/admin/create-custom-tokens'
|
||||
raise ValueError(
|
||||
'Failed to determine service account: {0}. Make sure to initialize the SDK '
|
||||
'with service account credentials or specify a service account ID with '
|
||||
'iam.serviceAccounts.signBlob permission. Please refer to {1} for more '
|
||||
'details on creating custom tokens.'.format(error, url))
|
||||
return self._signing_provider
|
||||
|
||||
def create_custom_token(self, uid, developer_claims=None, tenant_id=None):
|
||||
"""Builds and signs a Firebase custom auth token."""
|
||||
if developer_claims is not None:
|
||||
if not isinstance(developer_claims, dict):
|
||||
raise ValueError('developer_claims must be a dictionary')
|
||||
|
||||
disallowed_keys = set(developer_claims.keys()) & RESERVED_CLAIMS
|
||||
if disallowed_keys:
|
||||
if len(disallowed_keys) > 1:
|
||||
error_message = ('Developer claims {0} are reserved and '
|
||||
'cannot be specified.'.format(
|
||||
', '.join(disallowed_keys)))
|
||||
else:
|
||||
error_message = ('Developer claim {0} is reserved and '
|
||||
'cannot be specified.'.format(
|
||||
', '.join(disallowed_keys)))
|
||||
raise ValueError(error_message)
|
||||
|
||||
if not uid or not isinstance(uid, str) or len(uid) > 128:
|
||||
raise ValueError('uid must be a string between 1 and 128 characters.')
|
||||
|
||||
signing_provider = self.signing_provider
|
||||
now = int(time.time())
|
||||
payload = {
|
||||
'iss': signing_provider.signer_email,
|
||||
'sub': signing_provider.signer_email,
|
||||
'aud': FIREBASE_AUDIENCE,
|
||||
'uid': uid,
|
||||
'iat': now,
|
||||
'exp': now + MAX_TOKEN_LIFETIME_SECONDS,
|
||||
}
|
||||
if tenant_id:
|
||||
payload['tenant_id'] = tenant_id
|
||||
|
||||
if developer_claims is not None:
|
||||
payload['claims'] = developer_claims
|
||||
try:
|
||||
return jwt.encode(signing_provider.signer, payload)
|
||||
except google.auth.exceptions.TransportError as error:
|
||||
msg = 'Failed to sign custom token. {0}'.format(error)
|
||||
raise TokenSignError(msg, error)
|
||||
|
||||
|
||||
def create_session_cookie(self, id_token, expires_in):
|
||||
"""Creates a session cookie from the provided ID token."""
|
||||
id_token = id_token.decode('utf-8') if isinstance(id_token, bytes) else id_token
|
||||
if not isinstance(id_token, str) or not id_token:
|
||||
raise ValueError(
|
||||
'Illegal ID token provided: {0}. ID token must be a non-empty '
|
||||
'string.'.format(id_token))
|
||||
|
||||
if isinstance(expires_in, datetime.timedelta):
|
||||
expires_in = int(expires_in.total_seconds())
|
||||
if isinstance(expires_in, bool) or not isinstance(expires_in, int):
|
||||
raise ValueError('Illegal expiry duration: {0}.'.format(expires_in))
|
||||
if expires_in < MIN_SESSION_COOKIE_DURATION_SECONDS:
|
||||
raise ValueError('Illegal expiry duration: {0}. Duration must be at least {1} '
|
||||
'seconds.'.format(expires_in, MIN_SESSION_COOKIE_DURATION_SECONDS))
|
||||
if expires_in > MAX_SESSION_COOKIE_DURATION_SECONDS:
|
||||
raise ValueError('Illegal expiry duration: {0}. Duration must be at most {1} '
|
||||
'seconds.'.format(expires_in, MAX_SESSION_COOKIE_DURATION_SECONDS))
|
||||
|
||||
url = '{0}:createSessionCookie'.format(self.base_url)
|
||||
payload = {
|
||||
'idToken': id_token,
|
||||
'validDuration': expires_in,
|
||||
}
|
||||
try:
|
||||
body, http_resp = self.http_client.body_and_response('post', url, json=payload)
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise _auth_utils.handle_auth_backend_error(error)
|
||||
else:
|
||||
if not body or not body.get('sessionCookie'):
|
||||
raise _auth_utils.UnexpectedResponseError(
|
||||
'Failed to create session cookie.', http_response=http_resp)
|
||||
return body.get('sessionCookie')
|
||||
|
||||
|
||||
class TokenVerifier:
|
||||
"""Verifies ID tokens and session cookies."""
|
||||
|
||||
def __init__(self, app):
|
||||
session = cachecontrol.CacheControl(requests.Session())
|
||||
self.request = transport.requests.Request(session=session)
|
||||
self.id_token_verifier = _JWTVerifier(
|
||||
project_id=app.project_id, short_name='ID token',
|
||||
operation='verify_id_token()',
|
||||
doc_url='https://firebase.google.com/docs/auth/admin/verify-id-tokens',
|
||||
cert_url=ID_TOKEN_CERT_URI,
|
||||
issuer=ID_TOKEN_ISSUER_PREFIX,
|
||||
invalid_token_error=_auth_utils.InvalidIdTokenError,
|
||||
expired_token_error=ExpiredIdTokenError)
|
||||
self.cookie_verifier = _JWTVerifier(
|
||||
project_id=app.project_id, short_name='session cookie',
|
||||
operation='verify_session_cookie()',
|
||||
doc_url='https://firebase.google.com/docs/auth/admin/verify-id-tokens',
|
||||
cert_url=COOKIE_CERT_URI,
|
||||
issuer=COOKIE_ISSUER_PREFIX,
|
||||
invalid_token_error=InvalidSessionCookieError,
|
||||
expired_token_error=ExpiredSessionCookieError)
|
||||
|
||||
def verify_id_token(self, id_token):
|
||||
return self.id_token_verifier.verify(id_token, self.request)
|
||||
|
||||
def verify_session_cookie(self, cookie):
|
||||
return self.cookie_verifier.verify(cookie, self.request)
|
||||
|
||||
|
||||
class _JWTVerifier:
|
||||
"""Verifies Firebase JWTs (ID tokens or session cookies)."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.project_id = kwargs.pop('project_id')
|
||||
self.short_name = kwargs.pop('short_name')
|
||||
self.operation = kwargs.pop('operation')
|
||||
self.url = kwargs.pop('doc_url')
|
||||
self.cert_url = kwargs.pop('cert_url')
|
||||
self.issuer = kwargs.pop('issuer')
|
||||
if self.short_name[0].lower() in 'aeiou':
|
||||
self.articled_short_name = 'an {0}'.format(self.short_name)
|
||||
else:
|
||||
self.articled_short_name = 'a {0}'.format(self.short_name)
|
||||
self._invalid_token_error = kwargs.pop('invalid_token_error')
|
||||
self._expired_token_error = kwargs.pop('expired_token_error')
|
||||
|
||||
def verify(self, token, request):
|
||||
"""Verifies the signature and data for the provided JWT."""
|
||||
token = token.encode('utf-8') if isinstance(token, str) else token
|
||||
if not isinstance(token, bytes) or not token:
|
||||
raise ValueError(
|
||||
'Illegal {0} provided: {1}. {0} must be a non-empty '
|
||||
'string.'.format(self.short_name, token))
|
||||
|
||||
if not self.project_id:
|
||||
raise ValueError(
|
||||
'Failed to ascertain project ID from the credential or the environment. Project '
|
||||
'ID is required to call {0}. Initialize the app with a credentials.Certificate '
|
||||
'or set your Firebase project ID as an app option. Alternatively set the '
|
||||
'GOOGLE_CLOUD_PROJECT environment variable.'.format(self.operation))
|
||||
|
||||
header, payload = self._decode_unverified(token)
|
||||
issuer = payload.get('iss')
|
||||
audience = payload.get('aud')
|
||||
subject = payload.get('sub')
|
||||
expected_issuer = self.issuer + self.project_id
|
||||
|
||||
project_id_match_msg = (
|
||||
'Make sure the {0} comes from the same Firebase project as the service account used '
|
||||
'to authenticate this SDK.'.format(self.short_name))
|
||||
verify_id_token_msg = (
|
||||
'See {0} for details on how to retrieve {1}.'.format(self.url, self.short_name))
|
||||
|
||||
error_message = None
|
||||
if audience == FIREBASE_AUDIENCE:
|
||||
error_message = (
|
||||
'{0} expects {1}, but was given a custom '
|
||||
'token.'.format(self.operation, self.articled_short_name))
|
||||
elif not header.get('kid'):
|
||||
if header.get('alg') == 'HS256' and payload.get(
|
||||
'v') == 0 and 'uid' in payload.get('d', {}):
|
||||
error_message = (
|
||||
'{0} expects {1}, but was given a legacy custom '
|
||||
'token.'.format(self.operation, self.articled_short_name))
|
||||
else:
|
||||
error_message = 'Firebase {0} has no "kid" claim.'.format(self.short_name)
|
||||
elif header.get('alg') != 'RS256':
|
||||
error_message = (
|
||||
'Firebase {0} has incorrect algorithm. Expected "RS256" but got '
|
||||
'"{1}". {2}'.format(self.short_name, header.get('alg'), verify_id_token_msg))
|
||||
elif audience != self.project_id:
|
||||
error_message = (
|
||||
'Firebase {0} has incorrect "aud" (audience) claim. Expected "{1}" but '
|
||||
'got "{2}". {3} {4}'.format(self.short_name, self.project_id, audience,
|
||||
project_id_match_msg, verify_id_token_msg))
|
||||
elif issuer != expected_issuer:
|
||||
error_message = (
|
||||
'Firebase {0} has incorrect "iss" (issuer) claim. Expected "{1}" but '
|
||||
'got "{2}". {3} {4}'.format(self.short_name, expected_issuer, issuer,
|
||||
project_id_match_msg, verify_id_token_msg))
|
||||
elif subject is None or not isinstance(subject, str):
|
||||
error_message = (
|
||||
'Firebase {0} has no "sub" (subject) claim. '
|
||||
'{1}'.format(self.short_name, verify_id_token_msg))
|
||||
elif not subject:
|
||||
error_message = (
|
||||
'Firebase {0} has an empty string "sub" (subject) claim. '
|
||||
'{1}'.format(self.short_name, verify_id_token_msg))
|
||||
elif len(subject) > 128:
|
||||
error_message = (
|
||||
'Firebase {0} has a "sub" (subject) claim longer than 128 characters. '
|
||||
'{1}'.format(self.short_name, verify_id_token_msg))
|
||||
|
||||
if error_message:
|
||||
raise self._invalid_token_error(error_message)
|
||||
|
||||
try:
|
||||
verified_claims = google.oauth2.id_token.verify_token(
|
||||
token,
|
||||
request=request,
|
||||
audience=self.project_id,
|
||||
certs_url=self.cert_url)
|
||||
verified_claims['uid'] = verified_claims['sub']
|
||||
return verified_claims
|
||||
except google.auth.exceptions.TransportError as error:
|
||||
raise CertificateFetchError(str(error), cause=error)
|
||||
except ValueError as error:
|
||||
if 'Token expired' in str(error):
|
||||
raise self._expired_token_error(str(error), cause=error)
|
||||
raise self._invalid_token_error(str(error), cause=error)
|
||||
|
||||
def _decode_unverified(self, token):
|
||||
try:
|
||||
header = jwt.decode_header(token)
|
||||
payload = jwt.decode(token, verify=False)
|
||||
return header, payload
|
||||
except ValueError as error:
|
||||
raise self._invalid_token_error(str(error), cause=error)
|
||||
|
||||
|
||||
class TokenSignError(exceptions.UnknownError):
|
||||
"""Unexpected error while signing a Firebase custom token."""
|
||||
|
||||
def __init__(self, message, cause):
|
||||
exceptions.UnknownError.__init__(self, message, cause)
|
||||
|
||||
|
||||
class CertificateFetchError(exceptions.UnknownError):
|
||||
"""Failed to fetch some public key certificates required to verify a token."""
|
||||
|
||||
def __init__(self, message, cause):
|
||||
exceptions.UnknownError.__init__(self, message, cause)
|
||||
|
||||
|
||||
class ExpiredIdTokenError(_auth_utils.InvalidIdTokenError):
|
||||
"""The provided ID token is expired."""
|
||||
|
||||
def __init__(self, message, cause):
|
||||
_auth_utils.InvalidIdTokenError.__init__(self, message, cause)
|
||||
|
||||
|
||||
class RevokedIdTokenError(_auth_utils.InvalidIdTokenError):
|
||||
"""The provided ID token has been revoked."""
|
||||
|
||||
def __init__(self, message):
|
||||
_auth_utils.InvalidIdTokenError.__init__(self, message)
|
||||
|
||||
|
||||
class InvalidSessionCookieError(exceptions.InvalidArgumentError):
|
||||
"""The provided string is not a valid Firebase session cookie."""
|
||||
|
||||
def __init__(self, message, cause=None):
|
||||
exceptions.InvalidArgumentError.__init__(self, message, cause)
|
||||
|
||||
|
||||
class ExpiredSessionCookieError(InvalidSessionCookieError):
|
||||
"""The provided session cookie is expired."""
|
||||
|
||||
def __init__(self, message, cause):
|
||||
InvalidSessionCookieError.__init__(self, message, cause)
|
||||
|
||||
|
||||
class RevokedSessionCookieError(InvalidSessionCookieError):
|
||||
"""The provided session cookie has been revoked."""
|
||||
|
||||
def __init__(self, message):
|
||||
InvalidSessionCookieError.__init__(self, message)
|
103
venv/Lib/site-packages/firebase_admin/_user_identifier.py
Normal file
103
venv/Lib/site-packages/firebase_admin/_user_identifier.py
Normal file
|
@ -0,0 +1,103 @@
|
|||
# Copyright 2020 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.
|
||||
|
||||
"""Classes to uniquely identify a user."""
|
||||
|
||||
from firebase_admin import _auth_utils
|
||||
|
||||
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` object.
|
||||
|
||||
Args:
|
||||
uid: A user ID string.
|
||||
"""
|
||||
self._uid = _auth_utils.validate_uid(uid, required=True)
|
||||
|
||||
@property
|
||||
def uid(self):
|
||||
return self._uid
|
||||
|
||||
|
||||
class EmailIdentifier(UserIdentifier):
|
||||
"""Used for looking up an account by email.
|
||||
|
||||
See ``auth.get_user()``.
|
||||
"""
|
||||
|
||||
def __init__(self, email):
|
||||
"""Constructs a new `EmailIdentifier` object.
|
||||
|
||||
Args:
|
||||
email: A user email address string.
|
||||
"""
|
||||
self._email = _auth_utils.validate_email(email, required=True)
|
||||
|
||||
@property
|
||||
def email(self):
|
||||
return self._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` object.
|
||||
|
||||
Args:
|
||||
phone_number: A phone number string.
|
||||
"""
|
||||
self._phone_number = _auth_utils.validate_phone(phone_number, required=True)
|
||||
|
||||
@property
|
||||
def phone_number(self):
|
||||
return self._phone_number
|
||||
|
||||
|
||||
class ProviderIdentifier(UserIdentifier):
|
||||
"""Used for looking up an account by provider.
|
||||
|
||||
See ``auth.get_user()``.
|
||||
"""
|
||||
|
||||
def __init__(self, provider_id, provider_uid):
|
||||
"""Constructs a new `ProviderIdentifier` object.
|
||||
|
||||
Args:
|
||||
provider_id: A provider ID string.
|
||||
provider_uid: A provider UID string.
|
||||
"""
|
||||
self._provider_id = _auth_utils.validate_provider_id(provider_id, required=True)
|
||||
self._provider_uid = _auth_utils.validate_provider_uid(
|
||||
provider_uid, required=True)
|
||||
|
||||
@property
|
||||
def provider_id(self):
|
||||
return self._provider_id
|
||||
|
||||
@property
|
||||
def provider_uid(self):
|
||||
return self._provider_uid
|
520
venv/Lib/site-packages/firebase_admin/_user_import.py
Normal file
520
venv/Lib/site-packages/firebase_admin/_user_import.py
Normal file
|
@ -0,0 +1,520 @@
|
|||
# Copyright 2018 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 import sub module."""
|
||||
|
||||
import base64
|
||||
import json
|
||||
|
||||
from firebase_admin import _auth_utils
|
||||
|
||||
|
||||
def b64_encode(bytes_value):
|
||||
return base64.urlsafe_b64encode(bytes_value).decode()
|
||||
|
||||
|
||||
class UserProvider:
|
||||
"""Represents a user identity provider that can be associated with a Firebase user.
|
||||
|
||||
One or more providers can be specified in an ``ImportUserRecord`` when importing users via
|
||||
``auth.import_users()``.
|
||||
|
||||
Args:
|
||||
uid: User's unique ID assigned by the identity provider.
|
||||
provider_id: ID of the identity provider. This can be a short domain name or the identifier
|
||||
of an OpenID identity provider.
|
||||
email: User's email address (optional).
|
||||
display_name: User's display name (optional).
|
||||
photo_url: User's photo URL (optional).
|
||||
"""
|
||||
|
||||
def __init__(self, uid, provider_id, email=None, display_name=None, photo_url=None):
|
||||
self.uid = uid
|
||||
self.provider_id = provider_id
|
||||
self.email = email
|
||||
self.display_name = display_name
|
||||
self.photo_url = photo_url
|
||||
|
||||
@property
|
||||
def uid(self):
|
||||
return self._uid
|
||||
|
||||
@uid.setter
|
||||
def uid(self, uid):
|
||||
self._uid = _auth_utils.validate_uid(uid, required=True)
|
||||
|
||||
@property
|
||||
def provider_id(self):
|
||||
return self._provider_id
|
||||
|
||||
@provider_id.setter
|
||||
def provider_id(self, provider_id):
|
||||
self._provider_id = _auth_utils.validate_provider_id(provider_id, required=True)
|
||||
|
||||
@property
|
||||
def email(self):
|
||||
return self._email
|
||||
|
||||
@email.setter
|
||||
def email(self, email):
|
||||
self._email = _auth_utils.validate_email(email)
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return self._display_name
|
||||
|
||||
@display_name.setter
|
||||
def display_name(self, display_name):
|
||||
self._display_name = _auth_utils.validate_display_name(display_name)
|
||||
|
||||
@property
|
||||
def photo_url(self):
|
||||
return self._photo_url
|
||||
|
||||
@photo_url.setter
|
||||
def photo_url(self, photo_url):
|
||||
self._photo_url = _auth_utils.validate_photo_url(photo_url)
|
||||
|
||||
def to_dict(self):
|
||||
payload = {
|
||||
'rawId': self.uid,
|
||||
'providerId': self.provider_id,
|
||||
'displayName': self.display_name,
|
||||
'email': self.email,
|
||||
'photoUrl': self.photo_url,
|
||||
}
|
||||
return {k: v for k, v in payload.items() if v is not None}
|
||||
|
||||
|
||||
class ImportUserRecord:
|
||||
"""Represents a user account to be imported to Firebase Auth.
|
||||
|
||||
Must specify the ``uid`` field at a minimum. A sequence of ``ImportUserRecord`` objects can be
|
||||
passed to the ``auth.import_users()`` function, in order to import those users into Firebase
|
||||
Auth in bulk. If the ``password_hash`` is set on a user, a hash configuration must be
|
||||
specified when calling ``import_users()``.
|
||||
|
||||
Args:
|
||||
uid: User's unique ID. Must be a non-empty string not longer than 128 characters.
|
||||
email: User's email address (optional).
|
||||
email_verified: A boolean indicating whether the user's email has been verified (optional).
|
||||
display_name: User's display name (optional).
|
||||
phone_number: User's phone number (optional).
|
||||
photo_url: User's photo URL (optional).
|
||||
disabled: A boolean indicating whether this user account has been disabled (optional).
|
||||
user_metadata: An ``auth.UserMetadata`` instance with additional user metadata (optional).
|
||||
provider_data: A list of ``auth.UserProvider`` instances (optional).
|
||||
custom_claims: A ``dict`` of custom claims to be set on the user account (optional).
|
||||
password_hash: User's password hash as a ``bytes`` sequence (optional).
|
||||
password_salt: User's password salt as a ``bytes`` sequence (optional).
|
||||
|
||||
Raises:
|
||||
ValueError: If provided arguments are invalid.
|
||||
"""
|
||||
|
||||
def __init__(self, uid, email=None, email_verified=None, display_name=None, phone_number=None,
|
||||
photo_url=None, disabled=None, user_metadata=None, provider_data=None,
|
||||
custom_claims=None, password_hash=None, password_salt=None):
|
||||
self.uid = uid
|
||||
self.email = email
|
||||
self.display_name = display_name
|
||||
self.phone_number = phone_number
|
||||
self.photo_url = photo_url
|
||||
self.password_hash = password_hash
|
||||
self.password_salt = password_salt
|
||||
self.email_verified = email_verified
|
||||
self.disabled = disabled
|
||||
self.user_metadata = user_metadata
|
||||
self.provider_data = provider_data
|
||||
self.custom_claims = custom_claims
|
||||
|
||||
@property
|
||||
def uid(self):
|
||||
return self._uid
|
||||
|
||||
@uid.setter
|
||||
def uid(self, uid):
|
||||
self._uid = _auth_utils.validate_uid(uid, required=True)
|
||||
|
||||
@property
|
||||
def email(self):
|
||||
return self._email
|
||||
|
||||
@email.setter
|
||||
def email(self, email):
|
||||
self._email = _auth_utils.validate_email(email)
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return self._display_name
|
||||
|
||||
@display_name.setter
|
||||
def display_name(self, display_name):
|
||||
self._display_name = _auth_utils.validate_display_name(display_name)
|
||||
|
||||
@property
|
||||
def phone_number(self):
|
||||
return self._phone_number
|
||||
|
||||
@phone_number.setter
|
||||
def phone_number(self, phone_number):
|
||||
self._phone_number = _auth_utils.validate_phone(phone_number)
|
||||
|
||||
@property
|
||||
def photo_url(self):
|
||||
return self._photo_url
|
||||
|
||||
@photo_url.setter
|
||||
def photo_url(self, photo_url):
|
||||
self._photo_url = _auth_utils.validate_photo_url(photo_url)
|
||||
|
||||
@property
|
||||
def password_hash(self):
|
||||
return self._password_hash
|
||||
|
||||
@password_hash.setter
|
||||
def password_hash(self, password_hash):
|
||||
self._password_hash = _auth_utils.validate_bytes(password_hash, 'password_hash')
|
||||
|
||||
@property
|
||||
def password_salt(self):
|
||||
return self._password_salt
|
||||
|
||||
@password_salt.setter
|
||||
def password_salt(self, password_salt):
|
||||
self._password_salt = _auth_utils.validate_bytes(password_salt, 'password_salt')
|
||||
|
||||
@property
|
||||
def user_metadata(self):
|
||||
return self._user_metadata
|
||||
|
||||
@user_metadata.setter
|
||||
def user_metadata(self, user_metadata):
|
||||
created_at = user_metadata.creation_timestamp if user_metadata is not None else None
|
||||
last_login_at = user_metadata.last_sign_in_timestamp if user_metadata is not None else None
|
||||
self._created_at = _auth_utils.validate_timestamp(created_at, 'creation_timestamp')
|
||||
self._last_login_at = _auth_utils.validate_timestamp(
|
||||
last_login_at, 'last_sign_in_timestamp')
|
||||
self._user_metadata = user_metadata
|
||||
|
||||
@property
|
||||
def provider_data(self):
|
||||
return self._provider_data
|
||||
|
||||
@provider_data.setter
|
||||
def provider_data(self, provider_data):
|
||||
if provider_data is not None:
|
||||
try:
|
||||
if any([not isinstance(p, UserProvider) for p in provider_data]):
|
||||
raise ValueError('One or more provider data instances are invalid.')
|
||||
except TypeError:
|
||||
raise ValueError('provider_data must be iterable.')
|
||||
self._provider_data = provider_data
|
||||
|
||||
@property
|
||||
def custom_claims(self):
|
||||
return self._custom_claims
|
||||
|
||||
@custom_claims.setter
|
||||
def custom_claims(self, custom_claims):
|
||||
json_claims = json.dumps(custom_claims) if isinstance(
|
||||
custom_claims, dict) else custom_claims
|
||||
self._custom_claims_str = _auth_utils.validate_custom_claims(json_claims)
|
||||
self._custom_claims = custom_claims
|
||||
|
||||
def to_dict(self):
|
||||
"""Returns a dict representation of the user. For internal use only."""
|
||||
payload = {
|
||||
'localId': self.uid,
|
||||
'email': self.email,
|
||||
'displayName': self.display_name,
|
||||
'phoneNumber': self.phone_number,
|
||||
'photoUrl': self.photo_url,
|
||||
'emailVerified': (bool(self.email_verified)
|
||||
if self.email_verified is not None else None),
|
||||
'disabled': bool(self.disabled) if self.disabled is not None else None,
|
||||
'customAttributes': self._custom_claims_str,
|
||||
'createdAt': self._created_at,
|
||||
'lastLoginAt': self._last_login_at,
|
||||
'passwordHash': b64_encode(self.password_hash) if self.password_hash else None,
|
||||
'salt': b64_encode(self.password_salt) if self.password_salt else None,
|
||||
}
|
||||
if self.provider_data:
|
||||
payload['providerUserInfo'] = [p.to_dict() for p in self.provider_data]
|
||||
return {k: v for k, v in payload.items() if v is not None}
|
||||
|
||||
|
||||
class UserImportHash:
|
||||
"""Represents a hash algorithm used to hash user passwords.
|
||||
|
||||
An instance of this class must be specified when importing users with passwords via the
|
||||
``auth.import_users()`` API. Use one of the provided class methods to obtain new
|
||||
instances when required. Refer to `documentation`_ for more details.
|
||||
|
||||
.. _documentation: https://firebase.google.com/docs/auth/admin/import-users
|
||||
"""
|
||||
|
||||
def __init__(self, name, data=None):
|
||||
self._name = name
|
||||
self._data = data
|
||||
|
||||
def to_dict(self):
|
||||
payload = {'hashAlgorithm': self._name}
|
||||
if self._data:
|
||||
payload.update(self._data)
|
||||
return payload
|
||||
|
||||
@classmethod
|
||||
def _hmac(cls, name, key):
|
||||
data = {
|
||||
'signerKey': b64_encode(_auth_utils.validate_bytes(key, 'key', required=True))
|
||||
}
|
||||
return UserImportHash(name, data)
|
||||
|
||||
@classmethod
|
||||
def hmac_sha512(cls, key):
|
||||
"""Creates a new HMAC SHA512 algorithm instance.
|
||||
|
||||
Args:
|
||||
key: Signer key as a byte sequence.
|
||||
|
||||
Returns:
|
||||
UserImportHash: A new ``UserImportHash``.
|
||||
"""
|
||||
return cls._hmac('HMAC_SHA512', key)
|
||||
|
||||
@classmethod
|
||||
def hmac_sha256(cls, key):
|
||||
"""Creates a new HMAC SHA256 algorithm instance.
|
||||
|
||||
Args:
|
||||
key: Signer key as a byte sequence.
|
||||
|
||||
Returns:
|
||||
UserImportHash: A new ``UserImportHash``.
|
||||
"""
|
||||
return cls._hmac('HMAC_SHA256', key)
|
||||
|
||||
@classmethod
|
||||
def hmac_sha1(cls, key):
|
||||
"""Creates a new HMAC SHA1 algorithm instance.
|
||||
|
||||
Args:
|
||||
key: Signer key as a byte sequence.
|
||||
|
||||
Returns:
|
||||
UserImportHash: A new ``UserImportHash``.
|
||||
"""
|
||||
return cls._hmac('HMAC_SHA1', key)
|
||||
|
||||
@classmethod
|
||||
def hmac_md5(cls, key):
|
||||
"""Creates a new HMAC MD5 algorithm instance.
|
||||
|
||||
Args:
|
||||
key: Signer key as a byte sequence.
|
||||
|
||||
Returns:
|
||||
UserImportHash: A new ``UserImportHash``.
|
||||
"""
|
||||
return cls._hmac('HMAC_MD5', key)
|
||||
|
||||
@classmethod
|
||||
def md5(cls, rounds):
|
||||
"""Creates a new MD5 algorithm instance.
|
||||
|
||||
Args:
|
||||
rounds: Number of rounds. Must be an integer between 0 and 8192.
|
||||
|
||||
Returns:
|
||||
UserImportHash: A new ``UserImportHash``.
|
||||
"""
|
||||
return UserImportHash(
|
||||
'MD5',
|
||||
{'rounds': _auth_utils.validate_int(rounds, 'rounds', 0, 8192)})
|
||||
|
||||
@classmethod
|
||||
def sha1(cls, rounds):
|
||||
"""Creates a new SHA1 algorithm instance.
|
||||
|
||||
Args:
|
||||
rounds: Number of rounds. Must be an integer between 1 and 8192.
|
||||
|
||||
Returns:
|
||||
UserImportHash: A new ``UserImportHash``.
|
||||
"""
|
||||
return UserImportHash(
|
||||
'SHA1',
|
||||
{'rounds': _auth_utils.validate_int(rounds, 'rounds', 1, 8192)})
|
||||
|
||||
@classmethod
|
||||
def sha256(cls, rounds):
|
||||
"""Creates a new SHA256 algorithm instance.
|
||||
|
||||
Args:
|
||||
rounds: Number of rounds. Must be an integer between 1 and 8192.
|
||||
|
||||
Returns:
|
||||
UserImportHash: A new ``UserImportHash``.
|
||||
"""
|
||||
return UserImportHash(
|
||||
'SHA256',
|
||||
{'rounds': _auth_utils.validate_int(rounds, 'rounds', 1, 8192)})
|
||||
|
||||
@classmethod
|
||||
def sha512(cls, rounds):
|
||||
"""Creates a new SHA512 algorithm instance.
|
||||
|
||||
Args:
|
||||
rounds: Number of rounds. Must be an integer between 1 and 8192.
|
||||
|
||||
Returns:
|
||||
UserImportHash: A new ``UserImportHash``.
|
||||
"""
|
||||
return UserImportHash(
|
||||
'SHA512',
|
||||
{'rounds': _auth_utils.validate_int(rounds, 'rounds', 1, 8192)})
|
||||
|
||||
@classmethod
|
||||
def pbkdf_sha1(cls, rounds):
|
||||
"""Creates a new PBKDF SHA1 algorithm instance.
|
||||
|
||||
Args:
|
||||
rounds: Number of rounds. Must be an integer between 0 and 120000.
|
||||
|
||||
Returns:
|
||||
UserImportHash: A new ``UserImportHash``.
|
||||
"""
|
||||
return UserImportHash(
|
||||
'PBKDF_SHA1',
|
||||
{'rounds': _auth_utils.validate_int(rounds, 'rounds', 0, 120000)})
|
||||
|
||||
@classmethod
|
||||
def pbkdf2_sha256(cls, rounds):
|
||||
"""Creates a new PBKDF2 SHA256 algorithm instance.
|
||||
|
||||
Args:
|
||||
rounds: Number of rounds. Must be an integer between 0 and 120000.
|
||||
|
||||
Returns:
|
||||
UserImportHash: A new ``UserImportHash``.
|
||||
"""
|
||||
return UserImportHash(
|
||||
'PBKDF2_SHA256',
|
||||
{'rounds': _auth_utils.validate_int(rounds, 'rounds', 0, 120000)})
|
||||
|
||||
@classmethod
|
||||
def scrypt(cls, key, rounds, memory_cost, salt_separator=None):
|
||||
"""Creates a new Scrypt algorithm instance.
|
||||
|
||||
This is the modified Scrypt algorithm used by Firebase Auth. See ``standard_scrypt()``
|
||||
function for the standard Scrypt algorith,
|
||||
|
||||
Args:
|
||||
key: Signer key as a byte sequence.
|
||||
rounds: Number of rounds. Must be an integer between 1 and 8.
|
||||
memory_cost: Memory cost as an integer between 1 and 14.
|
||||
salt_separator: Salt separator as a byte sequence (optional).
|
||||
|
||||
Returns:
|
||||
UserImportHash: A new ``UserImportHash``.
|
||||
"""
|
||||
data = {
|
||||
'signerKey': b64_encode(_auth_utils.validate_bytes(key, 'key', required=True)),
|
||||
'rounds': _auth_utils.validate_int(rounds, 'rounds', 1, 8),
|
||||
'memoryCost': _auth_utils.validate_int(memory_cost, 'memory_cost', 1, 14),
|
||||
}
|
||||
if salt_separator:
|
||||
data['saltSeparator'] = b64_encode(_auth_utils.validate_bytes(
|
||||
salt_separator, 'salt_separator'))
|
||||
return UserImportHash('SCRYPT', data)
|
||||
|
||||
@classmethod
|
||||
def bcrypt(cls):
|
||||
"""Creates a new Bcrypt algorithm instance.
|
||||
|
||||
Returns:
|
||||
UserImportHash: A new ``UserImportHash``.
|
||||
"""
|
||||
return UserImportHash('BCRYPT')
|
||||
|
||||
@classmethod
|
||||
def standard_scrypt(cls, memory_cost, parallelization, block_size, derived_key_length):
|
||||
"""Creates a new standard Scrypt algorithm instance.
|
||||
|
||||
Args:
|
||||
memory_cost: Memory cost as a non-negaive integer.
|
||||
parallelization: Parallelization as a non-negative integer.
|
||||
block_size: Block size as a non-negative integer.
|
||||
derived_key_length: Derived key length as a non-negative integer.
|
||||
|
||||
Returns:
|
||||
UserImportHash: A new ``UserImportHash``.
|
||||
"""
|
||||
data = {
|
||||
'memoryCost': _auth_utils.validate_int(memory_cost, 'memory_cost', low=0),
|
||||
'parallelization': _auth_utils.validate_int(parallelization, 'parallelization', low=0),
|
||||
'blockSize': _auth_utils.validate_int(block_size, 'block_size', low=0),
|
||||
'dkLen': _auth_utils.validate_int(derived_key_length, 'derived_key_length', low=0),
|
||||
}
|
||||
return UserImportHash('STANDARD_SCRYPT', data)
|
||||
|
||||
|
||||
class ErrorInfo:
|
||||
"""Represents an error encountered while performing a batch operation such
|
||||
as importing users or deleting multiple user accounts.
|
||||
"""
|
||||
# TODO(rsgowman): This class used to be specific to importing users (hence
|
||||
# it's home in _user_import.py). It's now also used by bulk deletion of
|
||||
# users. Move this to a more common location.
|
||||
|
||||
def __init__(self, error):
|
||||
self._index = error['index']
|
||||
self._reason = error['message']
|
||||
|
||||
@property
|
||||
def index(self):
|
||||
return self._index
|
||||
|
||||
@property
|
||||
def reason(self):
|
||||
return self._reason
|
||||
|
||||
|
||||
class UserImportResult:
|
||||
"""Represents the result of a bulk user import operation.
|
||||
|
||||
See ``auth.import_users()`` API for more details.
|
||||
"""
|
||||
|
||||
def __init__(self, result, total):
|
||||
errors = result.get('error', [])
|
||||
self._success_count = total - len(errors)
|
||||
self._failure_count = len(errors)
|
||||
self._errors = [ErrorInfo(err) for err in errors]
|
||||
|
||||
@property
|
||||
def success_count(self):
|
||||
"""Returns the number of users successfully imported."""
|
||||
return self._success_count
|
||||
|
||||
@property
|
||||
def failure_count(self):
|
||||
"""Returns the number of users that failed to be imported."""
|
||||
return self._failure_count
|
||||
|
||||
@property
|
||||
def errors(self):
|
||||
"""Returns a list of ``auth.ErrorInfo`` instances describing the errors encountered."""
|
||||
return self._errors
|
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
|
341
venv/Lib/site-packages/firebase_admin/_utils.py
Normal file
341
venv/Lib/site-packages/firebase_admin/_utils.py
Normal file
|
@ -0,0 +1,341 @@
|
|||
# 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.
|
||||
|
||||
"""Internal utilities common to all modules."""
|
||||
|
||||
import io
|
||||
import json
|
||||
import socket
|
||||
|
||||
import googleapiclient
|
||||
import httplib2
|
||||
import requests
|
||||
|
||||
import firebase_admin
|
||||
from firebase_admin import exceptions
|
||||
|
||||
|
||||
_ERROR_CODE_TO_EXCEPTION_TYPE = {
|
||||
exceptions.INVALID_ARGUMENT: exceptions.InvalidArgumentError,
|
||||
exceptions.FAILED_PRECONDITION: exceptions.FailedPreconditionError,
|
||||
exceptions.OUT_OF_RANGE: exceptions.OutOfRangeError,
|
||||
exceptions.UNAUTHENTICATED: exceptions.UnauthenticatedError,
|
||||
exceptions.PERMISSION_DENIED: exceptions.PermissionDeniedError,
|
||||
exceptions.NOT_FOUND: exceptions.NotFoundError,
|
||||
exceptions.ABORTED: exceptions.AbortedError,
|
||||
exceptions.ALREADY_EXISTS: exceptions.AlreadyExistsError,
|
||||
exceptions.CONFLICT: exceptions.ConflictError,
|
||||
exceptions.RESOURCE_EXHAUSTED: exceptions.ResourceExhaustedError,
|
||||
exceptions.CANCELLED: exceptions.CancelledError,
|
||||
exceptions.DATA_LOSS: exceptions.DataLossError,
|
||||
exceptions.UNKNOWN: exceptions.UnknownError,
|
||||
exceptions.INTERNAL: exceptions.InternalError,
|
||||
exceptions.UNAVAILABLE: exceptions.UnavailableError,
|
||||
exceptions.DEADLINE_EXCEEDED: exceptions.DeadlineExceededError,
|
||||
}
|
||||
|
||||
|
||||
_HTTP_STATUS_TO_ERROR_CODE = {
|
||||
400: exceptions.INVALID_ARGUMENT,
|
||||
401: exceptions.UNAUTHENTICATED,
|
||||
403: exceptions.PERMISSION_DENIED,
|
||||
404: exceptions.NOT_FOUND,
|
||||
409: exceptions.CONFLICT,
|
||||
412: exceptions.FAILED_PRECONDITION,
|
||||
429: exceptions.RESOURCE_EXHAUSTED,
|
||||
500: exceptions.INTERNAL,
|
||||
503: exceptions.UNAVAILABLE,
|
||||
}
|
||||
|
||||
|
||||
# See https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
|
||||
_RPC_CODE_TO_ERROR_CODE = {
|
||||
1: exceptions.CANCELLED,
|
||||
2: exceptions.UNKNOWN,
|
||||
3: exceptions.INVALID_ARGUMENT,
|
||||
4: exceptions.DEADLINE_EXCEEDED,
|
||||
5: exceptions.NOT_FOUND,
|
||||
6: exceptions.ALREADY_EXISTS,
|
||||
7: exceptions.PERMISSION_DENIED,
|
||||
8: exceptions.RESOURCE_EXHAUSTED,
|
||||
9: exceptions.FAILED_PRECONDITION,
|
||||
10: exceptions.ABORTED,
|
||||
11: exceptions.OUT_OF_RANGE,
|
||||
13: exceptions.INTERNAL,
|
||||
14: exceptions.UNAVAILABLE,
|
||||
15: exceptions.DATA_LOSS,
|
||||
16: exceptions.UNAUTHENTICATED,
|
||||
}
|
||||
|
||||
|
||||
def _get_initialized_app(app):
|
||||
"""Returns a reference to an initialized App instance."""
|
||||
if app is None:
|
||||
return firebase_admin.get_app()
|
||||
|
||||
if isinstance(app, firebase_admin.App):
|
||||
initialized_app = firebase_admin.get_app(app.name)
|
||||
if app is not initialized_app:
|
||||
raise ValueError('Illegal app argument. App instance not '
|
||||
'initialized via the firebase module.')
|
||||
return app
|
||||
|
||||
raise ValueError('Illegal app argument. Argument must be of type '
|
||||
' firebase_admin.App, but given "{0}".'.format(type(app)))
|
||||
|
||||
|
||||
|
||||
def get_app_service(app, name, initializer):
|
||||
app = _get_initialized_app(app)
|
||||
return app._get_service(name, initializer) # pylint: disable=protected-access
|
||||
|
||||
|
||||
def handle_platform_error_from_requests(error, handle_func=None):
|
||||
"""Constructs a ``FirebaseError`` from the given requests error.
|
||||
|
||||
This can be used to handle errors returned by Google Cloud Platform (GCP) APIs.
|
||||
|
||||
Args:
|
||||
error: An error raised by the requests module while making an HTTP call to a GCP API.
|
||||
handle_func: A function that can be used to handle platform errors in a custom way. When
|
||||
specified, this function will be called with three arguments. It has the same
|
||||
signature as ```_handle_func_requests``, but may return ``None``.
|
||||
|
||||
Returns:
|
||||
FirebaseError: A ``FirebaseError`` that can be raised to the user code.
|
||||
"""
|
||||
if error.response is None:
|
||||
return handle_requests_error(error)
|
||||
|
||||
response = error.response
|
||||
content = response.content.decode()
|
||||
status_code = response.status_code
|
||||
error_dict, message = _parse_platform_error(content, status_code)
|
||||
exc = None
|
||||
if handle_func:
|
||||
exc = handle_func(error, message, error_dict)
|
||||
|
||||
return exc if exc else _handle_func_requests(error, message, error_dict)
|
||||
|
||||
|
||||
def handle_operation_error(error):
|
||||
"""Constructs a ``FirebaseError`` from the given operation error.
|
||||
|
||||
Args:
|
||||
error: An error returned by a long running operation.
|
||||
|
||||
Returns:
|
||||
FirebaseError: A ``FirebaseError`` that can be raised to the user code.
|
||||
"""
|
||||
if not isinstance(error, dict):
|
||||
return exceptions.UnknownError(
|
||||
message='Unknown error while making a remote service call: {0}'.format(error),
|
||||
cause=error)
|
||||
|
||||
rpc_code = error.get('code')
|
||||
message = error.get('message')
|
||||
error_code = _rpc_code_to_error_code(rpc_code)
|
||||
err_type = _error_code_to_exception_type(error_code)
|
||||
return err_type(message=message)
|
||||
|
||||
|
||||
def _handle_func_requests(error, message, error_dict):
|
||||
"""Constructs a ``FirebaseError`` from the given GCP error.
|
||||
|
||||
Args:
|
||||
error: An error raised by the requests module while making an HTTP call.
|
||||
message: A message to be included in the resulting ``FirebaseError``.
|
||||
error_dict: Parsed GCP error response.
|
||||
|
||||
Returns:
|
||||
FirebaseError: A ``FirebaseError`` that can be raised to the user code or None.
|
||||
"""
|
||||
code = error_dict.get('status')
|
||||
return handle_requests_error(error, message, code)
|
||||
|
||||
|
||||
def handle_requests_error(error, message=None, code=None):
|
||||
"""Constructs a ``FirebaseError`` from the given requests error.
|
||||
|
||||
This method is agnostic of the remote service that produced the error, whether it is a GCP
|
||||
service or otherwise. Therefore, this method does not attempt to parse the error response in
|
||||
any way.
|
||||
|
||||
Args:
|
||||
error: An error raised by the requests module while making an HTTP call.
|
||||
message: A message to be included in the resulting ``FirebaseError`` (optional). If not
|
||||
specified the string representation of the ``error`` argument is used as the message.
|
||||
code: A GCP error code that will be used to determine the resulting error type (optional).
|
||||
If not specified the HTTP status code on the error response is used to determine a
|
||||
suitable error code.
|
||||
|
||||
Returns:
|
||||
FirebaseError: A ``FirebaseError`` that can be raised to the user code.
|
||||
"""
|
||||
if isinstance(error, requests.exceptions.Timeout):
|
||||
return exceptions.DeadlineExceededError(
|
||||
message='Timed out while making an API call: {0}'.format(error),
|
||||
cause=error)
|
||||
if isinstance(error, requests.exceptions.ConnectionError):
|
||||
return exceptions.UnavailableError(
|
||||
message='Failed to establish a connection: {0}'.format(error),
|
||||
cause=error)
|
||||
if error.response is None:
|
||||
return exceptions.UnknownError(
|
||||
message='Unknown error while making a remote service call: {0}'.format(error),
|
||||
cause=error)
|
||||
|
||||
if not code:
|
||||
code = _http_status_to_error_code(error.response.status_code)
|
||||
if not message:
|
||||
message = str(error)
|
||||
|
||||
err_type = _error_code_to_exception_type(code)
|
||||
return err_type(message=message, cause=error, http_response=error.response)
|
||||
|
||||
|
||||
def handle_platform_error_from_googleapiclient(error, handle_func=None):
|
||||
"""Constructs a ``FirebaseError`` from the given googleapiclient error.
|
||||
|
||||
This can be used to handle errors returned by Google Cloud Platform (GCP) APIs.
|
||||
|
||||
Args:
|
||||
error: An error raised by the googleapiclient while making an HTTP call to a GCP API.
|
||||
handle_func: A function that can be used to handle platform errors in a custom way. When
|
||||
specified, this function will be called with three arguments. It has the same
|
||||
signature as ```_handle_func_googleapiclient``, but may return ``None``.
|
||||
|
||||
Returns:
|
||||
FirebaseError: A ``FirebaseError`` that can be raised to the user code.
|
||||
"""
|
||||
if not isinstance(error, googleapiclient.errors.HttpError):
|
||||
return handle_googleapiclient_error(error)
|
||||
|
||||
content = error.content.decode()
|
||||
status_code = error.resp.status
|
||||
error_dict, message = _parse_platform_error(content, status_code)
|
||||
http_response = _http_response_from_googleapiclient_error(error)
|
||||
exc = None
|
||||
if handle_func:
|
||||
exc = handle_func(error, message, error_dict, http_response)
|
||||
|
||||
return exc if exc else _handle_func_googleapiclient(error, message, error_dict, http_response)
|
||||
|
||||
|
||||
def _handle_func_googleapiclient(error, message, error_dict, http_response):
|
||||
"""Constructs a ``FirebaseError`` from the given GCP error.
|
||||
|
||||
Args:
|
||||
error: An error raised by the googleapiclient module while making an HTTP call.
|
||||
message: A message to be included in the resulting ``FirebaseError``.
|
||||
error_dict: Parsed GCP error response.
|
||||
http_response: A requests HTTP response object to associate with the exception.
|
||||
|
||||
Returns:
|
||||
FirebaseError: A ``FirebaseError`` that can be raised to the user code or None.
|
||||
"""
|
||||
code = error_dict.get('status')
|
||||
return handle_googleapiclient_error(error, message, code, http_response)
|
||||
|
||||
|
||||
def handle_googleapiclient_error(error, message=None, code=None, http_response=None):
|
||||
"""Constructs a ``FirebaseError`` from the given googleapiclient error.
|
||||
|
||||
This method is agnostic of the remote service that produced the error, whether it is a GCP
|
||||
service or otherwise. Therefore, this method does not attempt to parse the error response in
|
||||
any way.
|
||||
|
||||
Args:
|
||||
error: An error raised by the googleapiclient module while making an HTTP call.
|
||||
message: A message to be included in the resulting ``FirebaseError`` (optional). If not
|
||||
specified the string representation of the ``error`` argument is used as the message.
|
||||
code: A GCP error code that will be used to determine the resulting error type (optional).
|
||||
If not specified the HTTP status code on the error response is used to determine a
|
||||
suitable error code.
|
||||
http_response: A requests HTTP response object to associate with the exception (optional).
|
||||
If not specified, one will be created from the ``error``.
|
||||
|
||||
Returns:
|
||||
FirebaseError: A ``FirebaseError`` that can be raised to the user code.
|
||||
"""
|
||||
if isinstance(error, socket.timeout) or (
|
||||
isinstance(error, socket.error) and 'timed out' in str(error)):
|
||||
return exceptions.DeadlineExceededError(
|
||||
message='Timed out while making an API call: {0}'.format(error),
|
||||
cause=error)
|
||||
if isinstance(error, httplib2.ServerNotFoundError):
|
||||
return exceptions.UnavailableError(
|
||||
message='Failed to establish a connection: {0}'.format(error),
|
||||
cause=error)
|
||||
if not isinstance(error, googleapiclient.errors.HttpError):
|
||||
return exceptions.UnknownError(
|
||||
message='Unknown error while making a remote service call: {0}'.format(error),
|
||||
cause=error)
|
||||
|
||||
if not code:
|
||||
code = _http_status_to_error_code(error.resp.status)
|
||||
if not message:
|
||||
message = str(error)
|
||||
if not http_response:
|
||||
http_response = _http_response_from_googleapiclient_error(error)
|
||||
|
||||
err_type = _error_code_to_exception_type(code)
|
||||
return err_type(message=message, cause=error, http_response=http_response)
|
||||
|
||||
|
||||
def _http_response_from_googleapiclient_error(error):
|
||||
"""Creates a requests HTTP Response object from the given googleapiclient error."""
|
||||
resp = requests.models.Response()
|
||||
resp.raw = io.BytesIO(error.content)
|
||||
resp.status_code = error.resp.status
|
||||
return resp
|
||||
|
||||
|
||||
def _http_status_to_error_code(status):
|
||||
"""Maps an HTTP status to a platform error code."""
|
||||
return _HTTP_STATUS_TO_ERROR_CODE.get(status, exceptions.UNKNOWN)
|
||||
|
||||
def _rpc_code_to_error_code(rpc_code):
|
||||
"""Maps an RPC code to a platform error code."""
|
||||
return _RPC_CODE_TO_ERROR_CODE.get(rpc_code, exceptions.UNKNOWN)
|
||||
|
||||
def _error_code_to_exception_type(code):
|
||||
"""Maps a platform error code to an exception type."""
|
||||
return _ERROR_CODE_TO_EXCEPTION_TYPE.get(code, exceptions.UnknownError)
|
||||
|
||||
|
||||
def _parse_platform_error(content, status_code):
|
||||
"""Parses an HTTP error response from a Google Cloud Platform API and extracts the error code
|
||||
and message fields.
|
||||
|
||||
Args:
|
||||
content: Decoded content of the response body.
|
||||
status_code: HTTP status code.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing error code and message.
|
||||
"""
|
||||
data = {}
|
||||
try:
|
||||
parsed_body = json.loads(content)
|
||||
if isinstance(parsed_body, dict):
|
||||
data = parsed_body
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
error_dict = data.get('error', {})
|
||||
msg = error_dict.get('message')
|
||||
if not msg:
|
||||
msg = 'Unexpected HTTP response with status: {0}; body: {1}'.format(status_code, content)
|
||||
return error_dict, msg
|
883
venv/Lib/site-packages/firebase_admin/auth.py
Normal file
883
venv/Lib/site-packages/firebase_admin/auth.py
Normal file
|
@ -0,0 +1,883 @@
|
|||
# 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 Authentication module.
|
||||
|
||||
This module contains functions for minting and verifying JWTs used for
|
||||
authenticating against Firebase services. It also provides functions for
|
||||
creating and managing user accounts in Firebase projects.
|
||||
"""
|
||||
|
||||
from firebase_admin import _auth_client
|
||||
from firebase_admin import _auth_providers
|
||||
from firebase_admin import _auth_utils
|
||||
from firebase_admin import _user_identifier
|
||||
from firebase_admin import _token_gen
|
||||
from firebase_admin import _user_import
|
||||
from firebase_admin import _user_mgt
|
||||
from firebase_admin import _utils
|
||||
|
||||
|
||||
_AUTH_ATTRIBUTE = '_auth'
|
||||
|
||||
|
||||
__all__ = [
|
||||
'ActionCodeSettings',
|
||||
'CertificateFetchError',
|
||||
'Client',
|
||||
'ConfigurationNotFoundError',
|
||||
'DELETE_ATTRIBUTE',
|
||||
'EmailAlreadyExistsError',
|
||||
'ErrorInfo',
|
||||
'ExpiredIdTokenError',
|
||||
'ExpiredSessionCookieError',
|
||||
'ExportedUserRecord',
|
||||
'DeleteUsersResult',
|
||||
'GetUsersResult',
|
||||
'ImportUserRecord',
|
||||
'InsufficientPermissionError',
|
||||
'InvalidDynamicLinkDomainError',
|
||||
'InvalidIdTokenError',
|
||||
'InvalidSessionCookieError',
|
||||
'ListProviderConfigsPage',
|
||||
'ListUsersPage',
|
||||
'OIDCProviderConfig',
|
||||
'PhoneNumberAlreadyExistsError',
|
||||
'ProviderConfig',
|
||||
'RevokedIdTokenError',
|
||||
'RevokedSessionCookieError',
|
||||
'SAMLProviderConfig',
|
||||
'TokenSignError',
|
||||
'UidAlreadyExistsError',
|
||||
'UnexpectedResponseError',
|
||||
'UserImportHash',
|
||||
'UserImportResult',
|
||||
'UserInfo',
|
||||
'UserMetadata',
|
||||
'UserNotFoundError',
|
||||
'UserProvider',
|
||||
'UserRecord',
|
||||
|
||||
'UserIdentifier',
|
||||
'UidIdentifier',
|
||||
'EmailIdentifier',
|
||||
'PhoneIdentifier',
|
||||
'ProviderIdentifier',
|
||||
|
||||
'create_custom_token',
|
||||
'create_oidc_provider_config',
|
||||
'create_saml_provider_config',
|
||||
'create_session_cookie',
|
||||
'create_user',
|
||||
'delete_oidc_provider_config',
|
||||
'delete_saml_provider_config',
|
||||
'delete_user',
|
||||
'delete_users',
|
||||
'generate_email_verification_link',
|
||||
'generate_password_reset_link',
|
||||
'generate_sign_in_with_email_link',
|
||||
'get_oidc_provider_config',
|
||||
'get_saml_provider_config',
|
||||
'get_user',
|
||||
'get_user_by_email',
|
||||
'get_user_by_phone_number',
|
||||
'get_users',
|
||||
'import_users',
|
||||
'list_saml_provider_configs',
|
||||
'list_users',
|
||||
'revoke_refresh_tokens',
|
||||
'set_custom_user_claims',
|
||||
'update_oidc_provider_config',
|
||||
'update_saml_provider_config',
|
||||
'update_user',
|
||||
'verify_id_token',
|
||||
'verify_session_cookie',
|
||||
]
|
||||
|
||||
ActionCodeSettings = _user_mgt.ActionCodeSettings
|
||||
CertificateFetchError = _token_gen.CertificateFetchError
|
||||
Client = _auth_client.Client
|
||||
ConfigurationNotFoundError = _auth_utils.ConfigurationNotFoundError
|
||||
DELETE_ATTRIBUTE = _user_mgt.DELETE_ATTRIBUTE
|
||||
DeleteUsersResult = _user_mgt.DeleteUsersResult
|
||||
EmailAlreadyExistsError = _auth_utils.EmailAlreadyExistsError
|
||||
ErrorInfo = _user_import.ErrorInfo
|
||||
ExpiredIdTokenError = _token_gen.ExpiredIdTokenError
|
||||
ExpiredSessionCookieError = _token_gen.ExpiredSessionCookieError
|
||||
ExportedUserRecord = _user_mgt.ExportedUserRecord
|
||||
GetUsersResult = _user_mgt.GetUsersResult
|
||||
ImportUserRecord = _user_import.ImportUserRecord
|
||||
InsufficientPermissionError = _auth_utils.InsufficientPermissionError
|
||||
InvalidDynamicLinkDomainError = _auth_utils.InvalidDynamicLinkDomainError
|
||||
InvalidIdTokenError = _auth_utils.InvalidIdTokenError
|
||||
InvalidSessionCookieError = _token_gen.InvalidSessionCookieError
|
||||
ListProviderConfigsPage = _auth_providers.ListProviderConfigsPage
|
||||
ListUsersPage = _user_mgt.ListUsersPage
|
||||
OIDCProviderConfig = _auth_providers.OIDCProviderConfig
|
||||
PhoneNumberAlreadyExistsError = _auth_utils.PhoneNumberAlreadyExistsError
|
||||
ProviderConfig = _auth_providers.ProviderConfig
|
||||
RevokedIdTokenError = _token_gen.RevokedIdTokenError
|
||||
RevokedSessionCookieError = _token_gen.RevokedSessionCookieError
|
||||
SAMLProviderConfig = _auth_providers.SAMLProviderConfig
|
||||
TokenSignError = _token_gen.TokenSignError
|
||||
UidAlreadyExistsError = _auth_utils.UidAlreadyExistsError
|
||||
UnexpectedResponseError = _auth_utils.UnexpectedResponseError
|
||||
UserImportHash = _user_import.UserImportHash
|
||||
UserImportResult = _user_import.UserImportResult
|
||||
UserInfo = _user_mgt.UserInfo
|
||||
UserMetadata = _user_mgt.UserMetadata
|
||||
UserNotFoundError = _auth_utils.UserNotFoundError
|
||||
UserProvider = _user_import.UserProvider
|
||||
UserRecord = _user_mgt.UserRecord
|
||||
|
||||
UserIdentifier = _user_identifier.UserIdentifier
|
||||
UidIdentifier = _user_identifier.UidIdentifier
|
||||
EmailIdentifier = _user_identifier.EmailIdentifier
|
||||
PhoneIdentifier = _user_identifier.PhoneIdentifier
|
||||
ProviderIdentifier = _user_identifier.ProviderIdentifier
|
||||
|
||||
|
||||
def _get_client(app):
|
||||
"""Returns a client instance for an App.
|
||||
|
||||
If the App already has a client associated with it, simply returns
|
||||
it. Otherwise creates a new client, and adds it to the App before
|
||||
returning it.
|
||||
|
||||
Args:
|
||||
app: A Firebase App instance (or ``None`` to use the default App).
|
||||
|
||||
Returns:
|
||||
Client: A client for the specified App instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the app argument is invalid.
|
||||
"""
|
||||
return _utils.get_app_service(app, _AUTH_ATTRIBUTE, Client)
|
||||
|
||||
|
||||
def create_custom_token(uid, developer_claims=None, app=None):
|
||||
"""Builds and signs a Firebase custom auth token.
|
||||
|
||||
Args:
|
||||
uid: ID of the user for whom the token is created.
|
||||
developer_claims: A dictionary of claims to be included in the token
|
||||
(optional).
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
bytes: A token minted from the input parameters.
|
||||
|
||||
Raises:
|
||||
ValueError: If input parameters are invalid.
|
||||
TokenSignError: If an error occurs while signing the token using the remote IAM service.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
return client.create_custom_token(uid, developer_claims)
|
||||
|
||||
|
||||
def verify_id_token(id_token, app=None, check_revoked=False):
|
||||
"""Verifies the signature and data for the provided JWT.
|
||||
|
||||
Accepts a signed token string, verifies that it is current, and issued
|
||||
to this project, and that it was correctly signed by Google.
|
||||
|
||||
Args:
|
||||
id_token: A string of the encoded JWT.
|
||||
app: An App instance (optional).
|
||||
check_revoked: Boolean, If true, checks whether the token has been revoked (optional).
|
||||
|
||||
Returns:
|
||||
dict: A dictionary of key-value pairs parsed from the decoded JWT.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``id_token`` is a not a string or is empty.
|
||||
InvalidIdTokenError: If ``id_token`` is not a valid Firebase ID token.
|
||||
ExpiredIdTokenError: If the specified ID token has expired.
|
||||
RevokedIdTokenError: If ``check_revoked`` is ``True`` and the ID token has been revoked.
|
||||
CertificateFetchError: If an error occurs while fetching the public key certificates
|
||||
required to verify the ID token.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
return client.verify_id_token(id_token, check_revoked=check_revoked)
|
||||
|
||||
|
||||
def create_session_cookie(id_token, expires_in, app=None):
|
||||
"""Creates a new Firebase session cookie from the given ID token and options.
|
||||
|
||||
The returned JWT can be set as a server-side session cookie with a custom cookie policy.
|
||||
|
||||
Args:
|
||||
id_token: The Firebase ID token to exchange for a session cookie.
|
||||
expires_in: Duration until the cookie is expired. This can be specified
|
||||
as a numeric seconds value or a ``datetime.timedelta`` instance.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
bytes: A session cookie generated from the input parameters.
|
||||
|
||||
Raises:
|
||||
ValueError: If input parameters are invalid.
|
||||
FirebaseError: If an error occurs while creating the cookie.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
# pylint: disable=protected-access
|
||||
return client._token_generator.create_session_cookie(id_token, expires_in)
|
||||
|
||||
|
||||
def verify_session_cookie(session_cookie, check_revoked=False, app=None):
|
||||
"""Verifies a Firebase session cookie.
|
||||
|
||||
Accepts a session cookie string, verifies that it is current, and issued
|
||||
to this project, and that it was correctly signed by Google.
|
||||
|
||||
Args:
|
||||
session_cookie: A session cookie string to verify.
|
||||
check_revoked: Boolean, if true, checks whether the cookie has been revoked (optional).
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
dict: A dictionary of key-value pairs parsed from the decoded JWT.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``session_cookie`` is a not a string or is empty.
|
||||
InvalidSessionCookieError: If ``session_cookie`` is not a valid Firebase session cookie.
|
||||
ExpiredSessionCookieError: If the specified session cookie has expired.
|
||||
RevokedSessionCookieError: If ``check_revoked`` is ``True`` and the cookie has been revoked.
|
||||
CertificateFetchError: If an error occurs while fetching the public key certificates
|
||||
required to verify the session cookie.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
# pylint: disable=protected-access
|
||||
verified_claims = client._token_verifier.verify_session_cookie(session_cookie)
|
||||
if check_revoked:
|
||||
client._check_jwt_revoked(verified_claims, RevokedSessionCookieError, 'session cookie')
|
||||
return verified_claims
|
||||
|
||||
|
||||
def revoke_refresh_tokens(uid, app=None):
|
||||
"""Revokes all refresh tokens for an existing user.
|
||||
|
||||
This function updates the user's ``tokens_valid_after_timestamp`` to the current UTC
|
||||
in seconds since the epoch. It is important that the server on which this is called has its
|
||||
clock set correctly and synchronized.
|
||||
|
||||
While this revokes all sessions for a specified user and disables any new ID tokens for
|
||||
existing sessions from getting minted, existing ID tokens may remain active until their
|
||||
natural expiration (one hour). To verify that ID tokens are revoked, use
|
||||
``verify_id_token(idToken, check_revoked=True)``.
|
||||
|
||||
Args:
|
||||
uid: A user ID string.
|
||||
app: An App instance (optional).
|
||||
|
||||
Raises:
|
||||
ValueError: If the user ID is None, empty or malformed.
|
||||
FirebaseError: If an error occurs while revoking the refresh token.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
client.revoke_refresh_tokens(uid)
|
||||
|
||||
|
||||
def get_user(uid, app=None):
|
||||
"""Gets the user data corresponding to the specified user ID.
|
||||
|
||||
Args:
|
||||
uid: A user ID string.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
UserRecord: A user record instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the user ID is None, empty or malformed.
|
||||
UserNotFoundError: If the specified user ID does not exist.
|
||||
FirebaseError: If an error occurs while retrieving the user.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
return client.get_user(uid=uid)
|
||||
|
||||
|
||||
def get_user_by_email(email, app=None):
|
||||
"""Gets the user data corresponding to the specified user email.
|
||||
|
||||
Args:
|
||||
email: A user email address string.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
UserRecord: A user record instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the email is None, empty or malformed.
|
||||
UserNotFoundError: If no user exists by the specified email address.
|
||||
FirebaseError: If an error occurs while retrieving the user.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
return client.get_user_by_email(email=email)
|
||||
|
||||
|
||||
def get_user_by_phone_number(phone_number, app=None):
|
||||
"""Gets the user data corresponding to the specified phone number.
|
||||
|
||||
Args:
|
||||
phone_number: A phone number string.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
UserRecord: A user record instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the phone number is None, empty or malformed.
|
||||
UserNotFoundError: If no user exists by the specified phone number.
|
||||
FirebaseError: If an error occurs while retrieving the user.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
return client.get_user_by_phone_number(phone_number=phone_number)
|
||||
|
||||
|
||||
def get_users(identifiers, app=None):
|
||||
"""Gets the user data corresponding to the specified identifiers.
|
||||
|
||||
There are no ordering guarantees; in particular, the nth entry in the
|
||||
result list is not guaranteed to correspond to the nth entry in the input
|
||||
parameters list.
|
||||
|
||||
A maximum of 100 identifiers may be supplied. If more than 100
|
||||
identifiers are supplied, this method raises a `ValueError`.
|
||||
|
||||
Args:
|
||||
identifiers (list[UserIdentifier]): A list of ``UserIdentifier``
|
||||
instances used to indicate which user records should be returned.
|
||||
Must have <= 100 entries.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
GetUsersResult: A ``GetUsersResult`` instance corresponding to the
|
||||
specified identifiers.
|
||||
|
||||
Raises:
|
||||
ValueError: If any of the identifiers are invalid or if more than 100
|
||||
identifiers are specified.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
return client.get_users(identifiers)
|
||||
|
||||
|
||||
def list_users(page_token=None, max_results=_user_mgt.MAX_LIST_USERS_RESULTS, app=None):
|
||||
"""Retrieves a page of user accounts from a Firebase project.
|
||||
|
||||
The ``page_token`` argument governs the starting point of the page. The ``max_results``
|
||||
argument governs the maximum number of user accounts that may be included in the returned page.
|
||||
This function never returns None. If there are no user accounts in the Firebase project, this
|
||||
returns an empty page.
|
||||
|
||||
Args:
|
||||
page_token: A non-empty page token string, which indicates the starting point of the page
|
||||
(optional). Defaults to ``None``, which will retrieve the first page of users.
|
||||
max_results: A positive integer indicating the maximum number of users to include in the
|
||||
returned page (optional). Defaults to 1000, which is also the maximum number allowed.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
ListUsersPage: A page of user accounts.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``max_results`` or ``page_token`` are invalid.
|
||||
FirebaseError: If an error occurs while retrieving the user accounts.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
return client.list_users(page_token=page_token, max_results=max_results)
|
||||
|
||||
|
||||
def create_user(**kwargs): # pylint: disable=differing-param-doc
|
||||
"""Creates a new user account with the specified properties.
|
||||
|
||||
Args:
|
||||
kwargs: A series of keyword arguments (optional).
|
||||
|
||||
Keyword Args:
|
||||
uid: User ID to assign to the newly created user (optional).
|
||||
display_name: The user's display name (optional).
|
||||
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).
|
||||
photo_url: The user's photo URL (optional).
|
||||
password: The user's raw, unhashed password. (optional).
|
||||
disabled: A boolean indicating whether or not the user account is disabled (optional).
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
UserRecord: A user record instance for the newly created user.
|
||||
|
||||
Raises:
|
||||
ValueError: If the specified user properties are invalid.
|
||||
FirebaseError: If an error occurs while creating the user account.
|
||||
"""
|
||||
app = kwargs.pop('app', None)
|
||||
client = _get_client(app)
|
||||
return client.create_user(**kwargs)
|
||||
|
||||
|
||||
def update_user(uid, **kwargs): # pylint: disable=differing-param-doc
|
||||
"""Updates an existing user account with the specified properties.
|
||||
|
||||
Args:
|
||||
uid: A user ID string.
|
||||
kwargs: A series of keyword arguments (optional).
|
||||
|
||||
Keyword Args:
|
||||
display_name: The user's display name (optional). Can be removed by explicitly passing
|
||||
``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 ``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 containing the custom claims to be set on the
|
||||
user account (optional). To remove all custom claims, pass ``auth.DELETE_ATTRIBUTE``.
|
||||
valid_since: An integer signifying the seconds since the epoch (optional). This field is
|
||||
set by ``revoke_refresh_tokens`` and it is discouraged to set this field directly.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
UserRecord: An updated user record instance for the user.
|
||||
|
||||
Raises:
|
||||
ValueError: If the specified user ID or properties are invalid.
|
||||
FirebaseError: If an error occurs while updating the user account.
|
||||
"""
|
||||
app = kwargs.pop('app', None)
|
||||
client = _get_client(app)
|
||||
return client.update_user(uid, **kwargs)
|
||||
|
||||
|
||||
def set_custom_user_claims(uid, custom_claims, app=None):
|
||||
"""Sets additional claims on an existing user account.
|
||||
|
||||
Custom claims set via this function can be used to define user roles and privilege levels.
|
||||
These claims propagate to all the devices where the user is already signed in (after token
|
||||
expiration or when token refresh is forced), and next time the user signs in. The claims
|
||||
can be accessed via the user's ID token JWT. If a reserved OIDC claim is specified (sub, iat,
|
||||
iss, etc), an error is thrown. Claims payload must also not be larger then 1000 characters
|
||||
when serialized into a JSON string.
|
||||
|
||||
Args:
|
||||
uid: A user ID string.
|
||||
custom_claims: A dictionary or a JSON string of custom claims. Pass None to unset any
|
||||
claims set previously.
|
||||
app: An App instance (optional).
|
||||
|
||||
Raises:
|
||||
ValueError: If the specified user ID or the custom claims are invalid.
|
||||
FirebaseError: If an error occurs while updating the user account.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
client.set_custom_user_claims(uid, custom_claims=custom_claims)
|
||||
|
||||
|
||||
def delete_user(uid, app=None):
|
||||
"""Deletes the user identified by the specified user ID.
|
||||
|
||||
Args:
|
||||
uid: A user ID string.
|
||||
app: An App instance (optional).
|
||||
|
||||
Raises:
|
||||
ValueError: If the user ID is None, empty or malformed.
|
||||
FirebaseError: If an error occurs while deleting the user account.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
client.delete_user(uid)
|
||||
|
||||
|
||||
def delete_users(uids, app=None):
|
||||
"""Deletes the users specified by the given identifiers.
|
||||
|
||||
Deleting a non-existing user does not generate an error (the method is
|
||||
idempotent.) Non-existing users are considered to be successfully deleted
|
||||
and are therefore included in the `DeleteUserResult.success_count` value.
|
||||
|
||||
A maximum of 1000 identifiers may be supplied. If more than 1000
|
||||
identifiers are supplied, this method raises a `ValueError`.
|
||||
|
||||
Args:
|
||||
uids: A list of strings indicating the uids of the users to be deleted.
|
||||
Must have <= 1000 entries.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
DeleteUsersResult: The total number of successful/failed deletions, as
|
||||
well as the array of errors that correspond to the failed deletions.
|
||||
|
||||
Raises:
|
||||
ValueError: If any of the identifiers are invalid or if more than 1000
|
||||
identifiers are specified.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
return client.delete_users(uids)
|
||||
|
||||
|
||||
def import_users(users, hash_alg=None, app=None):
|
||||
"""Imports the specified list of users into Firebase Auth.
|
||||
|
||||
At most 1000 users can be imported at a time. This operation is optimized for bulk imports and
|
||||
will ignore checks on identifier uniqueness which could result in duplications. The
|
||||
``hash_alg`` parameter must be specified when importing users with passwords. Refer to the
|
||||
``UserImportHash`` class for supported hash algorithms.
|
||||
|
||||
Args:
|
||||
users: A list of ``ImportUserRecord`` instances to import. Length of the list must not
|
||||
exceed 1000.
|
||||
hash_alg: A ``UserImportHash`` object (optional). Required when importing users with
|
||||
passwords.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
UserImportResult: An object summarizing the result of the import operation.
|
||||
|
||||
Raises:
|
||||
ValueError: If the provided arguments are invalid.
|
||||
FirebaseError: If an error occurs while importing users.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
return client.import_users(users, hash_alg)
|
||||
|
||||
|
||||
def generate_password_reset_link(email, action_code_settings=None, app=None):
|
||||
"""Generates the out-of-band email action link for password reset flows for the specified email
|
||||
address.
|
||||
|
||||
Args:
|
||||
email: The email of the user whose password is to be reset.
|
||||
action_code_settings: ``ActionCodeSettings`` instance (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.
|
||||
app: An App instance (optional).
|
||||
Returns:
|
||||
link: The password reset link created by the API
|
||||
|
||||
Raises:
|
||||
ValueError: If the provided arguments are invalid
|
||||
FirebaseError: If an error occurs while generating the link
|
||||
"""
|
||||
client = _get_client(app)
|
||||
return client.generate_password_reset_link(email, action_code_settings=action_code_settings)
|
||||
|
||||
|
||||
def generate_email_verification_link(email, action_code_settings=None, app=None):
|
||||
"""Generates the out-of-band email action link for email verification flows for the specified
|
||||
email address.
|
||||
|
||||
Args:
|
||||
email: The email of the user to be verified.
|
||||
action_code_settings: ``ActionCodeSettings`` instance (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.
|
||||
app: An App instance (optional).
|
||||
Returns:
|
||||
link: The email verification link created by the API
|
||||
|
||||
Raises:
|
||||
ValueError: If the provided arguments are invalid
|
||||
FirebaseError: If an error occurs while generating the link
|
||||
"""
|
||||
client = _get_client(app)
|
||||
return client.generate_email_verification_link(
|
||||
email, action_code_settings=action_code_settings)
|
||||
|
||||
|
||||
def generate_sign_in_with_email_link(email, action_code_settings, app=None):
|
||||
"""Generates the out-of-band email action link for email link sign-in flows, using the action
|
||||
code settings provided.
|
||||
|
||||
Args:
|
||||
email: The email of the user signing in.
|
||||
action_code_settings: ``ActionCodeSettings`` instance. Defines whether
|
||||
the link is to be handled by a mobile app and the additional state information to be
|
||||
passed in the deep link.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
link: The email sign-in link created by the API
|
||||
|
||||
Raises:
|
||||
ValueError: If the provided arguments are invalid
|
||||
FirebaseError: If an error occurs while generating the link
|
||||
"""
|
||||
client = _get_client(app)
|
||||
return client.generate_sign_in_with_email_link(
|
||||
email, action_code_settings=action_code_settings)
|
||||
|
||||
|
||||
def get_oidc_provider_config(provider_id, app=None):
|
||||
"""Returns the ``OIDCProviderConfig`` with the given ID.
|
||||
|
||||
Args:
|
||||
provider_id: Provider ID string.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
OIDCProviderConfig: An OIDC provider config instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the provider ID is invalid, empty or does not have ``oidc.`` prefix.
|
||||
ConfigurationNotFoundError: If no OIDC provider is available with the given identifier.
|
||||
FirebaseError: If an error occurs while retrieving the OIDC provider.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
return client.get_oidc_provider_config(provider_id)
|
||||
|
||||
def create_oidc_provider_config(
|
||||
provider_id, client_id, issuer, display_name=None, enabled=None, app=None):
|
||||
"""Creates a new OIDC provider config from the given parameters.
|
||||
|
||||
OIDC provider support requires Google Cloud's Identity Platform (GCIP). To learn more about
|
||||
GCIP, including pricing and features, see https://cloud.google.com/identity-platform.
|
||||
|
||||
Args:
|
||||
provider_id: Provider ID string. Must have the prefix ``oidc.``.
|
||||
client_id: Client ID of the new config.
|
||||
issuer: Issuer of the new config. Must be a valid URL.
|
||||
display_name: The user-friendly display name to the current configuration (optional).
|
||||
This name is also used as the provider label in the Cloud Console.
|
||||
enabled: A boolean indicating whether the provider configuration is enabled or disabled
|
||||
(optional). A user cannot sign in using a disabled provider.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
OIDCProviderConfig: The newly created OIDC provider config instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If any of the specified input parameters are invalid.
|
||||
FirebaseError: If an error occurs while creating the new OIDC provider config.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
return client.create_oidc_provider_config(
|
||||
provider_id, client_id=client_id, issuer=issuer, display_name=display_name,
|
||||
enabled=enabled)
|
||||
|
||||
|
||||
def update_oidc_provider_config(
|
||||
provider_id, client_id=None, issuer=None, display_name=None, enabled=None, app=None):
|
||||
"""Updates an existing OIDC provider config with the given parameters.
|
||||
|
||||
Args:
|
||||
provider_id: Provider ID string. Must have the prefix ``oidc.``.
|
||||
client_id: Client ID of the new config (optional).
|
||||
issuer: Issuer of the new config (optional). Must be a valid URL.
|
||||
display_name: The user-friendly display name of the current configuration (optional).
|
||||
Pass ``auth.DELETE_ATTRIBUTE`` to delete the current display name.
|
||||
enabled: A boolean indicating whether the provider configuration is enabled or disabled
|
||||
(optional).
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
OIDCProviderConfig: The updated OIDC provider config instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If any of the specified input parameters are invalid.
|
||||
FirebaseError: If an error occurs while updating the OIDC provider config.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
return client.update_oidc_provider_config(
|
||||
provider_id, client_id=client_id, issuer=issuer, display_name=display_name,
|
||||
enabled=enabled)
|
||||
|
||||
|
||||
def delete_oidc_provider_config(provider_id, app=None):
|
||||
"""Deletes the ``OIDCProviderConfig`` with the given ID.
|
||||
|
||||
Args:
|
||||
provider_id: Provider ID string.
|
||||
app: An App instance (optional).
|
||||
|
||||
Raises:
|
||||
ValueError: If the provider ID is invalid, empty or does not have ``oidc.`` prefix.
|
||||
ConfigurationNotFoundError: If no OIDC provider is available with the given identifier.
|
||||
FirebaseError: If an error occurs while deleting the OIDC provider.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
client.delete_oidc_provider_config(provider_id)
|
||||
|
||||
|
||||
def list_oidc_provider_configs(
|
||||
page_token=None, max_results=_auth_providers.MAX_LIST_CONFIGS_RESULTS, app=None):
|
||||
"""Retrieves a page of OIDC provider configs from a Firebase project.
|
||||
|
||||
The ``page_token`` argument governs the starting point of the page. The ``max_results``
|
||||
argument governs the maximum number of configs that may be included in the returned
|
||||
page. This function never returns ``None``. If there are no OIDC configs in the Firebase
|
||||
project, this returns an empty page.
|
||||
|
||||
Args:
|
||||
page_token: A non-empty page token string, which indicates the starting point of the
|
||||
page (optional). Defaults to ``None``, which will retrieve the first page of users.
|
||||
max_results: A positive integer indicating the maximum number of users to include in
|
||||
the returned page (optional). Defaults to 100, which is also the maximum number
|
||||
allowed.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
ListProviderConfigsPage: A page of OIDC provider config instances.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``max_results`` or ``page_token`` are invalid.
|
||||
FirebaseError: If an error occurs while retrieving the OIDC provider configs.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
return client.list_oidc_provider_configs(page_token, max_results)
|
||||
|
||||
|
||||
def get_saml_provider_config(provider_id, app=None):
|
||||
"""Returns the ``SAMLProviderConfig`` with the given ID.
|
||||
|
||||
Args:
|
||||
provider_id: Provider ID string.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
SAMLProviderConfig: A SAML provider config instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the provider ID is invalid, empty or does not have ``saml.`` prefix.
|
||||
ConfigurationNotFoundError: If no SAML provider is available with the given identifier.
|
||||
FirebaseError: If an error occurs while retrieving the SAML provider.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
return client.get_saml_provider_config(provider_id)
|
||||
|
||||
|
||||
def create_saml_provider_config(
|
||||
provider_id, idp_entity_id, sso_url, x509_certificates, rp_entity_id, callback_url,
|
||||
display_name=None, enabled=None, app=None):
|
||||
"""Creates a new SAML provider config from the given parameters.
|
||||
|
||||
SAML provider support requires Google Cloud's Identity Platform (GCIP). To learn more about
|
||||
GCIP, including pricing and features, see https://cloud.google.com/identity-platform.
|
||||
|
||||
Args:
|
||||
provider_id: Provider ID string. Must have the prefix ``saml.``.
|
||||
idp_entity_id: The SAML IdP entity identifier.
|
||||
sso_url: The SAML IdP SSO URL. Must be a valid URL.
|
||||
x509_certificates: The list of SAML IdP X.509 certificates issued by CA for this provider.
|
||||
Multiple certificates are accepted to prevent outages during IdP key rotation (for
|
||||
example ADFS rotates every 10 days). When the Auth server receives a SAML response, it
|
||||
will match the SAML response with the certificate on record. Otherwise the response is
|
||||
rejected. Developers are expected to manage the certificate updates as keys are
|
||||
rotated.
|
||||
rp_entity_id: The SAML relying party (service provider) entity ID. This is defined by the
|
||||
developer but needs to be provided to the SAML IdP.
|
||||
callback_url: Callback URL string. This is fixed and must always be the same as the OAuth
|
||||
redirect URL provisioned by Firebase Auth, unless a custom authDomain is used.
|
||||
display_name: The user-friendly display name to the current configuration (optional). This
|
||||
name is also used as the provider label in the Cloud Console.
|
||||
enabled: A boolean indicating whether the provider configuration is enabled or disabled
|
||||
(optional). A user cannot sign in using a disabled provider.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
SAMLProviderConfig: The newly created SAML provider config instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If any of the specified input parameters are invalid.
|
||||
FirebaseError: If an error occurs while creating the new SAML provider config.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
return client.create_saml_provider_config(
|
||||
provider_id, idp_entity_id=idp_entity_id, sso_url=sso_url,
|
||||
x509_certificates=x509_certificates, rp_entity_id=rp_entity_id, callback_url=callback_url,
|
||||
display_name=display_name, enabled=enabled)
|
||||
|
||||
|
||||
def update_saml_provider_config(
|
||||
provider_id, idp_entity_id=None, sso_url=None, x509_certificates=None,
|
||||
rp_entity_id=None, callback_url=None, display_name=None, enabled=None, app=None):
|
||||
"""Updates an existing SAML provider config with the given parameters.
|
||||
|
||||
Args:
|
||||
provider_id: Provider ID string. Must have the prefix ``saml.``.
|
||||
idp_entity_id: The SAML IdP entity identifier (optional).
|
||||
sso_url: The SAML IdP SSO URL. Must be a valid URL (optional).
|
||||
x509_certificates: The list of SAML IdP X.509 certificates issued by CA for this
|
||||
provider (optional).
|
||||
rp_entity_id: The SAML relying party entity ID (optional).
|
||||
callback_url: Callback URL string (optional).
|
||||
display_name: The user-friendly display name of the current configuration (optional).
|
||||
Pass ``auth.DELETE_ATTRIBUTE`` to delete the current display name.
|
||||
enabled: A boolean indicating whether the provider configuration is enabled or disabled
|
||||
(optional).
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
SAMLProviderConfig: The updated SAML provider config instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If any of the specified input parameters are invalid.
|
||||
FirebaseError: If an error occurs while updating the SAML provider config.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
return client.update_saml_provider_config(
|
||||
provider_id, idp_entity_id=idp_entity_id, sso_url=sso_url,
|
||||
x509_certificates=x509_certificates, rp_entity_id=rp_entity_id,
|
||||
callback_url=callback_url, display_name=display_name, enabled=enabled)
|
||||
|
||||
|
||||
def delete_saml_provider_config(provider_id, app=None):
|
||||
"""Deletes the ``SAMLProviderConfig`` with the given ID.
|
||||
|
||||
Args:
|
||||
provider_id: Provider ID string.
|
||||
app: An App instance (optional).
|
||||
|
||||
Raises:
|
||||
ValueError: If the provider ID is invalid, empty or does not have ``saml.`` prefix.
|
||||
ConfigurationNotFoundError: If no SAML provider is available with the given identifier.
|
||||
FirebaseError: If an error occurs while deleting the SAML provider.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
client.delete_saml_provider_config(provider_id)
|
||||
|
||||
|
||||
def list_saml_provider_configs(
|
||||
page_token=None, max_results=_auth_providers.MAX_LIST_CONFIGS_RESULTS, app=None):
|
||||
"""Retrieves a page of SAML provider configs from a Firebase project.
|
||||
|
||||
The ``page_token`` argument governs the starting point of the page. The ``max_results``
|
||||
argument governs the maximum number of configs that may be included in the returned
|
||||
page. This function never returns ``None``. If there are no SAML configs in the Firebase
|
||||
project, this returns an empty page.
|
||||
|
||||
Args:
|
||||
page_token: A non-empty page token string, which indicates the starting point of the
|
||||
page (optional). Defaults to ``None``, which will retrieve the first page of users.
|
||||
max_results: A positive integer indicating the maximum number of users to include in
|
||||
the returned page (optional). Defaults to 100, which is also the maximum number
|
||||
allowed.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
ListProviderConfigsPage: A page of SAML provider config instances.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``max_results`` or ``page_token`` are invalid.
|
||||
FirebaseError: If an error occurs while retrieving the SAML provider configs.
|
||||
"""
|
||||
client = _get_client(app)
|
||||
return client.list_saml_provider_configs(page_token, max_results)
|
214
venv/Lib/site-packages/firebase_admin/credentials.py
Normal file
214
venv/Lib/site-packages/firebase_admin/credentials.py
Normal file
|
@ -0,0 +1,214 @@
|
|||
# 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 credentials module."""
|
||||
import collections
|
||||
import json
|
||||
|
||||
import google.auth
|
||||
from google.auth.transport import requests
|
||||
from google.oauth2 import credentials
|
||||
from google.oauth2 import service_account
|
||||
|
||||
|
||||
_request = requests.Request()
|
||||
_scopes = [
|
||||
'https://www.googleapis.com/auth/cloud-platform',
|
||||
'https://www.googleapis.com/auth/datastore',
|
||||
'https://www.googleapis.com/auth/devstorage.read_write',
|
||||
'https://www.googleapis.com/auth/firebase',
|
||||
'https://www.googleapis.com/auth/identitytoolkit',
|
||||
'https://www.googleapis.com/auth/userinfo.email'
|
||||
]
|
||||
|
||||
AccessTokenInfo = collections.namedtuple('AccessTokenInfo', ['access_token', 'expiry'])
|
||||
"""Data included in an OAuth2 access token.
|
||||
|
||||
Contains the access token string and the expiry time. The expirty time is exposed as a
|
||||
``datetime`` value.
|
||||
"""
|
||||
|
||||
|
||||
class Base:
|
||||
"""Provides OAuth2 access tokens for accessing Firebase services."""
|
||||
|
||||
def get_access_token(self):
|
||||
"""Fetches a Google OAuth2 access token using this credential instance.
|
||||
|
||||
Returns:
|
||||
AccessTokenInfo: An access token obtained using the credential.
|
||||
"""
|
||||
google_cred = self.get_credential()
|
||||
google_cred.refresh(_request)
|
||||
return AccessTokenInfo(google_cred.token, google_cred.expiry)
|
||||
|
||||
def get_credential(self):
|
||||
"""Returns the Google credential instance used for authentication."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Certificate(Base):
|
||||
"""A credential initialized from a JSON certificate keyfile."""
|
||||
|
||||
_CREDENTIAL_TYPE = 'service_account'
|
||||
|
||||
def __init__(self, cert):
|
||||
"""Initializes a credential from a Google service account certificate.
|
||||
|
||||
Service account certificates can be downloaded as JSON files from the Firebase console.
|
||||
To instantiate a credential from a certificate file, either specify the file path or a
|
||||
dict representing the parsed contents of the file.
|
||||
|
||||
Args:
|
||||
cert: Path to a certificate file or a dict representing the contents of a certificate.
|
||||
|
||||
Raises:
|
||||
IOError: If the specified certificate file doesn't exist or cannot be read.
|
||||
ValueError: If the specified certificate is invalid.
|
||||
"""
|
||||
super(Certificate, self).__init__()
|
||||
if isinstance(cert, str):
|
||||
with open(cert) as json_file:
|
||||
json_data = json.load(json_file)
|
||||
elif isinstance(cert, dict):
|
||||
json_data = cert
|
||||
else:
|
||||
raise ValueError(
|
||||
'Invalid certificate argument: "{0}". Certificate argument must be a file path, '
|
||||
'or a dict containing the parsed file contents.'.format(cert))
|
||||
|
||||
if json_data.get('type') != self._CREDENTIAL_TYPE:
|
||||
raise ValueError('Invalid service account certificate. Certificate must contain a '
|
||||
'"type" field set to "{0}".'.format(self._CREDENTIAL_TYPE))
|
||||
try:
|
||||
self._g_credential = service_account.Credentials.from_service_account_info(
|
||||
json_data, scopes=_scopes)
|
||||
except ValueError as error:
|
||||
raise ValueError('Failed to initialize a certificate credential. '
|
||||
'Caused by: "{0}"'.format(error))
|
||||
|
||||
@property
|
||||
def project_id(self):
|
||||
return self._g_credential.project_id
|
||||
|
||||
@property
|
||||
def signer(self):
|
||||
return self._g_credential.signer
|
||||
|
||||
@property
|
||||
def service_account_email(self):
|
||||
return self._g_credential.service_account_email
|
||||
|
||||
def get_credential(self):
|
||||
"""Returns the underlying Google credential.
|
||||
|
||||
Returns:
|
||||
google.auth.credentials.Credentials: A Google Auth credential instance."""
|
||||
return self._g_credential
|
||||
|
||||
|
||||
class ApplicationDefault(Base):
|
||||
"""A Google Application Default credential."""
|
||||
|
||||
def __init__(self):
|
||||
"""Creates an instance that will use Application Default credentials.
|
||||
|
||||
The credentials will be lazily initialized when get_credential() or
|
||||
project_id() is called. See those methods for possible errors raised.
|
||||
"""
|
||||
super(ApplicationDefault, self).__init__()
|
||||
self._g_credential = None # Will be lazily-loaded via _load_credential().
|
||||
|
||||
def get_credential(self):
|
||||
"""Returns the underlying Google credential.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.DefaultCredentialsError: If Application Default
|
||||
credentials cannot be initialized in the current environment.
|
||||
Returns:
|
||||
google.auth.credentials.Credentials: A Google Auth credential instance."""
|
||||
self._load_credential()
|
||||
return self._g_credential
|
||||
|
||||
@property
|
||||
def project_id(self):
|
||||
"""Returns the project_id from the underlying Google credential.
|
||||
|
||||
Raises:
|
||||
google.auth.exceptions.DefaultCredentialsError: If Application Default
|
||||
credentials cannot be initialized in the current environment.
|
||||
Returns:
|
||||
str: The project id."""
|
||||
self._load_credential()
|
||||
return self._project_id
|
||||
|
||||
def _load_credential(self):
|
||||
if not self._g_credential:
|
||||
self._g_credential, self._project_id = google.auth.default(scopes=_scopes)
|
||||
|
||||
class RefreshToken(Base):
|
||||
"""A credential initialized from an existing refresh token."""
|
||||
|
||||
_CREDENTIAL_TYPE = 'authorized_user'
|
||||
|
||||
def __init__(self, refresh_token):
|
||||
"""Initializes a credential from a refresh token JSON file.
|
||||
|
||||
The JSON must consist of client_id, client_secert and refresh_token fields. Refresh
|
||||
token files are typically created and managed by the gcloud SDK. To instantiate
|
||||
a credential from a refresh token file, either specify the file path or a dict
|
||||
representing the parsed contents of the file.
|
||||
|
||||
Args:
|
||||
refresh_token: Path to a refresh token file or a dict representing the contents of a
|
||||
refresh token file.
|
||||
|
||||
Raises:
|
||||
IOError: If the specified file doesn't exist or cannot be read.
|
||||
ValueError: If the refresh token configuration is invalid.
|
||||
"""
|
||||
super(RefreshToken, self).__init__()
|
||||
if isinstance(refresh_token, str):
|
||||
with open(refresh_token) as json_file:
|
||||
json_data = json.load(json_file)
|
||||
elif isinstance(refresh_token, dict):
|
||||
json_data = refresh_token
|
||||
else:
|
||||
raise ValueError(
|
||||
'Invalid refresh token argument: "{0}". Refresh token argument must be a file '
|
||||
'path, or a dict containing the parsed file contents.'.format(refresh_token))
|
||||
|
||||
if json_data.get('type') != self._CREDENTIAL_TYPE:
|
||||
raise ValueError('Invalid refresh token configuration. JSON must contain a '
|
||||
'"type" field set to "{0}".'.format(self._CREDENTIAL_TYPE))
|
||||
self._g_credential = credentials.Credentials.from_authorized_user_info(json_data, _scopes)
|
||||
|
||||
@property
|
||||
def client_id(self):
|
||||
return self._g_credential.client_id
|
||||
|
||||
@property
|
||||
def client_secret(self):
|
||||
return self._g_credential.client_secret
|
||||
|
||||
@property
|
||||
def refresh_token(self):
|
||||
return self._g_credential.refresh_token
|
||||
|
||||
def get_credential(self):
|
||||
"""Returns the underlying Google credential.
|
||||
|
||||
Returns:
|
||||
google.auth.credentials.Credentials: A Google Auth credential instance."""
|
||||
return self._g_credential
|
991
venv/Lib/site-packages/firebase_admin/db.py
Normal file
991
venv/Lib/site-packages/firebase_admin/db.py
Normal file
|
@ -0,0 +1,991 @@
|
|||
# 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 Realtime Database module.
|
||||
|
||||
This module contains functions and classes that facilitate interacting with the Firebase Realtime
|
||||
Database. It supports basic data manipulation operations, as well as complex queries such as
|
||||
limit queries and range queries. However, it does not support realtime update notifications. This
|
||||
module uses the Firebase REST API underneath.
|
||||
"""
|
||||
|
||||
import collections
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from urllib import parse
|
||||
|
||||
import google.auth
|
||||
import requests
|
||||
|
||||
import firebase_admin
|
||||
from firebase_admin import exceptions
|
||||
from firebase_admin import _http_client
|
||||
from firebase_admin import _sseclient
|
||||
from firebase_admin import _utils
|
||||
|
||||
|
||||
_DB_ATTRIBUTE = '_database'
|
||||
_INVALID_PATH_CHARACTERS = '[].?#$'
|
||||
_RESERVED_FILTERS = ('$key', '$value', '$priority')
|
||||
_USER_AGENT = 'Firebase/HTTP/{0}/{1}.{2}/AdminPython'.format(
|
||||
firebase_admin.__version__, sys.version_info.major, sys.version_info.minor)
|
||||
_TRANSACTION_MAX_RETRIES = 25
|
||||
_EMULATOR_HOST_ENV_VAR = 'FIREBASE_DATABASE_EMULATOR_HOST'
|
||||
|
||||
|
||||
def reference(path='/', app=None, url=None):
|
||||
"""Returns a database ``Reference`` representing the node at the specified path.
|
||||
|
||||
If no path is specified, this function returns a ``Reference`` that represents the database
|
||||
root. By default, the returned References provide access to the Firebase Database specified at
|
||||
app initialization. To connect to a different database instance in the same Firebase project,
|
||||
specify the ``url`` parameter.
|
||||
|
||||
Args:
|
||||
path: Path to a node in the Firebase realtime database (optional).
|
||||
app: An App instance (optional).
|
||||
url: Base URL of the Firebase Database instance (optional). When specified, takes
|
||||
precedence over the the ``databaseURL`` option set at app initialization.
|
||||
|
||||
Returns:
|
||||
Reference: A newly initialized Reference.
|
||||
|
||||
Raises:
|
||||
ValueError: If the specified path or app is invalid.
|
||||
"""
|
||||
service = _utils.get_app_service(app, _DB_ATTRIBUTE, _DatabaseService)
|
||||
client = service.get_client(url)
|
||||
return Reference(client=client, path=path)
|
||||
|
||||
def _parse_path(path):
|
||||
"""Parses a path string into a set of segments."""
|
||||
if not isinstance(path, str):
|
||||
raise ValueError('Invalid path: "{0}". Path must be a string.'.format(path))
|
||||
if any(ch in path for ch in _INVALID_PATH_CHARACTERS):
|
||||
raise ValueError(
|
||||
'Invalid path: "{0}". Path contains illegal characters.'.format(path))
|
||||
return [seg for seg in path.split('/') if seg]
|
||||
|
||||
|
||||
class Event:
|
||||
"""Represents a realtime update event received from the database."""
|
||||
|
||||
def __init__(self, sse_event):
|
||||
self._sse_event = sse_event
|
||||
self._data = json.loads(sse_event.data)
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
"""Parsed JSON data of this event."""
|
||||
return self._data['data']
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
"""Path of the database reference that triggered this event."""
|
||||
return self._data['path']
|
||||
|
||||
@property
|
||||
def event_type(self):
|
||||
"""Event type string (put, patch)."""
|
||||
return self._sse_event.event_type
|
||||
|
||||
|
||||
class ListenerRegistration:
|
||||
"""Represents the addition of an event listener to a database reference."""
|
||||
|
||||
def __init__(self, callback, sse):
|
||||
"""Initializes a new listener with given parameters.
|
||||
|
||||
This is an internal API. Use the ``db.Reference.listen()`` method to start a
|
||||
new listener.
|
||||
|
||||
Args:
|
||||
callback: The callback function to fire in case of event.
|
||||
sse: A transport session to make requests with.
|
||||
"""
|
||||
self._callback = callback
|
||||
self._sse = sse
|
||||
self._thread = threading.Thread(target=self._start_listen)
|
||||
self._thread.start()
|
||||
|
||||
def _start_listen(self):
|
||||
# iterate the sse client's generator
|
||||
for sse_event in self._sse:
|
||||
# only inject data events
|
||||
if sse_event:
|
||||
self._callback(Event(sse_event))
|
||||
|
||||
def close(self):
|
||||
"""Stops the event listener represented by this registration
|
||||
|
||||
This closes the SSE HTTP connection, and joins the background thread.
|
||||
"""
|
||||
self._sse.close()
|
||||
self._thread.join()
|
||||
|
||||
|
||||
class Reference:
|
||||
"""Reference represents a node in the Firebase realtime database."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Creates a new Reference using the provided parameters.
|
||||
|
||||
This method is for internal use only. Use db.reference() to obtain an instance of
|
||||
Reference.
|
||||
"""
|
||||
self._client = kwargs.get('client')
|
||||
if 'segments' in kwargs:
|
||||
self._segments = kwargs.get('segments')
|
||||
else:
|
||||
self._segments = _parse_path(kwargs.get('path'))
|
||||
self._pathurl = '/' + '/'.join(self._segments)
|
||||
|
||||
@property
|
||||
def key(self):
|
||||
if self._segments:
|
||||
return self._segments[-1]
|
||||
return None
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._pathurl
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
if self._segments:
|
||||
return Reference(client=self._client, segments=self._segments[:-1])
|
||||
return None
|
||||
|
||||
def child(self, path):
|
||||
"""Returns a Reference to the specified child node.
|
||||
|
||||
The path may point to an immediate child of the current Reference, or a deeply nested
|
||||
child. Child paths must not begin with '/'.
|
||||
|
||||
Args:
|
||||
path: Path to the child node.
|
||||
|
||||
Returns:
|
||||
Reference: A database Reference representing the specified child node.
|
||||
|
||||
Raises:
|
||||
ValueError: If the child path is not a string, not well-formed or begins with '/'.
|
||||
"""
|
||||
if not path or not isinstance(path, str):
|
||||
raise ValueError(
|
||||
'Invalid path argument: "{0}". Path must be a non-empty string.'.format(path))
|
||||
if path.startswith('/'):
|
||||
raise ValueError(
|
||||
'Invalid path argument: "{0}". Child path must not start with "/"'.format(path))
|
||||
full_path = self._pathurl + '/' + path
|
||||
return Reference(client=self._client, path=full_path)
|
||||
|
||||
def get(self, etag=False, shallow=False):
|
||||
"""Returns the value, and optionally the ETag, at the current location of the database.
|
||||
|
||||
Args:
|
||||
etag: A boolean indicating whether the Etag value should be returned or not (optional).
|
||||
shallow: A boolean indicating whether to execute a shallow read (optional). Shallow
|
||||
reads do not retrieve the child nodes of the current database location. Cannot be
|
||||
set to True if ``etag`` is also set to True.
|
||||
|
||||
Returns:
|
||||
object: If etag is False returns the decoded JSON value of the current database location.
|
||||
If etag is True, returns a 2-tuple consisting of the decoded JSON value and the Etag
|
||||
associated with the current database location.
|
||||
|
||||
Raises:
|
||||
ValueError: If both ``etag`` and ``shallow`` are set to True.
|
||||
FirebaseError: If an error occurs while communicating with the remote database server.
|
||||
"""
|
||||
if etag:
|
||||
if shallow:
|
||||
raise ValueError('etag and shallow cannot both be set to True.')
|
||||
headers, data = self._client.headers_and_body(
|
||||
'get', self._add_suffix(), headers={'X-Firebase-ETag' : 'true'})
|
||||
return data, headers.get('ETag')
|
||||
|
||||
params = 'shallow=true' if shallow else None
|
||||
return self._client.body('get', self._add_suffix(), params=params)
|
||||
|
||||
def get_if_changed(self, etag):
|
||||
"""Gets data in this location only if the specified ETag does not match.
|
||||
|
||||
Args:
|
||||
etag: The ETag value to be checked against the ETag of the current location.
|
||||
|
||||
Returns:
|
||||
tuple: A 3-tuple consisting of a boolean, a decoded JSON value and an ETag. If the ETag
|
||||
specified by the caller did not match, the boolen value will be True and the JSON
|
||||
and ETag values would reflect the corresponding values in the database. If the ETag
|
||||
matched, the boolean value will be False and the other elements of the tuple will be
|
||||
None.
|
||||
|
||||
Raises:
|
||||
ValueError: If the ETag is not a string.
|
||||
FirebaseError: If an error occurs while communicating with the remote database server.
|
||||
"""
|
||||
if not isinstance(etag, str):
|
||||
raise ValueError('ETag must be a string.')
|
||||
|
||||
resp = self._client.request('get', self._add_suffix(), headers={'if-none-match': etag})
|
||||
if resp.status_code == 304:
|
||||
return False, None, None
|
||||
|
||||
return True, resp.json(), resp.headers.get('ETag')
|
||||
|
||||
def set(self, value):
|
||||
"""Sets the data at this location to the given value.
|
||||
|
||||
The value must be JSON-serializable and not None.
|
||||
|
||||
Args:
|
||||
value: JSON-serializable value to be set at this location.
|
||||
|
||||
Raises:
|
||||
ValueError: If the provided value is None.
|
||||
TypeError: If the value is not JSON-serializable.
|
||||
FirebaseError: If an error occurs while communicating with the remote database server.
|
||||
"""
|
||||
if value is None:
|
||||
raise ValueError('Value must not be None.')
|
||||
self._client.request('put', self._add_suffix(), json=value, params='print=silent')
|
||||
|
||||
def set_if_unchanged(self, expected_etag, value):
|
||||
"""Conditonally sets the data at this location to the given value.
|
||||
|
||||
Sets the data at this location to the given value only if ``expected_etag`` is same as the
|
||||
ETag value in the database.
|
||||
|
||||
Args:
|
||||
expected_etag: Value of ETag we want to check.
|
||||
value: JSON-serializable value to be set at this location.
|
||||
|
||||
Returns:
|
||||
tuple: A 3-tuple consisting of a boolean, a decoded JSON value and an ETag. The boolean
|
||||
indicates whether the set operation was successful or not. The decoded JSON and the
|
||||
ETag corresponds to the latest value in this database location.
|
||||
|
||||
Raises:
|
||||
ValueError: If the value is None, or if expected_etag is not a string.
|
||||
FirebaseError: If an error occurs while communicating with the remote database server.
|
||||
"""
|
||||
# pylint: disable=missing-raises-doc
|
||||
if not isinstance(expected_etag, str):
|
||||
raise ValueError('Expected ETag must be a string.')
|
||||
if value is None:
|
||||
raise ValueError('Value must not be none.')
|
||||
|
||||
try:
|
||||
headers = self._client.headers(
|
||||
'put', self._add_suffix(), json=value, headers={'if-match': expected_etag})
|
||||
return True, value, headers.get('ETag')
|
||||
except exceptions.FailedPreconditionError as error:
|
||||
http_response = error.http_response
|
||||
if http_response is not None and 'ETag' in http_response.headers:
|
||||
etag = http_response.headers['ETag']
|
||||
snapshot = http_response.json()
|
||||
return False, snapshot, etag
|
||||
|
||||
raise error
|
||||
|
||||
def push(self, value=''):
|
||||
"""Creates a new child node.
|
||||
|
||||
The optional value argument can be used to provide an initial value for the child node. If
|
||||
no value is provided, child node will have empty string as the default value.
|
||||
|
||||
Args:
|
||||
value: JSON-serializable initial value for the child node (optional).
|
||||
|
||||
Returns:
|
||||
Reference: A Reference representing the newly created child node.
|
||||
|
||||
Raises:
|
||||
ValueError: If the value is None.
|
||||
TypeError: If the value is not JSON-serializable.
|
||||
FirebaseError: If an error occurs while communicating with the remote database server.
|
||||
"""
|
||||
if value is None:
|
||||
raise ValueError('Value must not be None.')
|
||||
output = self._client.body('post', self._add_suffix(), json=value)
|
||||
push_id = output.get('name')
|
||||
return self.child(push_id)
|
||||
|
||||
def update(self, value):
|
||||
"""Updates the specified child keys of this Reference to the provided values.
|
||||
|
||||
Args:
|
||||
value: A dictionary containing the child keys to update, and their new values.
|
||||
|
||||
Raises:
|
||||
ValueError: If value is empty or not a dictionary.
|
||||
FirebaseError: If an error occurs while communicating with the remote database server.
|
||||
"""
|
||||
if not value or not isinstance(value, dict):
|
||||
raise ValueError('Value argument must be a non-empty dictionary.')
|
||||
if None in value.keys():
|
||||
raise ValueError('Dictionary must not contain None keys.')
|
||||
self._client.request('patch', self._add_suffix(), json=value, params='print=silent')
|
||||
|
||||
def delete(self):
|
||||
"""Deletes this node from the database.
|
||||
|
||||
Raises:
|
||||
FirebaseError: If an error occurs while communicating with the remote database server.
|
||||
"""
|
||||
self._client.request('delete', self._add_suffix())
|
||||
|
||||
def listen(self, callback):
|
||||
"""Registers the ``callback`` function to receive realtime updates.
|
||||
|
||||
The specified callback function will get invoked with ``db.Event`` objects for each
|
||||
realtime update received from the database. It will also get called whenever the SDK
|
||||
reconnects to the server due to network issues or credential expiration. In general,
|
||||
the OAuth2 credentials used to authorize connections to the server expire every hour.
|
||||
Therefore clients should expect the ``callback`` to fire at least once every hour, even if
|
||||
there are no updates in the database.
|
||||
|
||||
This API is based on the event streaming support available in the Firebase REST API. Each
|
||||
call to ``listen()`` starts a new HTTP connection and a background thread. This is an
|
||||
experimental feature. It currently does not honor the auth overrides and timeout settings.
|
||||
Cannot be used in thread-constrained environments like Google App Engine.
|
||||
|
||||
Args:
|
||||
callback: A function to be called when a data change is detected.
|
||||
|
||||
Returns:
|
||||
ListenerRegistration: An object that can be used to stop the event listener.
|
||||
|
||||
Raises:
|
||||
FirebaseError: If an error occurs while starting the initial HTTP connection.
|
||||
"""
|
||||
return self._listen_with_session(callback)
|
||||
|
||||
def transaction(self, transaction_update):
|
||||
"""Atomically modifies the data at this location.
|
||||
|
||||
Unlike a normal ``set()``, which just overwrites the data regardless of its previous state,
|
||||
``transaction()`` is used to modify the existing value to a new value, ensuring there are
|
||||
no conflicts with other clients simultaneously writing to the same location.
|
||||
|
||||
This is accomplished by passing an update function which is used to transform the current
|
||||
value of this reference into a new value. If another client writes to this location before
|
||||
the new value is successfully saved, the update function is called again with the new
|
||||
current value, and the write will be retried. In case of repeated failures, this method
|
||||
will retry the transaction up to 25 times before giving up and raising a
|
||||
TransactionAbortedError. The update function may also force an early abort by raising an
|
||||
exception instead of returning a value.
|
||||
|
||||
Args:
|
||||
transaction_update: A function which will be passed the current data stored at this
|
||||
location. The function should return the new value it would like written. If
|
||||
an exception is raised, the transaction will be aborted, and the data at this
|
||||
location will not be modified. The exceptions raised by this function are
|
||||
propagated to the caller of the transaction method.
|
||||
|
||||
Returns:
|
||||
object: New value of the current database Reference (only if the transaction commits).
|
||||
|
||||
Raises:
|
||||
TransactionAbortedError: If the transaction aborts after exhausting all retry attempts.
|
||||
ValueError: If transaction_update is not a function.
|
||||
"""
|
||||
if not callable(transaction_update):
|
||||
raise ValueError('transaction_update must be a function.')
|
||||
|
||||
tries = 0
|
||||
data, etag = self.get(etag=True)
|
||||
while tries < _TRANSACTION_MAX_RETRIES:
|
||||
new_data = transaction_update(data)
|
||||
success, data, etag = self.set_if_unchanged(etag, new_data)
|
||||
if success:
|
||||
return new_data
|
||||
tries += 1
|
||||
|
||||
raise TransactionAbortedError('Transaction aborted after failed retries.')
|
||||
|
||||
def order_by_child(self, path):
|
||||
"""Returns a Query that orders data by child values.
|
||||
|
||||
Returned Query can be used to set additional parameters, and execute complex database
|
||||
queries (e.g. limit queries, range queries).
|
||||
|
||||
Args:
|
||||
path: Path to a valid child of the current Reference.
|
||||
|
||||
Returns:
|
||||
Query: A database Query instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the child path is not a string, not well-formed or None.
|
||||
"""
|
||||
if path in _RESERVED_FILTERS:
|
||||
raise ValueError('Illegal child path: {0}'.format(path))
|
||||
return Query(order_by=path, client=self._client, pathurl=self._add_suffix())
|
||||
|
||||
def order_by_key(self):
|
||||
"""Creates a Query that orderes data by key.
|
||||
|
||||
Returned Query can be used to set additional parameters, and execute complex database
|
||||
queries (e.g. limit queries, range queries).
|
||||
|
||||
Returns:
|
||||
Query: A database Query instance.
|
||||
"""
|
||||
return Query(order_by='$key', client=self._client, pathurl=self._add_suffix())
|
||||
|
||||
def order_by_value(self):
|
||||
"""Creates a Query that orderes data by value.
|
||||
|
||||
Returned Query can be used to set additional parameters, and execute complex database
|
||||
queries (e.g. limit queries, range queries).
|
||||
|
||||
Returns:
|
||||
Query: A database Query instance.
|
||||
"""
|
||||
return Query(order_by='$value', client=self._client, pathurl=self._add_suffix())
|
||||
|
||||
def _add_suffix(self, suffix='.json'):
|
||||
return self._pathurl + suffix
|
||||
|
||||
def _listen_with_session(self, callback, session=None):
|
||||
url = self._client.base_url + self._add_suffix()
|
||||
if not session:
|
||||
session = self._client.create_listener_session()
|
||||
|
||||
try:
|
||||
sse = _sseclient.SSEClient(url, session)
|
||||
return ListenerRegistration(callback, sse)
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise _Client.handle_rtdb_error(error)
|
||||
|
||||
|
||||
class Query:
|
||||
"""Represents a complex query that can be executed on a Reference.
|
||||
|
||||
Complex queries can consist of up to 2 components: a required ordering constraint, and an
|
||||
optional filtering constraint. At the server, data is first sorted according to the given
|
||||
ordering constraint (e.g. order by child). Then the filtering constraint (e.g. limit, range)
|
||||
is applied on the sorted data to produce the final result. Despite the ordering constraint,
|
||||
the final result is returned by the server as an unordered collection. Therefore the Query
|
||||
interface performs another round of sorting at the client-side before returning the results
|
||||
to the caller. This client-side sorted results are returned to the user as a Python
|
||||
OrderedDict.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
order_by = kwargs.pop('order_by')
|
||||
if not order_by or not isinstance(order_by, str):
|
||||
raise ValueError('order_by field must be a non-empty string')
|
||||
if order_by not in _RESERVED_FILTERS:
|
||||
if order_by.startswith('/'):
|
||||
raise ValueError('Invalid path argument: "{0}". Child path must not start '
|
||||
'with "/"'.format(order_by))
|
||||
segments = _parse_path(order_by)
|
||||
order_by = '/'.join(segments)
|
||||
self._client = kwargs.pop('client')
|
||||
self._pathurl = kwargs.pop('pathurl')
|
||||
self._order_by = order_by
|
||||
self._params = {'orderBy' : json.dumps(order_by)}
|
||||
if kwargs:
|
||||
raise ValueError('Unexpected keyword arguments: {0}'.format(kwargs))
|
||||
|
||||
def limit_to_first(self, limit):
|
||||
"""Creates a query with limit, and anchors it to the start of the window.
|
||||
|
||||
Args:
|
||||
limit: The maximum number of child nodes to return.
|
||||
|
||||
Returns:
|
||||
Query: The updated Query instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the value is not an integer, or set_limit_last() was called previously.
|
||||
"""
|
||||
if not isinstance(limit, int) or limit < 0:
|
||||
raise ValueError('Limit must be a non-negative integer.')
|
||||
if 'limitToLast' in self._params:
|
||||
raise ValueError('Cannot set both first and last limits.')
|
||||
self._params['limitToFirst'] = limit
|
||||
return self
|
||||
|
||||
def limit_to_last(self, limit):
|
||||
"""Creates a query with limit, and anchors it to the end of the window.
|
||||
|
||||
Args:
|
||||
limit: The maximum number of child nodes to return.
|
||||
|
||||
Returns:
|
||||
Query: The updated Query instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the value is not an integer, or set_limit_first() was called previously.
|
||||
"""
|
||||
if not isinstance(limit, int) or limit < 0:
|
||||
raise ValueError('Limit must be a non-negative integer.')
|
||||
if 'limitToFirst' in self._params:
|
||||
raise ValueError('Cannot set both first and last limits.')
|
||||
self._params['limitToLast'] = limit
|
||||
return self
|
||||
|
||||
def start_at(self, start):
|
||||
"""Sets the lower bound for a range query.
|
||||
|
||||
The Query will only return child nodes with a value greater than or equal to the specified
|
||||
value.
|
||||
|
||||
Args:
|
||||
start: JSON-serializable value to start at, inclusive.
|
||||
|
||||
Returns:
|
||||
Query: The updated Query instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the value is ``None``.
|
||||
"""
|
||||
if start is None:
|
||||
raise ValueError('Start value must not be None.')
|
||||
self._params['startAt'] = json.dumps(start)
|
||||
return self
|
||||
|
||||
def end_at(self, end):
|
||||
"""Sets the upper bound for a range query.
|
||||
|
||||
The Query will only return child nodes with a value less than or equal to the specified
|
||||
value.
|
||||
|
||||
Args:
|
||||
end: JSON-serializable value to end at, inclusive.
|
||||
|
||||
Returns:
|
||||
Query: The updated Query instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the value is ``None``.
|
||||
"""
|
||||
if end is None:
|
||||
raise ValueError('End value must not be None.')
|
||||
self._params['endAt'] = json.dumps(end)
|
||||
return self
|
||||
|
||||
def equal_to(self, value):
|
||||
"""Sets an equals constraint on the Query.
|
||||
|
||||
The Query will only return child nodes whose value is equal to the specified value.
|
||||
|
||||
Args:
|
||||
value: JSON-serializable value to query for.
|
||||
|
||||
Returns:
|
||||
Query: The updated Query instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the value is ``None``.
|
||||
"""
|
||||
if value is None:
|
||||
raise ValueError('Equal to value must not be None.')
|
||||
self._params['equalTo'] = json.dumps(value)
|
||||
return self
|
||||
|
||||
@property
|
||||
def _querystr(self):
|
||||
params = []
|
||||
for key in sorted(self._params):
|
||||
params.append('{0}={1}'.format(key, self._params[key]))
|
||||
return '&'.join(params)
|
||||
|
||||
def get(self):
|
||||
"""Executes this Query and returns the results.
|
||||
|
||||
The results will be returned as a sorted list or an OrderedDict.
|
||||
|
||||
Returns:
|
||||
object: Decoded JSON result of the Query.
|
||||
|
||||
Raises:
|
||||
FirebaseError: If an error occurs while communicating with the remote database server.
|
||||
"""
|
||||
result = self._client.body('get', self._pathurl, params=self._querystr)
|
||||
if isinstance(result, (dict, list)) and self._order_by != '$priority':
|
||||
return _Sorter(result, self._order_by).get()
|
||||
return result
|
||||
|
||||
|
||||
class TransactionAbortedError(exceptions.AbortedError):
|
||||
"""A transaction was aborted aftr exceeding the maximum number of retries."""
|
||||
|
||||
def __init__(self, message):
|
||||
exceptions.AbortedError.__init__(self, message)
|
||||
|
||||
|
||||
class _Sorter:
|
||||
"""Helper class for sorting query results."""
|
||||
|
||||
def __init__(self, results, order_by):
|
||||
if isinstance(results, dict):
|
||||
self.dict_input = True
|
||||
entries = [_SortEntry(k, v, order_by) for k, v in results.items()]
|
||||
elif isinstance(results, list):
|
||||
self.dict_input = False
|
||||
entries = [_SortEntry(k, v, order_by) for k, v in enumerate(results)]
|
||||
else:
|
||||
raise ValueError('Sorting not supported for "{0}" object.'.format(type(results)))
|
||||
self.sort_entries = sorted(entries)
|
||||
|
||||
def get(self):
|
||||
if self.dict_input:
|
||||
return collections.OrderedDict([(e.key, e.value) for e in self.sort_entries])
|
||||
|
||||
return [e.value for e in self.sort_entries]
|
||||
|
||||
|
||||
class _SortEntry:
|
||||
"""A wrapper that is capable of sorting items in a dictionary."""
|
||||
|
||||
_type_none = 0
|
||||
_type_bool_false = 1
|
||||
_type_bool_true = 2
|
||||
_type_numeric = 3
|
||||
_type_string = 4
|
||||
_type_object = 5
|
||||
|
||||
def __init__(self, key, value, order_by):
|
||||
self._key = key
|
||||
self._value = value
|
||||
if order_by in ('$key', '$priority'):
|
||||
self._index = key
|
||||
elif order_by == '$value':
|
||||
self._index = value
|
||||
else:
|
||||
self._index = _SortEntry._extract_child(value, order_by)
|
||||
self._index_type = _SortEntry._get_index_type(self._index)
|
||||
|
||||
@property
|
||||
def key(self):
|
||||
return self._key
|
||||
|
||||
@property
|
||||
def index(self):
|
||||
return self._index
|
||||
|
||||
@property
|
||||
def index_type(self):
|
||||
return self._index_type
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self._value
|
||||
|
||||
@classmethod
|
||||
def _get_index_type(cls, index):
|
||||
"""Assigns an integer code to the type of the index.
|
||||
|
||||
The index type determines how differently typed values are sorted. This ordering is based
|
||||
on https://firebase.google.com/docs/database/rest/retrieve-data#section-rest-ordered-data
|
||||
"""
|
||||
if index is None:
|
||||
return cls._type_none
|
||||
if isinstance(index, bool) and not index:
|
||||
return cls._type_bool_false
|
||||
if isinstance(index, bool) and index:
|
||||
return cls._type_bool_true
|
||||
if isinstance(index, (int, float)):
|
||||
return cls._type_numeric
|
||||
if isinstance(index, str):
|
||||
return cls._type_string
|
||||
|
||||
return cls._type_object
|
||||
|
||||
@classmethod
|
||||
def _extract_child(cls, value, path):
|
||||
segments = path.split('/')
|
||||
current = value
|
||||
for segment in segments:
|
||||
if isinstance(current, dict):
|
||||
current = current.get(segment)
|
||||
else:
|
||||
return None
|
||||
return current
|
||||
|
||||
def _compare(self, other):
|
||||
"""Compares two _SortEntry instances.
|
||||
|
||||
If the indices have the same numeric or string type, compare them directly. Ties are
|
||||
broken by comparing the keys. If the indices have the same type, but are neither numeric
|
||||
nor string, compare the keys. In all other cases compare based on the ordering provided
|
||||
by index types.
|
||||
"""
|
||||
self_key, other_key = self.index_type, other.index_type
|
||||
if self_key == other_key:
|
||||
if self_key in (self._type_numeric, self._type_string) and self.index != other.index:
|
||||
self_key, other_key = self.index, other.index
|
||||
else:
|
||||
self_key, other_key = self.key, other.key
|
||||
|
||||
if self_key < other_key:
|
||||
return -1
|
||||
if self_key > other_key:
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
def __lt__(self, other):
|
||||
return self._compare(other) < 0
|
||||
|
||||
def __le__(self, other):
|
||||
return self._compare(other) <= 0
|
||||
|
||||
def __gt__(self, other):
|
||||
return self._compare(other) > 0
|
||||
|
||||
def __ge__(self, other):
|
||||
return self._compare(other) >= 0
|
||||
|
||||
def __eq__(self, other):
|
||||
return self._compare(other) == 0
|
||||
|
||||
|
||||
class _DatabaseService:
|
||||
"""Service that maintains a collection of database clients."""
|
||||
|
||||
_DEFAULT_AUTH_OVERRIDE = '_admin_'
|
||||
|
||||
def __init__(self, app):
|
||||
self._credential = app.credential
|
||||
db_url = app.options.get('databaseURL')
|
||||
if db_url:
|
||||
_DatabaseService._parse_db_url(db_url) # Just for validation.
|
||||
self._db_url = db_url
|
||||
else:
|
||||
self._db_url = None
|
||||
auth_override = _DatabaseService._get_auth_override(app)
|
||||
if auth_override not in (self._DEFAULT_AUTH_OVERRIDE, {}):
|
||||
self._auth_override = json.dumps(auth_override, separators=(',', ':'))
|
||||
else:
|
||||
self._auth_override = None
|
||||
self._timeout = app.options.get('httpTimeout', _http_client.DEFAULT_TIMEOUT_SECONDS)
|
||||
self._clients = {}
|
||||
|
||||
emulator_host = os.environ.get(_EMULATOR_HOST_ENV_VAR)
|
||||
if emulator_host:
|
||||
if '//' in emulator_host:
|
||||
raise ValueError(
|
||||
'Invalid {0}: "{1}". It must follow format "host:port".'.format(
|
||||
_EMULATOR_HOST_ENV_VAR, emulator_host))
|
||||
self._emulator_host = emulator_host
|
||||
else:
|
||||
self._emulator_host = None
|
||||
|
||||
def get_client(self, db_url=None):
|
||||
"""Creates a client based on the db_url. Clients may be cached."""
|
||||
if db_url is None:
|
||||
db_url = self._db_url
|
||||
|
||||
base_url, namespace = _DatabaseService._parse_db_url(db_url, self._emulator_host)
|
||||
if base_url == 'https://{0}.firebaseio.com'.format(namespace):
|
||||
# Production base_url. No need to specify namespace in query params.
|
||||
params = {}
|
||||
credential = self._credential.get_credential()
|
||||
else:
|
||||
# Emulator base_url. Use fake credentials and specify ?ns=foo in query params.
|
||||
credential = _EmulatorAdminCredentials()
|
||||
params = {'ns': namespace}
|
||||
if self._auth_override:
|
||||
params['auth_variable_override'] = self._auth_override
|
||||
|
||||
client_cache_key = (base_url, json.dumps(params, sort_keys=True))
|
||||
if client_cache_key not in self._clients:
|
||||
client = _Client(credential, base_url, self._timeout, params)
|
||||
self._clients[client_cache_key] = client
|
||||
return self._clients[client_cache_key]
|
||||
|
||||
@classmethod
|
||||
def _parse_db_url(cls, url, emulator_host=None):
|
||||
"""Parses (base_url, namespace) from a database URL.
|
||||
|
||||
The input can be either a production URL (https://foo-bar.firebaseio.com/)
|
||||
or an Emulator URL (http://localhost:8080/?ns=foo-bar). In case of Emulator
|
||||
URL, the namespace is extracted from the query param ns. The resulting
|
||||
base_url never includes query params.
|
||||
|
||||
If url is a production URL and emulator_host is specified, the result
|
||||
base URL will use emulator_host instead. emulator_host is ignored
|
||||
if url is already an emulator URL.
|
||||
"""
|
||||
if not url or not isinstance(url, str):
|
||||
raise ValueError(
|
||||
'Invalid database URL: "{0}". Database URL must be a non-empty '
|
||||
'URL string.'.format(url))
|
||||
parsed_url = parse.urlparse(url)
|
||||
if parsed_url.netloc.endswith('.firebaseio.com'):
|
||||
return cls._parse_production_url(parsed_url, emulator_host)
|
||||
|
||||
return cls._parse_emulator_url(parsed_url)
|
||||
|
||||
@classmethod
|
||||
def _parse_production_url(cls, parsed_url, emulator_host):
|
||||
"""Parses production URL like https://foo-bar.firebaseio.com/"""
|
||||
if parsed_url.scheme != 'https':
|
||||
raise ValueError(
|
||||
'Invalid database URL scheme: "{0}". Database URL must be an HTTPS URL.'.format(
|
||||
parsed_url.scheme))
|
||||
namespace = parsed_url.netloc.split('.')[0]
|
||||
if not namespace:
|
||||
raise ValueError(
|
||||
'Invalid database URL: "{0}". Database URL must be a valid URL to a '
|
||||
'Firebase Realtime Database instance.'.format(parsed_url.geturl()))
|
||||
|
||||
if emulator_host:
|
||||
base_url = 'http://{0}'.format(emulator_host)
|
||||
else:
|
||||
base_url = 'https://{0}'.format(parsed_url.netloc)
|
||||
return base_url, namespace
|
||||
|
||||
@classmethod
|
||||
def _parse_emulator_url(cls, parsed_url):
|
||||
"""Parses emulator URL like http://localhost:8080/?ns=foo-bar"""
|
||||
query_ns = parse.parse_qs(parsed_url.query).get('ns')
|
||||
if parsed_url.scheme != 'http' or (not query_ns or len(query_ns) != 1 or not query_ns[0]):
|
||||
raise ValueError(
|
||||
'Invalid database URL: "{0}". Database URL must be a valid URL to a '
|
||||
'Firebase Realtime Database instance.'.format(parsed_url.geturl()))
|
||||
|
||||
namespace = query_ns[0]
|
||||
base_url = '{0}://{1}'.format(parsed_url.scheme, parsed_url.netloc)
|
||||
return base_url, namespace
|
||||
|
||||
@classmethod
|
||||
def _get_auth_override(cls, app):
|
||||
auth_override = app.options.get('databaseAuthVariableOverride', cls._DEFAULT_AUTH_OVERRIDE)
|
||||
if auth_override == cls._DEFAULT_AUTH_OVERRIDE or auth_override is None:
|
||||
return auth_override
|
||||
if not isinstance(auth_override, dict):
|
||||
raise ValueError('Invalid databaseAuthVariableOverride option: "{0}". Override '
|
||||
'value must be a dict or None.'.format(auth_override))
|
||||
|
||||
return auth_override
|
||||
|
||||
def close(self):
|
||||
for value in self._clients.values():
|
||||
value.close()
|
||||
self._clients = {}
|
||||
|
||||
|
||||
class _Client(_http_client.JsonHttpClient):
|
||||
"""HTTP client used to make REST calls.
|
||||
|
||||
_Client maintains an HTTP session, and handles authenticating HTTP requests along with
|
||||
marshalling and unmarshalling of JSON data.
|
||||
"""
|
||||
|
||||
def __init__(self, credential, base_url, timeout, params=None):
|
||||
"""Creates a new _Client from the given parameters.
|
||||
|
||||
This exists primarily to enable testing. For regular use, obtain _Client instances by
|
||||
calling the from_app() class method.
|
||||
|
||||
Args:
|
||||
credential: A Google credential that can be used to authenticate requests.
|
||||
base_url: A URL prefix to be added to all outgoing requests. This is typically the
|
||||
Firebase Realtime Database URL.
|
||||
timeout: HTTP request timeout in seconds. If set to None connections will never
|
||||
timeout, which is the default behavior of the underlying requests library.
|
||||
params: Dict of query parameters to add to all outgoing requests.
|
||||
"""
|
||||
super().__init__(
|
||||
credential=credential, base_url=base_url,
|
||||
timeout=timeout, headers={'User-Agent': _USER_AGENT})
|
||||
self.credential = credential
|
||||
self.params = params if params else {}
|
||||
|
||||
def request(self, method, url, **kwargs):
|
||||
"""Makes an HTTP call using the Python requests library.
|
||||
|
||||
Extends the request() method of the parent JsonHttpClient class. Handles default
|
||||
params like auth overrides, and low-level exceptions.
|
||||
|
||||
Args:
|
||||
method: HTTP method name as a string (e.g. get, post).
|
||||
url: URL path of the remote endpoint. This will be appended to the server's base URL.
|
||||
kwargs: An additional set of keyword arguments to be passed into requests API
|
||||
(e.g. json, params).
|
||||
|
||||
Returns:
|
||||
Response: An HTTP response object.
|
||||
|
||||
Raises:
|
||||
FirebaseError: If an error occurs while making the HTTP call.
|
||||
"""
|
||||
query = '&'.join('{0}={1}'.format(key, self.params[key]) for key in self.params)
|
||||
extra_params = kwargs.get('params')
|
||||
if extra_params:
|
||||
if query:
|
||||
query = extra_params + '&' + query
|
||||
else:
|
||||
query = extra_params
|
||||
kwargs['params'] = query
|
||||
|
||||
try:
|
||||
return super(_Client, self).request(method, url, **kwargs)
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise _Client.handle_rtdb_error(error)
|
||||
|
||||
def create_listener_session(self):
|
||||
return _sseclient.KeepAuthSession(self.credential)
|
||||
|
||||
@classmethod
|
||||
def handle_rtdb_error(cls, error):
|
||||
"""Converts an error encountered while calling RTDB into a FirebaseError."""
|
||||
if error.response is None:
|
||||
return _utils.handle_requests_error(error)
|
||||
|
||||
message = cls._extract_error_message(error.response)
|
||||
return _utils.handle_requests_error(error, message=message)
|
||||
|
||||
@classmethod
|
||||
def _extract_error_message(cls, response):
|
||||
"""Extracts an error message from an error response.
|
||||
|
||||
If the server has sent a JSON response with an 'error' field, which is the typical
|
||||
behavior of the Realtime Database REST API, parses the response to retrieve the error
|
||||
message. If the server has sent a non-JSON response, returns the full response
|
||||
as the error message.
|
||||
"""
|
||||
message = None
|
||||
try:
|
||||
# RTDB error format: {"error": "text message"}
|
||||
data = response.json()
|
||||
if isinstance(data, dict):
|
||||
message = data.get('error')
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if not message:
|
||||
message = 'Unexpected response from database: {0}'.format(response.content.decode())
|
||||
|
||||
return message
|
||||
|
||||
# Temporarily disable the lint rule. For more information see:
|
||||
# https://github.com/googleapis/google-auth-library-python/pull/561
|
||||
# pylint: disable=abstract-method
|
||||
class _EmulatorAdminCredentials(google.auth.credentials.Credentials):
|
||||
def __init__(self):
|
||||
google.auth.credentials.Credentials.__init__(self)
|
||||
self.token = 'owner'
|
||||
|
||||
def refresh(self, request):
|
||||
pass
|
237
venv/Lib/site-packages/firebase_admin/exceptions.py
Normal file
237
venv/Lib/site-packages/firebase_admin/exceptions.py
Normal file
|
@ -0,0 +1,237 @@
|
|||
# Copyright 2019 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 Exceptions module.
|
||||
|
||||
This module defines the base types for exceptions and the platform-wide error codes as outlined in
|
||||
https://cloud.google.com/apis/design/errors.
|
||||
|
||||
:class:`FirebaseError` is the parent class of all exceptions raised by the Admin SDK. It contains
|
||||
the ``code``, ``http_response`` and ``cause`` properties common to all Firebase exception types.
|
||||
Each exception also carries a message that outlines what went wrong. This can be logged for
|
||||
audit or debugging purposes.
|
||||
|
||||
When calling an Admin SDK API, developers can catch the parent ``FirebaseError`` and
|
||||
inspect its ``code`` to implement fine-grained error handling. Alternatively, developers can
|
||||
catch one or more subtypes of ``FirebaseError``. Under normal conditions, any given API can raise
|
||||
only a small subset of the available exception subtypes. However, the SDK also exposes rare error
|
||||
conditions like connection timeouts and other I/O errors as instances of ``FirebaseError``.
|
||||
Therefore it is always a good idea to have a handler specified for ``FirebaseError``, after all the
|
||||
subtype error handlers.
|
||||
"""
|
||||
|
||||
|
||||
#: Error code for ``InvalidArgumentError`` type.
|
||||
INVALID_ARGUMENT = 'INVALID_ARGUMENT'
|
||||
|
||||
#: Error code for ``FailedPreconditionError`` type.
|
||||
FAILED_PRECONDITION = 'FAILED_PRECONDITION'
|
||||
|
||||
#: Error code for ``OutOfRangeError`` type.
|
||||
OUT_OF_RANGE = 'OUT_OF_RANGE'
|
||||
|
||||
#: Error code for ``UnauthenticatedError`` type.
|
||||
UNAUTHENTICATED = 'UNAUTHENTICATED'
|
||||
|
||||
#: Error code for ``PermissionDeniedError`` type.
|
||||
PERMISSION_DENIED = 'PERMISSION_DENIED'
|
||||
|
||||
#: Error code for ``NotFoundError`` type.
|
||||
NOT_FOUND = 'NOT_FOUND'
|
||||
|
||||
#: Error code for ``ConflictError`` type.
|
||||
CONFLICT = 'CONFLICT'
|
||||
|
||||
#: Error code for ``AbortedError`` type.
|
||||
ABORTED = 'ABORTED'
|
||||
|
||||
#: Error code for ``AlreadyExistsError`` type.
|
||||
ALREADY_EXISTS = 'ALREADY_EXISTS'
|
||||
|
||||
#: Error code for ``ResourceExhaustedError`` type.
|
||||
RESOURCE_EXHAUSTED = 'RESOURCE_EXHAUSTED'
|
||||
|
||||
#: Error code for ``CancelledError`` type.
|
||||
CANCELLED = 'CANCELLED'
|
||||
|
||||
#: Error code for ``DataLossError`` type.
|
||||
DATA_LOSS = 'DATA_LOSS'
|
||||
|
||||
#: Error code for ``UnknownError`` type.
|
||||
UNKNOWN = 'UNKNOWN'
|
||||
|
||||
#: Error code for ``InternalError`` type.
|
||||
INTERNAL = 'INTERNAL'
|
||||
|
||||
#: Error code for ``UnavailableError`` type.
|
||||
UNAVAILABLE = 'UNAVAILABLE'
|
||||
|
||||
#: Error code for ``DeadlineExceededError`` type.
|
||||
DEADLINE_EXCEEDED = 'DEADLINE_EXCEEDED'
|
||||
|
||||
|
||||
class FirebaseError(Exception):
|
||||
"""Base class for all errors raised by the Admin SDK.
|
||||
|
||||
Args:
|
||||
code: A string error code that represents the type of the exception. Possible error
|
||||
codes are defined in https://cloud.google.com/apis/design/errors#handling_errors.
|
||||
message: A human-readable error message string.
|
||||
cause: The exception that caused this error (optional).
|
||||
http_response: If this error was caused by an HTTP error response, this property is
|
||||
set to the ``requests.Response`` object that represents the HTTP response (optional).
|
||||
See https://2.python-requests.org/en/master/api/#requests.Response for details of
|
||||
this object.
|
||||
"""
|
||||
|
||||
def __init__(self, code, message, cause=None, http_response=None):
|
||||
Exception.__init__(self, message)
|
||||
self._code = code
|
||||
self._cause = cause
|
||||
self._http_response = http_response
|
||||
|
||||
@property
|
||||
def code(self):
|
||||
return self._code
|
||||
|
||||
@property
|
||||
def cause(self):
|
||||
return self._cause
|
||||
|
||||
@property
|
||||
def http_response(self):
|
||||
return self._http_response
|
||||
|
||||
|
||||
class InvalidArgumentError(FirebaseError):
|
||||
"""Client specified an invalid argument."""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
FirebaseError.__init__(self, INVALID_ARGUMENT, message, cause, http_response)
|
||||
|
||||
|
||||
class FailedPreconditionError(FirebaseError):
|
||||
"""Request can not be executed in the current system state, such as deleting a non-empty
|
||||
directory."""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
FirebaseError.__init__(self, FAILED_PRECONDITION, message, cause, http_response)
|
||||
|
||||
|
||||
class OutOfRangeError(FirebaseError):
|
||||
"""Client specified an invalid range."""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
FirebaseError.__init__(self, OUT_OF_RANGE, message, cause, http_response)
|
||||
|
||||
|
||||
class UnauthenticatedError(FirebaseError):
|
||||
"""Request not authenticated due to missing, invalid, or expired OAuth token."""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
FirebaseError.__init__(self, UNAUTHENTICATED, message, cause, http_response)
|
||||
|
||||
|
||||
class PermissionDeniedError(FirebaseError):
|
||||
"""Client does not have sufficient permission.
|
||||
|
||||
This can happen because the OAuth token does not have the right scopes, the client doesn't
|
||||
have permission, or the API has not been enabled for the client project.
|
||||
"""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
FirebaseError.__init__(self, PERMISSION_DENIED, message, cause, http_response)
|
||||
|
||||
|
||||
class NotFoundError(FirebaseError):
|
||||
"""A specified resource is not found, or the request is rejected by undisclosed reasons, such
|
||||
as whitelisting."""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
FirebaseError.__init__(self, NOT_FOUND, message, cause, http_response)
|
||||
|
||||
|
||||
class ConflictError(FirebaseError):
|
||||
"""Concurrency conflict, such as read-modify-write conflict."""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
FirebaseError.__init__(self, CONFLICT, message, cause, http_response)
|
||||
|
||||
|
||||
class AbortedError(FirebaseError):
|
||||
"""Concurrency conflict, such as read-modify-write conflict."""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
FirebaseError.__init__(self, ABORTED, message, cause, http_response)
|
||||
|
||||
|
||||
class AlreadyExistsError(FirebaseError):
|
||||
"""The resource that a client tried to create already exists."""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
FirebaseError.__init__(self, ALREADY_EXISTS, message, cause, http_response)
|
||||
|
||||
|
||||
class ResourceExhaustedError(FirebaseError):
|
||||
"""Either out of resource quota or reaching rate limiting."""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
FirebaseError.__init__(self, RESOURCE_EXHAUSTED, message, cause, http_response)
|
||||
|
||||
|
||||
class CancelledError(FirebaseError):
|
||||
"""Request cancelled by the client."""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
FirebaseError.__init__(self, CANCELLED, message, cause, http_response)
|
||||
|
||||
|
||||
class DataLossError(FirebaseError):
|
||||
"""Unrecoverable data loss or data corruption."""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
FirebaseError.__init__(self, DATA_LOSS, message, cause, http_response)
|
||||
|
||||
|
||||
class UnknownError(FirebaseError):
|
||||
"""Unknown server error."""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
FirebaseError.__init__(self, UNKNOWN, message, cause, http_response)
|
||||
|
||||
|
||||
class InternalError(FirebaseError):
|
||||
"""Internal server error."""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
FirebaseError.__init__(self, INTERNAL, message, cause, http_response)
|
||||
|
||||
|
||||
class UnavailableError(FirebaseError):
|
||||
"""Service unavailable. Typically the server is down."""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
FirebaseError.__init__(self, UNAVAILABLE, message, cause, http_response)
|
||||
|
||||
|
||||
class DeadlineExceededError(FirebaseError):
|
||||
"""Request deadline exceeded.
|
||||
|
||||
This will happen only if the caller sets a deadline that is shorter than the method's
|
||||
default deadline (i.e. requested deadline is not enough for the server to process the
|
||||
request) and the request did not finish within the deadline.
|
||||
"""
|
||||
|
||||
def __init__(self, message, cause=None, http_response=None):
|
||||
FirebaseError.__init__(self, DEADLINE_EXCEEDED, message, cause, http_response)
|
76
venv/Lib/site-packages/firebase_admin/firestore.py
Normal file
76
venv/Lib/site-packages/firebase_admin/firestore.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
# 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.
|
||||
|
||||
"""Cloud Firestore module.
|
||||
|
||||
This module contains utilities for accessing the Google Cloud Firestore databases associated with
|
||||
Firebase apps. This requires the ``google-cloud-firestore`` Python module.
|
||||
"""
|
||||
|
||||
try:
|
||||
from google.cloud import firestore # pylint: disable=import-error,no-name-in-module
|
||||
existing = globals().keys()
|
||||
for key, value in firestore.__dict__.items():
|
||||
if not key.startswith('_') and key not in existing:
|
||||
globals()[key] = value
|
||||
except ImportError:
|
||||
raise ImportError('Failed to import the Cloud Firestore library for Python. Make sure '
|
||||
'to install the "google-cloud-firestore" module.')
|
||||
|
||||
from firebase_admin import _utils
|
||||
|
||||
|
||||
_FIRESTORE_ATTRIBUTE = '_firestore'
|
||||
|
||||
|
||||
def client(app=None):
|
||||
"""Returns a client that can be used to interact with Google Cloud Firestore.
|
||||
|
||||
Args:
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
google.cloud.firestore.Firestore: A `Firestore Client`_.
|
||||
|
||||
Raises:
|
||||
ValueError: If a project ID is not specified either via options, credentials or
|
||||
environment variables, or if the specified project ID is not a valid string.
|
||||
|
||||
.. _Firestore Client: https://googlecloudplatform.github.io/google-cloud-python/latest\
|
||||
/firestore/client.html
|
||||
"""
|
||||
fs_client = _utils.get_app_service(app, _FIRESTORE_ATTRIBUTE, _FirestoreClient.from_app)
|
||||
return fs_client.get()
|
||||
|
||||
|
||||
class _FirestoreClient:
|
||||
"""Holds a Google Cloud Firestore client instance."""
|
||||
|
||||
def __init__(self, credentials, project):
|
||||
self._client = firestore.Client(credentials=credentials, project=project)
|
||||
|
||||
def get(self):
|
||||
return self._client
|
||||
|
||||
@classmethod
|
||||
def from_app(cls, app):
|
||||
"""Creates a new _FirestoreClient for the specified app."""
|
||||
credentials = app.credential.get_credential()
|
||||
project = app.project_id
|
||||
if not project:
|
||||
raise ValueError(
|
||||
'Project ID is required to access Firestore. Either set the projectId option, '
|
||||
'or use service account credentials. Alternatively, set the GOOGLE_CLOUD_PROJECT '
|
||||
'environment variable.')
|
||||
return _FirestoreClient(credentials, project)
|
99
venv/Lib/site-packages/firebase_admin/instance_id.py
Normal file
99
venv/Lib/site-packages/firebase_admin/instance_id.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
# 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 Instance ID module.
|
||||
|
||||
This module enables deleting instance IDs associated with Firebase projects.
|
||||
"""
|
||||
|
||||
import requests
|
||||
|
||||
from firebase_admin import _http_client
|
||||
from firebase_admin import _utils
|
||||
|
||||
|
||||
_IID_SERVICE_URL = 'https://console.firebase.google.com/v1/'
|
||||
_IID_ATTRIBUTE = '_iid'
|
||||
|
||||
|
||||
def _get_iid_service(app):
|
||||
return _utils.get_app_service(app, _IID_ATTRIBUTE, _InstanceIdService)
|
||||
|
||||
|
||||
def delete_instance_id(instance_id, app=None):
|
||||
"""Deletes the specified instance ID and the associated data from Firebase.
|
||||
|
||||
Note that Google Analytics for Firebase uses its own form of Instance ID to
|
||||
keep track of analytics data. Therefore deleting a regular Instance ID does
|
||||
not delete Analytics data. See `Delete an Instance ID`_ for more information.
|
||||
|
||||
Args:
|
||||
instance_id: A non-empty instance ID string.
|
||||
app: An App instance (optional).
|
||||
|
||||
Raises:
|
||||
InstanceIdError: If an error occurs while invoking the backend instance ID service.
|
||||
ValueError: If the specified instance ID or app is invalid.
|
||||
|
||||
.. _Delete an Instance ID: https://firebase.google.com/support/privacy\
|
||||
/manage-iids#delete_an_instance_id
|
||||
"""
|
||||
_get_iid_service(app).delete_instance_id(instance_id)
|
||||
|
||||
|
||||
class _InstanceIdService:
|
||||
"""Provides methods for interacting with the remote instance ID service."""
|
||||
|
||||
error_codes = {
|
||||
400: 'Malformed instance ID argument.',
|
||||
401: 'Request not authorized.',
|
||||
403: 'Project does not match instance ID or the client does not have '
|
||||
'sufficient privileges.',
|
||||
404: 'Failed to find the instance ID.',
|
||||
409: 'Already deleted.',
|
||||
429: 'Request throttled out by the backend server.',
|
||||
500: 'Internal server error.',
|
||||
503: 'Backend servers are over capacity. Try again later.'
|
||||
}
|
||||
|
||||
def __init__(self, app):
|
||||
project_id = app.project_id
|
||||
if not project_id:
|
||||
raise ValueError(
|
||||
'Project ID is required to access Instance ID service. Either set the projectId '
|
||||
'option, or use service account credentials. Alternatively, set the '
|
||||
'GOOGLE_CLOUD_PROJECT environment variable.')
|
||||
self._project_id = project_id
|
||||
self._client = _http_client.JsonHttpClient(
|
||||
credential=app.credential.get_credential(), base_url=_IID_SERVICE_URL)
|
||||
|
||||
def delete_instance_id(self, instance_id):
|
||||
if not isinstance(instance_id, str) or not instance_id:
|
||||
raise ValueError('Instance ID must be a non-empty string.')
|
||||
path = 'project/{0}/instanceId/{1}'.format(self._project_id, instance_id)
|
||||
try:
|
||||
self._client.request('delete', path)
|
||||
except requests.exceptions.RequestException as error:
|
||||
msg = self._extract_message(instance_id, error)
|
||||
raise _utils.handle_requests_error(error, msg)
|
||||
|
||||
def _extract_message(self, instance_id, error):
|
||||
if error.response is None:
|
||||
return None
|
||||
status = error.response.status_code
|
||||
msg = self.error_codes.get(status)
|
||||
if msg:
|
||||
return 'Instance ID "{0}": {1}'.format(instance_id, msg)
|
||||
|
||||
return 'Instance ID "{0}": {1}'.format(instance_id, error)
|
495
venv/Lib/site-packages/firebase_admin/messaging.py
Normal file
495
venv/Lib/site-packages/firebase_admin/messaging.py
Normal file
|
@ -0,0 +1,495 @@
|
|||
# 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 Cloud Messaging module."""
|
||||
|
||||
import json
|
||||
|
||||
import googleapiclient
|
||||
from googleapiclient import http
|
||||
from googleapiclient import _auth
|
||||
import requests
|
||||
|
||||
import firebase_admin
|
||||
from firebase_admin import _http_client
|
||||
from firebase_admin import _messaging_encoder
|
||||
from firebase_admin import _messaging_utils
|
||||
from firebase_admin import _utils
|
||||
|
||||
|
||||
_MESSAGING_ATTRIBUTE = '_messaging'
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AndroidConfig',
|
||||
'AndroidFCMOptions',
|
||||
'AndroidNotification',
|
||||
'APNSConfig',
|
||||
'APNSFCMOptions',
|
||||
'APNSPayload',
|
||||
'Aps',
|
||||
'ApsAlert',
|
||||
'BatchResponse',
|
||||
'CriticalSound',
|
||||
'ErrorInfo',
|
||||
'FCMOptions',
|
||||
'LightSettings',
|
||||
'Message',
|
||||
'MulticastMessage',
|
||||
'Notification',
|
||||
'QuotaExceededError',
|
||||
'SenderIdMismatchError',
|
||||
'SendResponse',
|
||||
'ThirdPartyAuthError',
|
||||
'TopicManagementResponse',
|
||||
'UnregisteredError',
|
||||
'WebpushConfig',
|
||||
'WebpushFCMOptions',
|
||||
'WebpushNotification',
|
||||
'WebpushNotificationAction',
|
||||
|
||||
'send',
|
||||
'send_all',
|
||||
'send_multicast',
|
||||
'subscribe_to_topic',
|
||||
'unsubscribe_from_topic',
|
||||
]
|
||||
|
||||
|
||||
AndroidConfig = _messaging_utils.AndroidConfig
|
||||
AndroidFCMOptions = _messaging_utils.AndroidFCMOptions
|
||||
AndroidNotification = _messaging_utils.AndroidNotification
|
||||
APNSConfig = _messaging_utils.APNSConfig
|
||||
APNSFCMOptions = _messaging_utils.APNSFCMOptions
|
||||
APNSPayload = _messaging_utils.APNSPayload
|
||||
Aps = _messaging_utils.Aps
|
||||
ApsAlert = _messaging_utils.ApsAlert
|
||||
CriticalSound = _messaging_utils.CriticalSound
|
||||
FCMOptions = _messaging_utils.FCMOptions
|
||||
LightSettings = _messaging_utils.LightSettings
|
||||
Message = _messaging_encoder.Message
|
||||
MulticastMessage = _messaging_encoder.MulticastMessage
|
||||
Notification = _messaging_utils.Notification
|
||||
WebpushConfig = _messaging_utils.WebpushConfig
|
||||
WebpushFCMOptions = _messaging_utils.WebpushFCMOptions
|
||||
WebpushNotification = _messaging_utils.WebpushNotification
|
||||
WebpushNotificationAction = _messaging_utils.WebpushNotificationAction
|
||||
|
||||
QuotaExceededError = _messaging_utils.QuotaExceededError
|
||||
SenderIdMismatchError = _messaging_utils.SenderIdMismatchError
|
||||
ThirdPartyAuthError = _messaging_utils.ThirdPartyAuthError
|
||||
UnregisteredError = _messaging_utils.UnregisteredError
|
||||
|
||||
|
||||
def _get_messaging_service(app):
|
||||
return _utils.get_app_service(app, _MESSAGING_ATTRIBUTE, _MessagingService)
|
||||
|
||||
def send(message, dry_run=False, app=None):
|
||||
"""Sends the given message via Firebase Cloud Messaging (FCM).
|
||||
|
||||
If the ``dry_run`` mode is enabled, the message will not be actually delivered to the
|
||||
recipients. Instead FCM performs all the usual validations, and emulates the send operation.
|
||||
|
||||
Args:
|
||||
message: An instance of ``messaging.Message``.
|
||||
dry_run: A boolean indicating whether to run the operation in dry run mode (optional).
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
string: A message ID string that uniquely identifies the sent the message.
|
||||
|
||||
Raises:
|
||||
FirebaseError: If an error occurs while sending the message to the FCM service.
|
||||
ValueError: If the input arguments are invalid.
|
||||
"""
|
||||
return _get_messaging_service(app).send(message, dry_run)
|
||||
|
||||
def send_all(messages, dry_run=False, app=None):
|
||||
"""Sends the given list of messages via Firebase Cloud Messaging as a single batch.
|
||||
|
||||
If the ``dry_run`` mode is enabled, the message will not be actually delivered to the
|
||||
recipients. Instead FCM performs all the usual validations, and emulates the send operation.
|
||||
|
||||
Args:
|
||||
messages: A list of ``messaging.Message`` instances.
|
||||
dry_run: A boolean indicating whether to run the operation in dry run mode (optional).
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
BatchResponse: A ``messaging.BatchResponse`` instance.
|
||||
|
||||
Raises:
|
||||
FirebaseError: If an error occurs while sending the message to the FCM service.
|
||||
ValueError: If the input arguments are invalid.
|
||||
"""
|
||||
return _get_messaging_service(app).send_all(messages, dry_run)
|
||||
|
||||
def send_multicast(multicast_message, dry_run=False, app=None):
|
||||
"""Sends the given mutlicast message to all tokens via Firebase Cloud Messaging (FCM).
|
||||
|
||||
If the ``dry_run`` mode is enabled, the message will not be actually delivered to the
|
||||
recipients. Instead FCM performs all the usual validations, and emulates the send operation.
|
||||
|
||||
Args:
|
||||
multicast_message: An instance of ``messaging.MulticastMessage``.
|
||||
dry_run: A boolean indicating whether to run the operation in dry run mode (optional).
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
BatchResponse: A ``messaging.BatchResponse`` instance.
|
||||
|
||||
Raises:
|
||||
FirebaseError: If an error occurs while sending the message to the FCM service.
|
||||
ValueError: If the input arguments are invalid.
|
||||
"""
|
||||
if not isinstance(multicast_message, MulticastMessage):
|
||||
raise ValueError('Message must be an instance of messaging.MulticastMessage class.')
|
||||
messages = [Message(
|
||||
data=multicast_message.data,
|
||||
notification=multicast_message.notification,
|
||||
android=multicast_message.android,
|
||||
webpush=multicast_message.webpush,
|
||||
apns=multicast_message.apns,
|
||||
fcm_options=multicast_message.fcm_options,
|
||||
token=token
|
||||
) for token in multicast_message.tokens]
|
||||
return _get_messaging_service(app).send_all(messages, dry_run)
|
||||
|
||||
def subscribe_to_topic(tokens, topic, app=None):
|
||||
"""Subscribes a list of registration tokens to an FCM topic.
|
||||
|
||||
Args:
|
||||
tokens: A non-empty list of device registration tokens. List may not have more than 1000
|
||||
elements.
|
||||
topic: Name of the topic to subscribe to. May contain the ``/topics/`` prefix.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
TopicManagementResponse: A ``TopicManagementResponse`` instance.
|
||||
|
||||
Raises:
|
||||
FirebaseError: If an error occurs while communicating with instance ID service.
|
||||
ValueError: If the input arguments are invalid.
|
||||
"""
|
||||
return _get_messaging_service(app).make_topic_management_request(
|
||||
tokens, topic, 'iid/v1:batchAdd')
|
||||
|
||||
def unsubscribe_from_topic(tokens, topic, app=None):
|
||||
"""Unsubscribes a list of registration tokens from an FCM topic.
|
||||
|
||||
Args:
|
||||
tokens: A non-empty list of device registration tokens. List may not have more than 1000
|
||||
elements.
|
||||
topic: Name of the topic to unsubscribe from. May contain the ``/topics/`` prefix.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
TopicManagementResponse: A ``TopicManagementResponse`` instance.
|
||||
|
||||
Raises:
|
||||
FirebaseError: If an error occurs while communicating with instance ID service.
|
||||
ValueError: If the input arguments are invalid.
|
||||
"""
|
||||
return _get_messaging_service(app).make_topic_management_request(
|
||||
tokens, topic, 'iid/v1:batchRemove')
|
||||
|
||||
|
||||
class ErrorInfo:
|
||||
"""An error encountered when performing a topic management operation."""
|
||||
|
||||
def __init__(self, index, reason):
|
||||
self._index = index
|
||||
self._reason = reason
|
||||
|
||||
@property
|
||||
def index(self):
|
||||
"""Index of the registration token to which this error is related to."""
|
||||
return self._index
|
||||
|
||||
@property
|
||||
def reason(self):
|
||||
"""String describing the nature of the error."""
|
||||
return self._reason
|
||||
|
||||
|
||||
class TopicManagementResponse:
|
||||
"""The response received from a topic management operation."""
|
||||
|
||||
def __init__(self, resp):
|
||||
if not isinstance(resp, dict) or 'results' not in resp:
|
||||
raise ValueError('Unexpected topic management response: {0}.'.format(resp))
|
||||
self._success_count = 0
|
||||
self._failure_count = 0
|
||||
self._errors = []
|
||||
for index, result in enumerate(resp['results']):
|
||||
if 'error' in result:
|
||||
self._failure_count += 1
|
||||
self._errors.append(ErrorInfo(index, result['error']))
|
||||
else:
|
||||
self._success_count += 1
|
||||
|
||||
@property
|
||||
def success_count(self):
|
||||
"""Number of tokens that were successfully subscribed or unsubscribed."""
|
||||
return self._success_count
|
||||
|
||||
@property
|
||||
def failure_count(self):
|
||||
"""Number of tokens that could not be subscribed or unsubscribed due to errors."""
|
||||
return self._failure_count
|
||||
|
||||
@property
|
||||
def errors(self):
|
||||
"""A list of ``messaging.ErrorInfo`` objects (possibly empty)."""
|
||||
return self._errors
|
||||
|
||||
|
||||
class BatchResponse:
|
||||
"""The response received from a batch request to the FCM API."""
|
||||
|
||||
def __init__(self, responses):
|
||||
self._responses = responses
|
||||
self._success_count = len([resp for resp in responses if resp.success])
|
||||
|
||||
@property
|
||||
def responses(self):
|
||||
"""A list of ``messaging.SendResponse`` objects (possibly empty)."""
|
||||
return self._responses
|
||||
|
||||
@property
|
||||
def success_count(self):
|
||||
return self._success_count
|
||||
|
||||
@property
|
||||
def failure_count(self):
|
||||
return len(self.responses) - self.success_count
|
||||
|
||||
|
||||
class SendResponse:
|
||||
"""The response received from an individual batched request to the FCM API."""
|
||||
|
||||
def __init__(self, resp, exception):
|
||||
self._exception = exception
|
||||
self._message_id = None
|
||||
if resp:
|
||||
self._message_id = resp.get('name', None)
|
||||
|
||||
@property
|
||||
def message_id(self):
|
||||
"""A message ID string that uniquely identifies the message."""
|
||||
return self._message_id
|
||||
|
||||
@property
|
||||
def success(self):
|
||||
"""A boolean indicating if the request was successful."""
|
||||
return self._message_id is not None and not self._exception
|
||||
|
||||
@property
|
||||
def exception(self):
|
||||
"""A ``FirebaseError`` if an error occurs while sending the message to the FCM service."""
|
||||
return self._exception
|
||||
|
||||
|
||||
class _MessagingService:
|
||||
"""Service class that implements Firebase Cloud Messaging (FCM) functionality."""
|
||||
|
||||
FCM_URL = 'https://fcm.googleapis.com/v1/projects/{0}/messages:send'
|
||||
FCM_BATCH_URL = 'https://fcm.googleapis.com/batch'
|
||||
IID_URL = 'https://iid.googleapis.com'
|
||||
IID_HEADERS = {'access_token_auth': 'true'}
|
||||
JSON_ENCODER = _messaging_encoder.MessageEncoder()
|
||||
|
||||
FCM_ERROR_TYPES = {
|
||||
'APNS_AUTH_ERROR': ThirdPartyAuthError,
|
||||
'QUOTA_EXCEEDED': QuotaExceededError,
|
||||
'SENDER_ID_MISMATCH': SenderIdMismatchError,
|
||||
'THIRD_PARTY_AUTH_ERROR': ThirdPartyAuthError,
|
||||
'UNREGISTERED': UnregisteredError,
|
||||
}
|
||||
|
||||
def __init__(self, app):
|
||||
project_id = app.project_id
|
||||
if not project_id:
|
||||
raise ValueError(
|
||||
'Project ID is required to access Cloud Messaging service. Either set the '
|
||||
'projectId option, or use service account credentials. Alternatively, set the '
|
||||
'GOOGLE_CLOUD_PROJECT environment variable.')
|
||||
self._fcm_url = _MessagingService.FCM_URL.format(project_id)
|
||||
self._fcm_headers = {
|
||||
'X-GOOG-API-FORMAT-VERSION': '2',
|
||||
'X-FIREBASE-CLIENT': 'fire-admin-python/{0}'.format(firebase_admin.__version__),
|
||||
}
|
||||
timeout = app.options.get('httpTimeout', _http_client.DEFAULT_TIMEOUT_SECONDS)
|
||||
self._client = _http_client.JsonHttpClient(
|
||||
credential=app.credential.get_credential(), timeout=timeout)
|
||||
self._transport = _auth.authorized_http(app.credential.get_credential())
|
||||
|
||||
@classmethod
|
||||
def encode_message(cls, message):
|
||||
if not isinstance(message, Message):
|
||||
raise ValueError('Message must be an instance of messaging.Message class.')
|
||||
return cls.JSON_ENCODER.default(message)
|
||||
|
||||
def send(self, message, dry_run=False):
|
||||
"""Sends the given message to FCM via the FCM v1 API."""
|
||||
data = self._message_data(message, dry_run)
|
||||
try:
|
||||
resp = self._client.body(
|
||||
'post',
|
||||
url=self._fcm_url,
|
||||
headers=self._fcm_headers,
|
||||
json=data
|
||||
)
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise self._handle_fcm_error(error)
|
||||
else:
|
||||
return resp['name']
|
||||
|
||||
def send_all(self, messages, dry_run=False):
|
||||
"""Sends the given messages to FCM via the batch API."""
|
||||
if not isinstance(messages, list):
|
||||
raise ValueError('messages must be a list of messaging.Message instances.')
|
||||
if len(messages) > 500:
|
||||
raise ValueError('messages must not contain more than 500 elements.')
|
||||
|
||||
responses = []
|
||||
|
||||
def batch_callback(_, response, error):
|
||||
exception = None
|
||||
if error:
|
||||
exception = self._handle_batch_error(error)
|
||||
send_response = SendResponse(response, exception)
|
||||
responses.append(send_response)
|
||||
|
||||
batch = http.BatchHttpRequest(
|
||||
callback=batch_callback, batch_uri=_MessagingService.FCM_BATCH_URL)
|
||||
for message in messages:
|
||||
body = json.dumps(self._message_data(message, dry_run))
|
||||
req = http.HttpRequest(
|
||||
http=self._transport,
|
||||
postproc=self._postproc,
|
||||
uri=self._fcm_url,
|
||||
method='POST',
|
||||
body=body,
|
||||
headers=self._fcm_headers
|
||||
)
|
||||
batch.add(req)
|
||||
|
||||
try:
|
||||
batch.execute()
|
||||
except googleapiclient.http.HttpError as error:
|
||||
raise self._handle_batch_error(error)
|
||||
else:
|
||||
return BatchResponse(responses)
|
||||
|
||||
def make_topic_management_request(self, tokens, topic, operation):
|
||||
"""Invokes the IID service for topic management functionality."""
|
||||
if isinstance(tokens, str):
|
||||
tokens = [tokens]
|
||||
if not isinstance(tokens, list) or not tokens:
|
||||
raise ValueError('Tokens must be a string or a non-empty list of strings.')
|
||||
invalid_str = [t for t in tokens if not isinstance(t, str) or not t]
|
||||
if invalid_str:
|
||||
raise ValueError('Tokens must be non-empty strings.')
|
||||
|
||||
if not isinstance(topic, str) or not topic:
|
||||
raise ValueError('Topic must be a non-empty string.')
|
||||
if not topic.startswith('/topics/'):
|
||||
topic = '/topics/{0}'.format(topic)
|
||||
data = {
|
||||
'to': topic,
|
||||
'registration_tokens': tokens,
|
||||
}
|
||||
url = '{0}/{1}'.format(_MessagingService.IID_URL, operation)
|
||||
try:
|
||||
resp = self._client.body(
|
||||
'post',
|
||||
url=url,
|
||||
json=data,
|
||||
headers=_MessagingService.IID_HEADERS
|
||||
)
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise self._handle_iid_error(error)
|
||||
else:
|
||||
return TopicManagementResponse(resp)
|
||||
|
||||
def _message_data(self, message, dry_run):
|
||||
data = {'message': _MessagingService.encode_message(message)}
|
||||
if dry_run:
|
||||
data['validate_only'] = True
|
||||
return data
|
||||
|
||||
def _postproc(self, _, body):
|
||||
"""Handle response from batch API request."""
|
||||
# This only gets called for 2xx responses.
|
||||
return json.loads(body.decode())
|
||||
|
||||
def _handle_fcm_error(self, error):
|
||||
"""Handles errors received from the FCM API."""
|
||||
return _utils.handle_platform_error_from_requests(
|
||||
error, _MessagingService._build_fcm_error_requests)
|
||||
|
||||
def _handle_iid_error(self, error):
|
||||
"""Handles errors received from the Instance ID API."""
|
||||
if error.response is None:
|
||||
raise _utils.handle_requests_error(error)
|
||||
|
||||
data = {}
|
||||
try:
|
||||
parsed_body = error.response.json()
|
||||
if isinstance(parsed_body, dict):
|
||||
data = parsed_body
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# IID error response format: {"error": "ErrorCode"}
|
||||
code = data.get('error')
|
||||
msg = None
|
||||
if code:
|
||||
msg = 'Error while calling the IID service: {0}'.format(code)
|
||||
else:
|
||||
msg = 'Unexpected HTTP response with status: {0}; body: {1}'.format(
|
||||
error.response.status_code, error.response.content.decode())
|
||||
|
||||
return _utils.handle_requests_error(error, msg)
|
||||
|
||||
def _handle_batch_error(self, error):
|
||||
"""Handles errors received from the googleapiclient while making batch requests."""
|
||||
return _utils.handle_platform_error_from_googleapiclient(
|
||||
error, _MessagingService._build_fcm_error_googleapiclient)
|
||||
|
||||
@classmethod
|
||||
def _build_fcm_error_requests(cls, error, message, error_dict):
|
||||
"""Parses an error response from the FCM API and creates a FCM-specific exception if
|
||||
appropriate."""
|
||||
exc_type = cls._build_fcm_error(error_dict)
|
||||
return exc_type(message, cause=error, http_response=error.response) if exc_type else None
|
||||
|
||||
@classmethod
|
||||
def _build_fcm_error_googleapiclient(cls, error, message, error_dict, http_response):
|
||||
"""Parses an error response from the FCM API and creates a FCM-specific exception if
|
||||
appropriate."""
|
||||
exc_type = cls._build_fcm_error(error_dict)
|
||||
return exc_type(message, cause=error, http_response=http_response) if exc_type else None
|
||||
|
||||
@classmethod
|
||||
def _build_fcm_error(cls, error_dict):
|
||||
if not error_dict:
|
||||
return None
|
||||
fcm_code = None
|
||||
for detail in error_dict.get('details', []):
|
||||
if detail.get('@type') == 'type.googleapis.com/google.firebase.fcm.v1.FcmError':
|
||||
fcm_code = detail.get('errorCode')
|
||||
break
|
||||
return _MessagingService.FCM_ERROR_TYPES.get(fcm_code)
|
983
venv/Lib/site-packages/firebase_admin/ml.py
Normal file
983
venv/Lib/site-packages/firebase_admin/ml.py
Normal file
|
@ -0,0 +1,983 @@
|
|||
# Copyright 2019 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 ML module.
|
||||
|
||||
This module contains functions for creating, updating, getting, listing,
|
||||
deleting, publishing and unpublishing Firebase ML models.
|
||||
"""
|
||||
|
||||
|
||||
import datetime
|
||||
import re
|
||||
import time
|
||||
import os
|
||||
from urllib import parse
|
||||
|
||||
import requests
|
||||
|
||||
import firebase_admin
|
||||
from firebase_admin import _http_client
|
||||
from firebase_admin import _utils
|
||||
from firebase_admin import exceptions
|
||||
|
||||
# pylint: disable=import-error,no-name-in-module
|
||||
try:
|
||||
from firebase_admin import storage
|
||||
_GCS_ENABLED = True
|
||||
except ImportError:
|
||||
_GCS_ENABLED = False
|
||||
|
||||
# pylint: disable=import-error,no-name-in-module
|
||||
try:
|
||||
import tensorflow as tf
|
||||
_TF_ENABLED = True
|
||||
except ImportError:
|
||||
_TF_ENABLED = False
|
||||
|
||||
_ML_ATTRIBUTE = '_ml'
|
||||
_MAX_PAGE_SIZE = 100
|
||||
_MODEL_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{1,60}$')
|
||||
_DISPLAY_NAME_PATTERN = re.compile(r'^[A-Za-z0-9_-]{1,32}$')
|
||||
_TAG_PATTERN = re.compile(r'^[A-Za-z0-9_-]{1,32}$')
|
||||
_GCS_TFLITE_URI_PATTERN = re.compile(
|
||||
r'^gs://(?P<bucket_name>[a-z0-9_.-]{3,63})/(?P<blob_name>.+)$')
|
||||
_AUTO_ML_MODEL_PATTERN = re.compile(
|
||||
r'^projects/(?P<project_id>[a-z0-9-]{6,30})/locations/(?P<location_id>[^/]+)/' +
|
||||
r'models/(?P<model_id>[A-Za-z0-9]+)$')
|
||||
_RESOURCE_NAME_PATTERN = re.compile(
|
||||
r'^projects/(?P<project_id>[a-z0-9-]{6,30})/models/(?P<model_id>[A-Za-z0-9_-]{1,60})$')
|
||||
_OPERATION_NAME_PATTERN = re.compile(
|
||||
r'^projects/(?P<project_id>[a-z0-9-]{6,30})/operations/[^/]+$')
|
||||
|
||||
|
||||
def _get_ml_service(app):
|
||||
""" Returns an _MLService instance for an App.
|
||||
|
||||
Args:
|
||||
app: A Firebase App instance (or None to use the default App).
|
||||
|
||||
Returns:
|
||||
_MLService: An _MLService for the specified App instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the app argument is invalid.
|
||||
"""
|
||||
return _utils.get_app_service(app, _ML_ATTRIBUTE, _MLService)
|
||||
|
||||
|
||||
def create_model(model, app=None):
|
||||
"""Creates a model in the current Firebase project.
|
||||
|
||||
Args:
|
||||
model: An ml.Model to create.
|
||||
app: A Firebase app instance (or None to use the default app).
|
||||
|
||||
Returns:
|
||||
Model: The model that was created in Firebase ML.
|
||||
"""
|
||||
ml_service = _get_ml_service(app)
|
||||
return Model.from_dict(ml_service.create_model(model), app=app)
|
||||
|
||||
|
||||
def update_model(model, app=None):
|
||||
"""Updates a model's metadata or model file.
|
||||
|
||||
Args:
|
||||
model: The ml.Model to update.
|
||||
app: A Firebase app instance (or None to use the default app).
|
||||
|
||||
Returns:
|
||||
Model: The updated model.
|
||||
"""
|
||||
ml_service = _get_ml_service(app)
|
||||
return Model.from_dict(ml_service.update_model(model), app=app)
|
||||
|
||||
|
||||
def publish_model(model_id, app=None):
|
||||
"""Publishes a Firebase ML model.
|
||||
|
||||
A published model can be downloaded to client apps.
|
||||
|
||||
Args:
|
||||
model_id: The id of the model to publish.
|
||||
app: A Firebase app instance (or None to use the default app).
|
||||
|
||||
Returns:
|
||||
Model: The published model.
|
||||
"""
|
||||
ml_service = _get_ml_service(app)
|
||||
return Model.from_dict(ml_service.set_published(model_id, publish=True), app=app)
|
||||
|
||||
|
||||
def unpublish_model(model_id, app=None):
|
||||
"""Unpublishes a Firebase ML model.
|
||||
|
||||
Args:
|
||||
model_id: The id of the model to unpublish.
|
||||
app: A Firebase app instance (or None to use the default app).
|
||||
|
||||
Returns:
|
||||
Model: The unpublished model.
|
||||
"""
|
||||
ml_service = _get_ml_service(app)
|
||||
return Model.from_dict(ml_service.set_published(model_id, publish=False), app=app)
|
||||
|
||||
|
||||
def get_model(model_id, app=None):
|
||||
"""Gets the model specified by the given ID.
|
||||
|
||||
Args:
|
||||
model_id: The id of the model to get.
|
||||
app: A Firebase app instance (or None to use the default app).
|
||||
|
||||
Returns:
|
||||
Model: The requested model.
|
||||
"""
|
||||
ml_service = _get_ml_service(app)
|
||||
return Model.from_dict(ml_service.get_model(model_id), app=app)
|
||||
|
||||
|
||||
def list_models(list_filter=None, page_size=None, page_token=None, app=None):
|
||||
"""Lists the current project's models.
|
||||
|
||||
Args:
|
||||
list_filter: a list filter string such as ``tags:'tag_1'``. None will return all models.
|
||||
page_size: A number between 1 and 100 inclusive that specifies the maximum
|
||||
number of models to return per page. None for default.
|
||||
page_token: A next page token returned from a previous page of results. None
|
||||
for first page of results.
|
||||
app: A Firebase app instance (or None to use the default app).
|
||||
|
||||
Returns:
|
||||
ListModelsPage: A (filtered) list of models.
|
||||
"""
|
||||
ml_service = _get_ml_service(app)
|
||||
return ListModelsPage(
|
||||
ml_service.list_models, list_filter, page_size, page_token, app=app)
|
||||
|
||||
|
||||
def delete_model(model_id, app=None):
|
||||
"""Deletes a model from the current project.
|
||||
|
||||
Args:
|
||||
model_id: The id of the model you wish to delete.
|
||||
app: A Firebase app instance (or None to use the default app).
|
||||
"""
|
||||
ml_service = _get_ml_service(app)
|
||||
ml_service.delete_model(model_id)
|
||||
|
||||
|
||||
class Model:
|
||||
"""A Firebase ML Model object.
|
||||
|
||||
Args:
|
||||
display_name: The display name of your model - used to identify your model in code.
|
||||
tags: Optional list of strings associated with your model. Can be used in list queries.
|
||||
model_format: A subclass of ModelFormat. (e.g. TFLiteFormat) Specifies the model details.
|
||||
"""
|
||||
def __init__(self, display_name=None, tags=None, model_format=None):
|
||||
self._app = None # Only needed for wait_for_unlo
|
||||
self._data = {}
|
||||
self._model_format = None
|
||||
|
||||
if display_name is not None:
|
||||
self.display_name = display_name
|
||||
if tags is not None:
|
||||
self.tags = tags
|
||||
if model_format is not None:
|
||||
self.model_format = model_format
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data, app=None):
|
||||
"""Create an instance of the object from a dict."""
|
||||
data_copy = dict(data)
|
||||
tflite_format = None
|
||||
tflite_format_data = data_copy.pop('tfliteModel', None)
|
||||
data_copy.pop('@type', None) # Returned by Operations. (Not needed)
|
||||
if tflite_format_data:
|
||||
tflite_format = TFLiteFormat.from_dict(tflite_format_data)
|
||||
model = Model(model_format=tflite_format)
|
||||
model._data = data_copy # pylint: disable=protected-access
|
||||
model._app = app # pylint: disable=protected-access
|
||||
return model
|
||||
|
||||
def _update_from_dict(self, data):
|
||||
copy = Model.from_dict(data)
|
||||
self.model_format = copy.model_format
|
||||
self._data = copy._data # pylint: disable=protected-access
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
# pylint: disable=protected-access
|
||||
return self._data == other._data and self._model_format == other._model_format
|
||||
return False
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
@property
|
||||
def model_id(self):
|
||||
"""The model's ID, unique to the project."""
|
||||
if not self._data.get('name'):
|
||||
return None
|
||||
_, model_id = _validate_and_parse_name(self._data.get('name'))
|
||||
return model_id
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
"""The model's display name, used to refer to the model in code and in
|
||||
the Firebase console."""
|
||||
return self._data.get('displayName')
|
||||
|
||||
@display_name.setter
|
||||
def display_name(self, display_name):
|
||||
self._data['displayName'] = _validate_display_name(display_name)
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def _convert_to_millis(date_string):
|
||||
if not date_string:
|
||||
return None
|
||||
format_str = '%Y-%m-%dT%H:%M:%S.%fZ'
|
||||
epoch = datetime.datetime.utcfromtimestamp(0)
|
||||
datetime_object = datetime.datetime.strptime(date_string, format_str)
|
||||
millis = int((datetime_object - epoch).total_seconds() * 1000)
|
||||
return millis
|
||||
|
||||
@property
|
||||
def create_time(self):
|
||||
"""The time the model was created."""
|
||||
return Model._convert_to_millis(self._data.get('createTime', None))
|
||||
|
||||
@property
|
||||
def update_time(self):
|
||||
"""The time the model was last updated."""
|
||||
return Model._convert_to_millis(self._data.get('updateTime', None))
|
||||
|
||||
@property
|
||||
def validation_error(self):
|
||||
"""Validation error message."""
|
||||
return self._data.get('state', {}).get('validationError', {}).get('message')
|
||||
|
||||
@property
|
||||
def published(self):
|
||||
"""True if the model is published and available for clients to
|
||||
download."""
|
||||
return bool(self._data.get('state', {}).get('published'))
|
||||
|
||||
@property
|
||||
def etag(self):
|
||||
"""The entity tag (ETag) of the model resource."""
|
||||
return self._data.get('etag')
|
||||
|
||||
@property
|
||||
def model_hash(self):
|
||||
"""SHA256 hash of the model binary."""
|
||||
return self._data.get('modelHash')
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
"""Tag strings, used for filtering query results."""
|
||||
return self._data.get('tags')
|
||||
|
||||
@tags.setter
|
||||
def tags(self, tags):
|
||||
self._data['tags'] = _validate_tags(tags)
|
||||
return self
|
||||
|
||||
@property
|
||||
def locked(self):
|
||||
"""True if the Model object is locked by an active operation."""
|
||||
return bool(self._data.get('activeOperations') and
|
||||
len(self._data.get('activeOperations')) > 0)
|
||||
|
||||
def wait_for_unlocked(self, max_time_seconds=None):
|
||||
"""Waits for the model to be unlocked. (All active operations complete)
|
||||
|
||||
Args:
|
||||
max_time_seconds: The maximum number of seconds to wait for the model to unlock.
|
||||
(None for no limit)
|
||||
|
||||
Raises:
|
||||
exceptions.DeadlineExceeded: If max_time_seconds passed and the model is still locked.
|
||||
"""
|
||||
if not self.locked:
|
||||
return
|
||||
ml_service = _get_ml_service(self._app)
|
||||
op_name = self._data.get('activeOperations')[0].get('name')
|
||||
model_dict = ml_service.handle_operation(
|
||||
ml_service.get_operation(op_name),
|
||||
wait_for_operation=True,
|
||||
max_time_seconds=max_time_seconds)
|
||||
self._update_from_dict(model_dict)
|
||||
|
||||
@property
|
||||
def model_format(self):
|
||||
"""The model's ``ModelFormat`` object, which represents the model's
|
||||
format and storage location."""
|
||||
return self._model_format
|
||||
|
||||
@model_format.setter
|
||||
def model_format(self, model_format):
|
||||
if model_format is not None:
|
||||
_validate_model_format(model_format)
|
||||
self._model_format = model_format #Can be None
|
||||
return self
|
||||
|
||||
def as_dict(self, for_upload=False):
|
||||
"""Returns a serializable representation of the object."""
|
||||
copy = dict(self._data)
|
||||
if self._model_format:
|
||||
copy.update(self._model_format.as_dict(for_upload=for_upload))
|
||||
return copy
|
||||
|
||||
|
||||
class ModelFormat:
|
||||
"""Abstract base class representing a Model Format such as TFLite."""
|
||||
def as_dict(self, for_upload=False):
|
||||
"""Returns a serializable representation of the object."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class TFLiteFormat(ModelFormat):
|
||||
"""Model format representing a TFLite model.
|
||||
|
||||
Args:
|
||||
model_source: A TFLiteModelSource sub class. Specifies the details of the model source.
|
||||
"""
|
||||
def __init__(self, model_source=None):
|
||||
self._data = {}
|
||||
self._model_source = None
|
||||
|
||||
if model_source is not None:
|
||||
self.model_source = model_source
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
"""Create an instance of the object from a dict."""
|
||||
data_copy = dict(data)
|
||||
tflite_format = TFLiteFormat(model_source=cls._init_model_source(data_copy))
|
||||
tflite_format._data = data_copy # pylint: disable=protected-access
|
||||
return tflite_format
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
# pylint: disable=protected-access
|
||||
return self._data == other._data and self._model_source == other._model_source
|
||||
return False
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
@staticmethod
|
||||
def _init_model_source(data):
|
||||
gcs_tflite_uri = data.pop('gcsTfliteUri', None)
|
||||
if gcs_tflite_uri:
|
||||
return TFLiteGCSModelSource(gcs_tflite_uri=gcs_tflite_uri)
|
||||
auto_ml_model = data.pop('automlModel', None)
|
||||
if auto_ml_model:
|
||||
return TFLiteAutoMlSource(auto_ml_model=auto_ml_model)
|
||||
return None
|
||||
|
||||
@property
|
||||
def model_source(self):
|
||||
"""The TF Lite model's location."""
|
||||
return self._model_source
|
||||
|
||||
@model_source.setter
|
||||
def model_source(self, model_source):
|
||||
if model_source is not None:
|
||||
if not isinstance(model_source, TFLiteModelSource):
|
||||
raise TypeError('Model source must be a TFLiteModelSource object.')
|
||||
self._model_source = model_source # Can be None
|
||||
|
||||
@property
|
||||
def size_bytes(self):
|
||||
"""The size in bytes of the TF Lite model."""
|
||||
return self._data.get('sizeBytes')
|
||||
|
||||
def as_dict(self, for_upload=False):
|
||||
"""Returns a serializable representation of the object."""
|
||||
copy = dict(self._data)
|
||||
if self._model_source:
|
||||
copy.update(self._model_source.as_dict(for_upload=for_upload))
|
||||
return {'tfliteModel': copy}
|
||||
|
||||
|
||||
class TFLiteModelSource:
|
||||
"""Abstract base class representing a model source for TFLite format models."""
|
||||
def as_dict(self, for_upload=False):
|
||||
"""Returns a serializable representation of the object."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class _CloudStorageClient:
|
||||
"""Cloud Storage helper class"""
|
||||
|
||||
GCS_URI = 'gs://{0}/{1}'
|
||||
BLOB_NAME = 'Firebase/ML/Models/{0}'
|
||||
|
||||
@staticmethod
|
||||
def _assert_gcs_enabled():
|
||||
if not _GCS_ENABLED:
|
||||
raise ImportError('Failed to import the Cloud Storage library for Python. Make sure '
|
||||
'to install the "google-cloud-storage" module.')
|
||||
|
||||
@staticmethod
|
||||
def _parse_gcs_tflite_uri(uri):
|
||||
# GCS Bucket naming rules are complex. The regex is not comprehensive.
|
||||
# See https://cloud.google.com/storage/docs/naming for full details.
|
||||
matcher = _GCS_TFLITE_URI_PATTERN.match(uri)
|
||||
if not matcher:
|
||||
raise ValueError('GCS TFLite URI format is invalid.')
|
||||
return matcher.group('bucket_name'), matcher.group('blob_name')
|
||||
|
||||
@staticmethod
|
||||
def upload(bucket_name, model_file_name, app):
|
||||
"""Upload a model file to the specified Storage bucket."""
|
||||
_CloudStorageClient._assert_gcs_enabled()
|
||||
|
||||
file_name = os.path.basename(model_file_name)
|
||||
bucket = storage.bucket(bucket_name, app=app)
|
||||
blob_name = _CloudStorageClient.BLOB_NAME.format(file_name)
|
||||
blob = bucket.blob(blob_name)
|
||||
blob.upload_from_filename(model_file_name)
|
||||
return _CloudStorageClient.GCS_URI.format(bucket.name, blob_name)
|
||||
|
||||
@staticmethod
|
||||
def sign_uri(gcs_tflite_uri, app):
|
||||
"""Makes the gcs_tflite_uri readable for GET for 10 minutes via signed_uri."""
|
||||
_CloudStorageClient._assert_gcs_enabled()
|
||||
bucket_name, blob_name = _CloudStorageClient._parse_gcs_tflite_uri(gcs_tflite_uri)
|
||||
bucket = storage.bucket(bucket_name, app=app)
|
||||
blob = bucket.blob(blob_name)
|
||||
return blob.generate_signed_url(
|
||||
version='v4',
|
||||
expiration=datetime.timedelta(minutes=10),
|
||||
method='GET'
|
||||
)
|
||||
|
||||
|
||||
class TFLiteGCSModelSource(TFLiteModelSource):
|
||||
"""TFLite model source representing a tflite model file stored in GCS."""
|
||||
|
||||
_STORAGE_CLIENT = _CloudStorageClient()
|
||||
|
||||
def __init__(self, gcs_tflite_uri, app=None):
|
||||
self._app = app
|
||||
self._gcs_tflite_uri = _validate_gcs_tflite_uri(gcs_tflite_uri)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return self._gcs_tflite_uri == other._gcs_tflite_uri # pylint: disable=protected-access
|
||||
return False
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
@classmethod
|
||||
def from_tflite_model_file(cls, model_file_name, bucket_name=None, app=None):
|
||||
"""Uploads the model file to an existing Google Cloud Storage bucket.
|
||||
|
||||
Args:
|
||||
model_file_name: The name of the model file.
|
||||
bucket_name: The name of an existing bucket. None to use the default bucket configured
|
||||
in the app.
|
||||
app: A Firebase app instance (or None to use the default app).
|
||||
|
||||
Returns:
|
||||
TFLiteGCSModelSource: The source created from the model_file
|
||||
|
||||
Raises:
|
||||
ImportError: If the Cloud Storage Library has not been installed.
|
||||
"""
|
||||
gcs_uri = TFLiteGCSModelSource._STORAGE_CLIENT.upload(bucket_name, model_file_name, app)
|
||||
return TFLiteGCSModelSource(gcs_tflite_uri=gcs_uri, app=app)
|
||||
|
||||
@staticmethod
|
||||
def _assert_tf_enabled():
|
||||
if not _TF_ENABLED:
|
||||
raise ImportError('Failed to import the tensorflow library for Python. Make sure '
|
||||
'to install the tensorflow module.')
|
||||
if not tf.version.VERSION.startswith('1.') and not tf.version.VERSION.startswith('2.'):
|
||||
raise ImportError('Expected tensorflow version 1.x or 2.x, but found {0}'
|
||||
.format(tf.version.VERSION))
|
||||
|
||||
@staticmethod
|
||||
def _tf_convert_from_saved_model(saved_model_dir):
|
||||
# Same for both v1.x and v2.x
|
||||
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
|
||||
return converter.convert()
|
||||
|
||||
@staticmethod
|
||||
def _tf_convert_from_keras_model(keras_model):
|
||||
"""Converts the given Keras model into a TF Lite model."""
|
||||
# Version 1.x conversion function takes a model file. Version 2.x takes the model itself.
|
||||
if tf.version.VERSION.startswith('1.'):
|
||||
keras_file = 'firebase_keras_model.h5'
|
||||
tf.keras.models.save_model(keras_model, keras_file)
|
||||
converter = tf.lite.TFLiteConverter.from_keras_model_file(keras_file)
|
||||
else:
|
||||
converter = tf.lite.TFLiteConverter.from_keras_model(keras_model)
|
||||
|
||||
return converter.convert()
|
||||
|
||||
@classmethod
|
||||
def from_saved_model(cls, saved_model_dir, model_file_name='firebase_ml_model.tflite',
|
||||
bucket_name=None, app=None):
|
||||
"""Creates a Tensor Flow Lite model from the saved model, and uploads the model to GCS.
|
||||
|
||||
Args:
|
||||
saved_model_dir: The saved model directory.
|
||||
model_file_name: The name that the tflite model will be saved as in Cloud Storage.
|
||||
bucket_name: The name of an existing bucket. None to use the default bucket configured
|
||||
in the app.
|
||||
app: Optional. A Firebase app instance (or None to use the default app)
|
||||
|
||||
Returns:
|
||||
TFLiteGCSModelSource: The source created from the saved_model_dir
|
||||
|
||||
Raises:
|
||||
ImportError: If the Tensor Flow or Cloud Storage Libraries have not been installed.
|
||||
"""
|
||||
TFLiteGCSModelSource._assert_tf_enabled()
|
||||
tflite_model = TFLiteGCSModelSource._tf_convert_from_saved_model(saved_model_dir)
|
||||
with open(model_file_name, 'wb') as model_file:
|
||||
model_file.write(tflite_model)
|
||||
return TFLiteGCSModelSource.from_tflite_model_file(model_file_name, bucket_name, app)
|
||||
|
||||
@classmethod
|
||||
def from_keras_model(cls, keras_model, model_file_name='firebase_ml_model.tflite',
|
||||
bucket_name=None, app=None):
|
||||
"""Creates a Tensor Flow Lite model from the keras model, and uploads the model to GCS.
|
||||
|
||||
Args:
|
||||
keras_model: A tf.keras model.
|
||||
model_file_name: The name that the tflite model will be saved as in Cloud Storage.
|
||||
bucket_name: The name of an existing bucket. None to use the default bucket configured
|
||||
in the app.
|
||||
app: Optional. A Firebase app instance (or None to use the default app)
|
||||
|
||||
Returns:
|
||||
TFLiteGCSModelSource: The source created from the keras_model
|
||||
|
||||
Raises:
|
||||
ImportError: If the Tensor Flow or Cloud Storage Libraries have not been installed.
|
||||
"""
|
||||
TFLiteGCSModelSource._assert_tf_enabled()
|
||||
tflite_model = TFLiteGCSModelSource._tf_convert_from_keras_model(keras_model)
|
||||
with open(model_file_name, 'wb') as model_file:
|
||||
model_file.write(tflite_model)
|
||||
return TFLiteGCSModelSource.from_tflite_model_file(model_file_name, bucket_name, app)
|
||||
|
||||
@property
|
||||
def gcs_tflite_uri(self):
|
||||
"""URI of the model file in Cloud Storage."""
|
||||
return self._gcs_tflite_uri
|
||||
|
||||
@gcs_tflite_uri.setter
|
||||
def gcs_tflite_uri(self, gcs_tflite_uri):
|
||||
self._gcs_tflite_uri = _validate_gcs_tflite_uri(gcs_tflite_uri)
|
||||
|
||||
def _get_signed_gcs_tflite_uri(self):
|
||||
"""Signs the GCS uri, so the model file can be uploaded to Firebase ML and verified."""
|
||||
return TFLiteGCSModelSource._STORAGE_CLIENT.sign_uri(self._gcs_tflite_uri, self._app)
|
||||
|
||||
def as_dict(self, for_upload=False):
|
||||
"""Returns a serializable representation of the object."""
|
||||
if for_upload:
|
||||
return {'gcsTfliteUri': self._get_signed_gcs_tflite_uri()}
|
||||
|
||||
return {'gcsTfliteUri': self._gcs_tflite_uri}
|
||||
|
||||
|
||||
class TFLiteAutoMlSource(TFLiteModelSource):
|
||||
"""TFLite model source representing a tflite model created with AutoML."""
|
||||
|
||||
def __init__(self, auto_ml_model, app=None):
|
||||
self._app = app
|
||||
self.auto_ml_model = auto_ml_model
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return self.auto_ml_model == other.auto_ml_model
|
||||
return False
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
@property
|
||||
def auto_ml_model(self):
|
||||
"""Resource name of the model, created by the AutoML API or Cloud console."""
|
||||
return self._auto_ml_model
|
||||
|
||||
@auto_ml_model.setter
|
||||
def auto_ml_model(self, auto_ml_model):
|
||||
self._auto_ml_model = _validate_auto_ml_model(auto_ml_model)
|
||||
|
||||
def as_dict(self, for_upload=False):
|
||||
"""Returns a serializable representation of the object."""
|
||||
# Upload is irrelevant for auto_ml models
|
||||
return {'automlModel': self._auto_ml_model}
|
||||
|
||||
|
||||
class ListModelsPage:
|
||||
"""Represents a page of models in a Firebase project.
|
||||
|
||||
Provides methods for traversing the models included in this page, as well as
|
||||
retrieving subsequent pages of models. The iterator returned by
|
||||
``iterate_all()`` can be used to iterate through all the models in the
|
||||
Firebase project starting from this page.
|
||||
"""
|
||||
def __init__(self, list_models_func, list_filter, page_size, page_token, app):
|
||||
self._list_models_func = list_models_func
|
||||
self._list_filter = list_filter
|
||||
self._page_size = page_size
|
||||
self._page_token = page_token
|
||||
self._app = app
|
||||
self._list_response = list_models_func(list_filter, page_size, page_token)
|
||||
|
||||
@property
|
||||
def models(self):
|
||||
"""A list of Models from this page."""
|
||||
return [
|
||||
Model.from_dict(model, app=self._app) for model in self._list_response.get('models', [])
|
||||
]
|
||||
|
||||
@property
|
||||
def list_filter(self):
|
||||
"""The filter string used to filter the models."""
|
||||
return self._list_filter
|
||||
|
||||
@property
|
||||
def next_page_token(self):
|
||||
"""Token identifying the next page of results."""
|
||||
return self._list_response.get('nextPageToken', '')
|
||||
|
||||
@property
|
||||
def has_next_page(self):
|
||||
"""True if more pages are available."""
|
||||
return bool(self.next_page_token)
|
||||
|
||||
def get_next_page(self):
|
||||
"""Retrieves the next page of models if available.
|
||||
|
||||
Returns:
|
||||
ListModelsPage: Next page of models, or None if this is the last page.
|
||||
"""
|
||||
if self.has_next_page:
|
||||
return ListModelsPage(
|
||||
self._list_models_func,
|
||||
self._list_filter,
|
||||
self._page_size,
|
||||
self.next_page_token,
|
||||
self._app)
|
||||
return None
|
||||
|
||||
def iterate_all(self):
|
||||
"""Retrieves an iterator for Models.
|
||||
|
||||
Returned iterator will iterate through all the models in the Firebase
|
||||
project starting from this page. The iterator will never buffer more than
|
||||
one page of models in memory at a time.
|
||||
|
||||
Returns:
|
||||
iterator: An iterator of Model instances.
|
||||
"""
|
||||
return _ModelIterator(self)
|
||||
|
||||
|
||||
class _ModelIterator:
|
||||
"""An iterator that allows iterating over models, one at a time.
|
||||
|
||||
This implementation loads a page of models into memory, and iterates on them.
|
||||
When the whole page has been traversed, it loads another page. This class
|
||||
never keeps more than one page of entries in memory.
|
||||
"""
|
||||
def __init__(self, current_page):
|
||||
if not isinstance(current_page, ListModelsPage):
|
||||
raise TypeError('Current page must be a ListModelsPage')
|
||||
self._current_page = current_page
|
||||
self._index = 0
|
||||
|
||||
def next(self):
|
||||
if self._index == len(self._current_page.models):
|
||||
if self._current_page.has_next_page:
|
||||
self._current_page = self._current_page.get_next_page()
|
||||
self._index = 0
|
||||
if self._index < len(self._current_page.models):
|
||||
result = self._current_page.models[self._index]
|
||||
self._index += 1
|
||||
return result
|
||||
raise StopIteration
|
||||
|
||||
def __next__(self):
|
||||
return self.next()
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
|
||||
def _validate_and_parse_name(name):
|
||||
# The resource name is added automatically from API call responses.
|
||||
# The only way it could be invalid is if someone tries to
|
||||
# create a model from a dictionary manually and does it incorrectly.
|
||||
matcher = _RESOURCE_NAME_PATTERN.match(name)
|
||||
if not matcher:
|
||||
raise ValueError('Model resource name format is invalid.')
|
||||
return matcher.group('project_id'), matcher.group('model_id')
|
||||
|
||||
|
||||
def _validate_model(model, update_mask=None):
|
||||
if not isinstance(model, Model):
|
||||
raise TypeError('Model must be an ml.Model.')
|
||||
if update_mask is None and not model.display_name:
|
||||
raise ValueError('Model must have a display name.')
|
||||
|
||||
|
||||
def _validate_model_id(model_id):
|
||||
if not _MODEL_ID_PATTERN.match(model_id):
|
||||
raise ValueError('Model ID format is invalid.')
|
||||
|
||||
|
||||
def _validate_operation_name(op_name):
|
||||
if not _OPERATION_NAME_PATTERN.match(op_name):
|
||||
raise ValueError('Operation name format is invalid.')
|
||||
return op_name
|
||||
|
||||
|
||||
def _validate_display_name(display_name):
|
||||
if not _DISPLAY_NAME_PATTERN.match(display_name):
|
||||
raise ValueError('Display name format is invalid.')
|
||||
return display_name
|
||||
|
||||
|
||||
def _validate_tags(tags):
|
||||
if not isinstance(tags, list) or not \
|
||||
all(isinstance(tag, str) for tag in tags):
|
||||
raise TypeError('Tags must be a list of strings.')
|
||||
if not all(_TAG_PATTERN.match(tag) for tag in tags):
|
||||
raise ValueError('Tag format is invalid.')
|
||||
return tags
|
||||
|
||||
|
||||
def _validate_gcs_tflite_uri(uri):
|
||||
# GCS Bucket naming rules are complex. The regex is not comprehensive.
|
||||
# See https://cloud.google.com/storage/docs/naming for full details.
|
||||
if not _GCS_TFLITE_URI_PATTERN.match(uri):
|
||||
raise ValueError('GCS TFLite URI format is invalid.')
|
||||
return uri
|
||||
|
||||
def _validate_auto_ml_model(model):
|
||||
if not _AUTO_ML_MODEL_PATTERN.match(model):
|
||||
raise ValueError('Model resource name format is invalid.')
|
||||
return model
|
||||
|
||||
|
||||
def _validate_model_format(model_format):
|
||||
if not isinstance(model_format, ModelFormat):
|
||||
raise TypeError('Model format must be a ModelFormat object.')
|
||||
return model_format
|
||||
|
||||
|
||||
def _validate_list_filter(list_filter):
|
||||
if list_filter is not None:
|
||||
if not isinstance(list_filter, str):
|
||||
raise TypeError('List filter must be a string or None.')
|
||||
|
||||
|
||||
def _validate_page_size(page_size):
|
||||
if page_size is not None:
|
||||
if type(page_size) is not int: # pylint: disable=unidiomatic-typecheck
|
||||
# Specifically type() to disallow boolean which is a subtype of int
|
||||
raise TypeError('Page size must be a number or None.')
|
||||
if page_size < 1 or page_size > _MAX_PAGE_SIZE:
|
||||
raise ValueError('Page size must be a positive integer between '
|
||||
'1 and {0}'.format(_MAX_PAGE_SIZE))
|
||||
|
||||
|
||||
def _validate_page_token(page_token):
|
||||
if page_token is not None:
|
||||
if not isinstance(page_token, str):
|
||||
raise TypeError('Page token must be a string or None.')
|
||||
|
||||
|
||||
class _MLService:
|
||||
"""Firebase ML service."""
|
||||
|
||||
PROJECT_URL = 'https://firebaseml.googleapis.com/v1beta2/projects/{0}/'
|
||||
OPERATION_URL = 'https://firebaseml.googleapis.com/v1beta2/'
|
||||
POLL_EXPONENTIAL_BACKOFF_FACTOR = 1.5
|
||||
POLL_BASE_WAIT_TIME_SECONDS = 3
|
||||
|
||||
def __init__(self, app):
|
||||
self._project_id = app.project_id
|
||||
if not self._project_id:
|
||||
raise ValueError(
|
||||
'Project ID is required to access ML service. Either set the '
|
||||
'projectId option, or use service account credentials.')
|
||||
self._project_url = _MLService.PROJECT_URL.format(self._project_id)
|
||||
ml_headers = {
|
||||
'X-FIREBASE-CLIENT': 'fire-admin-python/{0}'.format(firebase_admin.__version__),
|
||||
}
|
||||
self._client = _http_client.JsonHttpClient(
|
||||
credential=app.credential.get_credential(),
|
||||
headers=ml_headers,
|
||||
base_url=self._project_url)
|
||||
self._operation_client = _http_client.JsonHttpClient(
|
||||
credential=app.credential.get_credential(),
|
||||
headers=ml_headers,
|
||||
base_url=_MLService.OPERATION_URL)
|
||||
|
||||
def get_operation(self, op_name):
|
||||
_validate_operation_name(op_name)
|
||||
try:
|
||||
return self._operation_client.body('get', url=op_name)
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise _utils.handle_platform_error_from_requests(error)
|
||||
|
||||
def _exponential_backoff(self, current_attempt, stop_time):
|
||||
"""Sleeps for the appropriate amount of time. Or throws deadline exceeded."""
|
||||
delay_factor = pow(_MLService.POLL_EXPONENTIAL_BACKOFF_FACTOR, current_attempt)
|
||||
wait_time_seconds = delay_factor * _MLService.POLL_BASE_WAIT_TIME_SECONDS
|
||||
|
||||
if stop_time is not None:
|
||||
max_seconds_left = (stop_time - datetime.datetime.now()).total_seconds()
|
||||
if max_seconds_left < 1: # allow a bit of time for rpc
|
||||
raise exceptions.DeadlineExceededError('Polling max time exceeded.')
|
||||
wait_time_seconds = min(wait_time_seconds, max_seconds_left - 1)
|
||||
time.sleep(wait_time_seconds)
|
||||
|
||||
def handle_operation(self, operation, wait_for_operation=False, max_time_seconds=None):
|
||||
"""Handles long running operations.
|
||||
|
||||
Args:
|
||||
operation: The operation to handle.
|
||||
wait_for_operation: Should we allow polling for the operation to complete.
|
||||
If no polling is requested, a locked model will be returned instead.
|
||||
max_time_seconds: The maximum seconds to try polling for operation complete.
|
||||
(None for no limit)
|
||||
|
||||
Returns:
|
||||
dict: A dictionary of the returned model properties.
|
||||
|
||||
Raises:
|
||||
TypeError: if the operation is not a dictionary.
|
||||
ValueError: If the operation is malformed.
|
||||
UnknownError: If the server responds with an unexpected response.
|
||||
err: If the operation exceeds polling attempts or stop_time
|
||||
"""
|
||||
if not isinstance(operation, dict):
|
||||
raise TypeError('Operation must be a dictionary.')
|
||||
|
||||
if operation.get('done'):
|
||||
# Operations which are immediately done don't have an operation name
|
||||
if operation.get('response'):
|
||||
return operation.get('response')
|
||||
if operation.get('error'):
|
||||
raise _utils.handle_operation_error(operation.get('error'))
|
||||
raise exceptions.UnknownError(message='Internal Error: Malformed Operation.')
|
||||
|
||||
op_name = _validate_operation_name(operation.get('name'))
|
||||
metadata = operation.get('metadata', {})
|
||||
metadata_type = metadata.get('@type', '')
|
||||
if not metadata_type.endswith('ModelOperationMetadata'):
|
||||
raise TypeError('Unknown type of operation metadata.')
|
||||
_, model_id = _validate_and_parse_name(metadata.get('name'))
|
||||
current_attempt = 0
|
||||
start_time = datetime.datetime.now()
|
||||
stop_time = (None if max_time_seconds is None else
|
||||
start_time + datetime.timedelta(seconds=max_time_seconds))
|
||||
while wait_for_operation and not operation.get('done'):
|
||||
# We just got this operation. Wait before getting another
|
||||
# so we don't exceed the GetOperation maximum request rate.
|
||||
self._exponential_backoff(current_attempt, stop_time)
|
||||
operation = self.get_operation(op_name)
|
||||
current_attempt += 1
|
||||
|
||||
if operation.get('done'):
|
||||
if operation.get('response'):
|
||||
return operation.get('response')
|
||||
if operation.get('error'):
|
||||
raise _utils.handle_operation_error(operation.get('error'))
|
||||
|
||||
# If the operation is not complete or timed out, return a (locked) model instead
|
||||
return get_model(model_id).as_dict()
|
||||
|
||||
|
||||
def create_model(self, model):
|
||||
_validate_model(model)
|
||||
try:
|
||||
return self.handle_operation(
|
||||
self._client.body('post', url='models', json=model.as_dict(for_upload=True)))
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise _utils.handle_platform_error_from_requests(error)
|
||||
|
||||
def update_model(self, model, update_mask=None):
|
||||
_validate_model(model, update_mask)
|
||||
path = 'models/{0}'.format(model.model_id)
|
||||
if update_mask is not None:
|
||||
path = path + '?updateMask={0}'.format(update_mask)
|
||||
try:
|
||||
return self.handle_operation(
|
||||
self._client.body('patch', url=path, json=model.as_dict(for_upload=True)))
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise _utils.handle_platform_error_from_requests(error)
|
||||
|
||||
def set_published(self, model_id, publish):
|
||||
_validate_model_id(model_id)
|
||||
model_name = 'projects/{0}/models/{1}'.format(self._project_id, model_id)
|
||||
model = Model.from_dict({
|
||||
'name': model_name,
|
||||
'state': {
|
||||
'published': publish
|
||||
}
|
||||
})
|
||||
return self.update_model(model, update_mask='state.published')
|
||||
|
||||
def get_model(self, model_id):
|
||||
_validate_model_id(model_id)
|
||||
try:
|
||||
return self._client.body('get', url='models/{0}'.format(model_id))
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise _utils.handle_platform_error_from_requests(error)
|
||||
|
||||
def list_models(self, list_filter, page_size, page_token):
|
||||
""" lists Firebase ML models."""
|
||||
_validate_list_filter(list_filter)
|
||||
_validate_page_size(page_size)
|
||||
_validate_page_token(page_token)
|
||||
params = {}
|
||||
if list_filter:
|
||||
params['filter'] = list_filter
|
||||
if page_size:
|
||||
params['page_size'] = page_size
|
||||
if page_token:
|
||||
params['page_token'] = page_token
|
||||
path = 'models'
|
||||
if params:
|
||||
param_str = parse.urlencode(sorted(params.items()), True)
|
||||
path = path + '?' + param_str
|
||||
try:
|
||||
return self._client.body('get', url=path)
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise _utils.handle_platform_error_from_requests(error)
|
||||
|
||||
def delete_model(self, model_id):
|
||||
_validate_model_id(model_id)
|
||||
try:
|
||||
self._client.body('delete', url='models/{0}'.format(model_id))
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise _utils.handle_platform_error_from_requests(error)
|
664
venv/Lib/site-packages/firebase_admin/project_management.py
Normal file
664
venv/Lib/site-packages/firebase_admin/project_management.py
Normal file
|
@ -0,0 +1,664 @@
|
|||
# Copyright 2018 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 Project Management module.
|
||||
|
||||
This module enables management of resources in Firebase projects, such as Android and iOS apps.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import re
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
import firebase_admin
|
||||
from firebase_admin import exceptions
|
||||
from firebase_admin import _http_client
|
||||
from firebase_admin import _utils
|
||||
|
||||
|
||||
_PROJECT_MANAGEMENT_ATTRIBUTE = '_project_management'
|
||||
|
||||
|
||||
def _get_project_management_service(app):
|
||||
return _utils.get_app_service(app, _PROJECT_MANAGEMENT_ATTRIBUTE, _ProjectManagementService)
|
||||
|
||||
|
||||
def android_app(app_id, app=None):
|
||||
"""Obtains a reference to an Android app in the associated Firebase project.
|
||||
|
||||
Args:
|
||||
app_id: The app ID that identifies this Android app.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
AndroidApp: An ``AndroidApp`` instance.
|
||||
"""
|
||||
return AndroidApp(app_id=app_id, service=_get_project_management_service(app))
|
||||
|
||||
|
||||
def ios_app(app_id, app=None):
|
||||
"""Obtains a reference to an iOS app in the associated Firebase project.
|
||||
|
||||
Args:
|
||||
app_id: The app ID that identifies this iOS app.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
IOSApp: An ``IOSApp`` instance.
|
||||
"""
|
||||
return IOSApp(app_id=app_id, service=_get_project_management_service(app))
|
||||
|
||||
|
||||
def list_android_apps(app=None):
|
||||
"""Lists all Android apps in the associated Firebase project.
|
||||
|
||||
Args:
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
list: a list of ``AndroidApp`` instances referring to each Android app in the Firebase
|
||||
project.
|
||||
"""
|
||||
return _get_project_management_service(app).list_android_apps()
|
||||
|
||||
|
||||
def list_ios_apps(app=None):
|
||||
"""Lists all iOS apps in the associated Firebase project.
|
||||
|
||||
Args:
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
list: a list of ``IOSApp`` instances referring to each iOS app in the Firebase project.
|
||||
"""
|
||||
return _get_project_management_service(app).list_ios_apps()
|
||||
|
||||
|
||||
def create_android_app(package_name, display_name=None, app=None):
|
||||
"""Creates a new Android app in the associated Firebase project.
|
||||
|
||||
Args:
|
||||
package_name: The package name of the Android app to be created.
|
||||
display_name: A nickname for this Android app (optional).
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
AndroidApp: An ``AndroidApp`` instance that is a reference to the newly created app.
|
||||
"""
|
||||
return _get_project_management_service(app).create_android_app(package_name, display_name)
|
||||
|
||||
|
||||
def create_ios_app(bundle_id, display_name=None, app=None):
|
||||
"""Creates a new iOS app in the associated Firebase project.
|
||||
|
||||
Args:
|
||||
bundle_id: The bundle ID of the iOS app to be created.
|
||||
display_name: A nickname for this iOS app (optional).
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
IOSApp: An ``IOSApp`` instance that is a reference to the newly created app.
|
||||
"""
|
||||
return _get_project_management_service(app).create_ios_app(bundle_id, display_name)
|
||||
|
||||
|
||||
def _check_is_string_or_none(obj, field_name):
|
||||
if obj is None or isinstance(obj, str):
|
||||
return obj
|
||||
raise ValueError('{0} must be a string.'.format(field_name))
|
||||
|
||||
|
||||
def _check_is_nonempty_string(obj, field_name):
|
||||
if isinstance(obj, str) and obj:
|
||||
return obj
|
||||
raise ValueError('{0} must be a non-empty string.'.format(field_name))
|
||||
|
||||
|
||||
def _check_is_nonempty_string_or_none(obj, field_name):
|
||||
if obj is None:
|
||||
return None
|
||||
return _check_is_nonempty_string(obj, field_name)
|
||||
|
||||
|
||||
def _check_not_none(obj, field_name):
|
||||
if obj is None:
|
||||
raise ValueError('{0} cannot be None.'.format(field_name))
|
||||
return obj
|
||||
|
||||
|
||||
class AndroidApp:
|
||||
"""A reference to an Android app within a Firebase project.
|
||||
|
||||
Note: Unless otherwise specified, all methods defined in this class make an RPC.
|
||||
|
||||
Please use the module-level function ``android_app(app_id)`` to obtain instances of this class
|
||||
instead of instantiating it directly.
|
||||
"""
|
||||
|
||||
def __init__(self, app_id, service):
|
||||
self._app_id = app_id
|
||||
self._service = service
|
||||
|
||||
@property
|
||||
def app_id(self):
|
||||
"""Returns the app ID of the Android app to which this instance refers.
|
||||
|
||||
Note: This method does not make an RPC.
|
||||
|
||||
Returns:
|
||||
string: The app ID of the Android app to which this instance refers.
|
||||
"""
|
||||
return self._app_id
|
||||
|
||||
def get_metadata(self):
|
||||
"""Retrieves detailed information about this Android app.
|
||||
|
||||
Returns:
|
||||
AndroidAppMetadata: An ``AndroidAppMetadata`` instance.
|
||||
|
||||
Raises:
|
||||
FirebaseError: If an error occurs while communicating with the Firebase Project
|
||||
Management Service.
|
||||
"""
|
||||
return self._service.get_android_app_metadata(self._app_id)
|
||||
|
||||
def set_display_name(self, new_display_name):
|
||||
"""Updates the display name attribute of this Android app to the one given.
|
||||
|
||||
Args:
|
||||
new_display_name: The new display name for this Android app.
|
||||
|
||||
Returns:
|
||||
NoneType: None.
|
||||
|
||||
Raises:
|
||||
FirebaseError: If an error occurs while communicating with the Firebase Project
|
||||
Management Service.
|
||||
"""
|
||||
return self._service.set_android_app_display_name(self._app_id, new_display_name)
|
||||
|
||||
def get_config(self):
|
||||
"""Retrieves the configuration artifact associated with this Android app."""
|
||||
return self._service.get_android_app_config(self._app_id)
|
||||
|
||||
def get_sha_certificates(self):
|
||||
"""Retrieves the entire list of SHA certificates associated with this Android app.
|
||||
|
||||
Returns:
|
||||
list: A list of ``SHACertificate`` instances.
|
||||
|
||||
Raises:
|
||||
FirebaseError: If an error occurs while communicating with the Firebase Project
|
||||
Management Service.
|
||||
"""
|
||||
return self._service.get_sha_certificates(self._app_id)
|
||||
|
||||
def add_sha_certificate(self, certificate_to_add):
|
||||
"""Adds a SHA certificate to this Android app.
|
||||
|
||||
Args:
|
||||
certificate_to_add: The SHA certificate to add.
|
||||
|
||||
Returns:
|
||||
NoneType: None.
|
||||
|
||||
Raises:
|
||||
FirebaseError: If an error occurs while communicating with the Firebase Project
|
||||
Management Service. (For example, if the certificate_to_add already exists.)
|
||||
"""
|
||||
return self._service.add_sha_certificate(self._app_id, certificate_to_add)
|
||||
|
||||
def delete_sha_certificate(self, certificate_to_delete):
|
||||
"""Removes a SHA certificate from this Android app.
|
||||
|
||||
Args:
|
||||
certificate_to_delete: The SHA certificate to delete.
|
||||
|
||||
Returns:
|
||||
NoneType: None.
|
||||
|
||||
Raises:
|
||||
FirebaseError: If an error occurs while communicating with the Firebase Project
|
||||
Management Service. (For example, if the certificate_to_delete is not found.)
|
||||
"""
|
||||
return self._service.delete_sha_certificate(certificate_to_delete)
|
||||
|
||||
|
||||
class IOSApp:
|
||||
"""A reference to an iOS app within a Firebase project.
|
||||
|
||||
Note: Unless otherwise specified, all methods defined in this class make an RPC.
|
||||
|
||||
Please use the module-level function ``ios_app(app_id)`` to obtain instances of this class
|
||||
instead of instantiating it directly.
|
||||
"""
|
||||
|
||||
def __init__(self, app_id, service):
|
||||
self._app_id = app_id
|
||||
self._service = service
|
||||
|
||||
@property
|
||||
def app_id(self):
|
||||
"""Returns the app ID of the iOS app to which this instance refers.
|
||||
|
||||
Note: This method does not make an RPC.
|
||||
|
||||
Returns:
|
||||
string: The app ID of the iOS app to which this instance refers.
|
||||
"""
|
||||
return self._app_id
|
||||
|
||||
def get_metadata(self):
|
||||
"""Retrieves detailed information about this iOS app.
|
||||
|
||||
Returns:
|
||||
IOSAppMetadata: An ``IOSAppMetadata`` instance.
|
||||
|
||||
Raises:
|
||||
FirebaseError: If an error occurs while communicating with the Firebase Project
|
||||
Management Service.
|
||||
"""
|
||||
return self._service.get_ios_app_metadata(self._app_id)
|
||||
|
||||
def set_display_name(self, new_display_name):
|
||||
"""Updates the display name attribute of this iOS app to the one given.
|
||||
|
||||
Args:
|
||||
new_display_name: The new display name for this iOS app.
|
||||
|
||||
Returns:
|
||||
NoneType: None.
|
||||
|
||||
Raises:
|
||||
FirebaseError: If an error occurs while communicating with the Firebase Project
|
||||
Management Service.
|
||||
"""
|
||||
return self._service.set_ios_app_display_name(self._app_id, new_display_name)
|
||||
|
||||
def get_config(self):
|
||||
"""Retrieves the configuration artifact associated with this iOS app."""
|
||||
return self._service.get_ios_app_config(self._app_id)
|
||||
|
||||
|
||||
class _AppMetadata:
|
||||
"""Detailed information about a Firebase Android or iOS app."""
|
||||
|
||||
def __init__(self, name, app_id, display_name, project_id):
|
||||
# _name is the fully qualified resource name of this Android or iOS app; currently it is not
|
||||
# exposed to client code.
|
||||
self._name = _check_is_nonempty_string(name, 'name')
|
||||
self._app_id = _check_is_nonempty_string(app_id, 'app_id')
|
||||
self._display_name = _check_is_string_or_none(display_name, 'display_name')
|
||||
self._project_id = _check_is_nonempty_string(project_id, 'project_id')
|
||||
|
||||
@property
|
||||
def app_id(self):
|
||||
"""The globally unique, Firebase-assigned identifier of this Android or iOS app.
|
||||
|
||||
This ID is unique even across apps of different platforms.
|
||||
"""
|
||||
return self._app_id
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
"""The user-assigned display name of this Android or iOS app.
|
||||
|
||||
Note that the display name can be None if it has never been set by the user."""
|
||||
return self._display_name
|
||||
|
||||
@property
|
||||
def project_id(self):
|
||||
"""The permanent, globally unique, user-assigned ID of the parent Firebase project."""
|
||||
return self._project_id
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, type(self)):
|
||||
return False
|
||||
# pylint: disable=protected-access
|
||||
return (self._name == other._name and self.app_id == other.app_id and
|
||||
self.display_name == other.display_name and self.project_id == other.project_id)
|
||||
# pylint: enable=protected-access
|
||||
|
||||
|
||||
class AndroidAppMetadata(_AppMetadata):
|
||||
"""Android-specific information about an Android Firebase app."""
|
||||
|
||||
def __init__(self, package_name, name, app_id, display_name, project_id):
|
||||
"""Clients should not instantiate this class directly."""
|
||||
super(AndroidAppMetadata, self).__init__(name, app_id, display_name, project_id)
|
||||
self._package_name = _check_is_nonempty_string(package_name, 'package_name')
|
||||
|
||||
@property
|
||||
def package_name(self):
|
||||
"""The canonical package name of this Android app as it would appear in the Play Store."""
|
||||
return self._package_name
|
||||
|
||||
def __eq__(self, other):
|
||||
return (super(AndroidAppMetadata, self).__eq__(other) and
|
||||
self.package_name == other.package_name)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(
|
||||
(self._name, self.app_id, self.display_name, self.project_id, self.package_name))
|
||||
|
||||
|
||||
class IOSAppMetadata(_AppMetadata):
|
||||
"""iOS-specific information about an iOS Firebase app."""
|
||||
|
||||
def __init__(self, bundle_id, name, app_id, display_name, project_id):
|
||||
"""Clients should not instantiate this class directly."""
|
||||
super(IOSAppMetadata, self).__init__(name, app_id, display_name, project_id)
|
||||
self._bundle_id = _check_is_nonempty_string(bundle_id, 'bundle_id')
|
||||
|
||||
@property
|
||||
def bundle_id(self):
|
||||
"""The canonical bundle ID of this iOS app as it would appear in the iOS AppStore."""
|
||||
return self._bundle_id
|
||||
|
||||
def __eq__(self, other):
|
||||
return super(IOSAppMetadata, self).__eq__(other) and self.bundle_id == other.bundle_id
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self._name, self.app_id, self.display_name, self.project_id, self.bundle_id))
|
||||
|
||||
|
||||
class SHACertificate:
|
||||
"""Represents a SHA-1 or SHA-256 certificate associated with an Android app."""
|
||||
|
||||
SHA_1 = 'SHA_1'
|
||||
SHA_256 = 'SHA_256'
|
||||
|
||||
_SHA_1_RE = re.compile('^[0-9A-Fa-f]{40}$')
|
||||
_SHA_256_RE = re.compile('^[0-9A-Fa-f]{64}$')
|
||||
|
||||
def __init__(self, sha_hash, name=None):
|
||||
"""Creates a new SHACertificate instance.
|
||||
|
||||
Args:
|
||||
sha_hash: A string; the certificate hash for the Android app.
|
||||
name: The fully qualified resource name of this certificate; note that this field should
|
||||
be omitted if the instance is being constructed for the purpose of calling the
|
||||
add_sha_certificate() method on an ``AndroidApp``.
|
||||
|
||||
Raises:
|
||||
ValueError: If the sha_hash is not a valid SHA-1 or SHA-256 certificate hash.
|
||||
"""
|
||||
_check_is_nonempty_string(sha_hash, 'sha_hash')
|
||||
_check_is_nonempty_string_or_none(name, 'name')
|
||||
self._name = name
|
||||
self._sha_hash = sha_hash.lower()
|
||||
if SHACertificate._SHA_1_RE.match(sha_hash):
|
||||
self._cert_type = SHACertificate.SHA_1
|
||||
elif SHACertificate._SHA_256_RE.match(sha_hash):
|
||||
self._cert_type = SHACertificate.SHA_256
|
||||
else:
|
||||
raise ValueError(
|
||||
'The supplied certificate hash is neither a valid SHA-1 nor SHA_256 hash.')
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Returns the fully qualified resource name of this certificate, if known.
|
||||
|
||||
Returns:
|
||||
string: The fully qualified resource name of this certificate, if known; otherwise, the
|
||||
empty string.
|
||||
"""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def sha_hash(self):
|
||||
"""Returns the certificate hash.
|
||||
|
||||
Returns:
|
||||
string: The certificate hash.
|
||||
"""
|
||||
return self._sha_hash
|
||||
|
||||
@property
|
||||
def cert_type(self):
|
||||
"""Returns the type of the SHA certificate encoded in the hash.
|
||||
|
||||
Returns:
|
||||
string: One of 'SHA_1' or 'SHA_256'.
|
||||
"""
|
||||
return self._cert_type
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, SHACertificate):
|
||||
return False
|
||||
return (self.name == other.name and self.sha_hash == other.sha_hash and
|
||||
self.cert_type == other.cert_type)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.sha_hash, self.cert_type))
|
||||
|
||||
|
||||
class _ProjectManagementService:
|
||||
"""Provides methods for interacting with the Firebase Project Management Service."""
|
||||
|
||||
BASE_URL = 'https://firebase.googleapis.com'
|
||||
MAXIMUM_LIST_APPS_PAGE_SIZE = 100
|
||||
MAXIMUM_POLLING_ATTEMPTS = 8
|
||||
POLL_BASE_WAIT_TIME_SECONDS = 0.5
|
||||
POLL_EXPONENTIAL_BACKOFF_FACTOR = 1.5
|
||||
|
||||
ANDROID_APPS_RESOURCE_NAME = 'androidApps'
|
||||
ANDROID_APP_IDENTIFIER_NAME = 'packageName'
|
||||
IOS_APPS_RESOURCE_NAME = 'iosApps'
|
||||
IOS_APP_IDENTIFIER_NAME = 'bundleId'
|
||||
|
||||
def __init__(self, app):
|
||||
project_id = app.project_id
|
||||
if not project_id:
|
||||
raise ValueError(
|
||||
'Project ID is required to access the Firebase Project Management Service. Either '
|
||||
'set the projectId option, or use service account credentials. Alternatively, set '
|
||||
'the GOOGLE_CLOUD_PROJECT environment variable.')
|
||||
self._project_id = project_id
|
||||
version_header = 'Python/Admin/{0}'.format(firebase_admin.__version__)
|
||||
timeout = app.options.get('httpTimeout', _http_client.DEFAULT_TIMEOUT_SECONDS)
|
||||
self._client = _http_client.JsonHttpClient(
|
||||
credential=app.credential.get_credential(),
|
||||
base_url=_ProjectManagementService.BASE_URL,
|
||||
headers={'X-Client-Version': version_header},
|
||||
timeout=timeout)
|
||||
|
||||
def get_android_app_metadata(self, app_id):
|
||||
return self._get_app_metadata(
|
||||
platform_resource_name=_ProjectManagementService.ANDROID_APPS_RESOURCE_NAME,
|
||||
identifier_name=_ProjectManagementService.ANDROID_APP_IDENTIFIER_NAME,
|
||||
metadata_class=AndroidAppMetadata,
|
||||
app_id=app_id)
|
||||
|
||||
def get_ios_app_metadata(self, app_id):
|
||||
return self._get_app_metadata(
|
||||
platform_resource_name=_ProjectManagementService.IOS_APPS_RESOURCE_NAME,
|
||||
identifier_name=_ProjectManagementService.IOS_APP_IDENTIFIER_NAME,
|
||||
metadata_class=IOSAppMetadata,
|
||||
app_id=app_id)
|
||||
|
||||
def _get_app_metadata(self, platform_resource_name, identifier_name, metadata_class, app_id):
|
||||
"""Retrieves detailed information about an Android or iOS app."""
|
||||
_check_is_nonempty_string(app_id, 'app_id')
|
||||
path = '/v1beta1/projects/-/{0}/{1}'.format(platform_resource_name, app_id)
|
||||
response = self._make_request('get', path)
|
||||
return metadata_class(
|
||||
response[identifier_name],
|
||||
name=response['name'],
|
||||
app_id=response['appId'],
|
||||
display_name=response.get('displayName') or None,
|
||||
project_id=response['projectId'])
|
||||
|
||||
def set_android_app_display_name(self, app_id, new_display_name):
|
||||
self._set_display_name(
|
||||
app_id=app_id,
|
||||
new_display_name=new_display_name,
|
||||
platform_resource_name=_ProjectManagementService.ANDROID_APPS_RESOURCE_NAME)
|
||||
|
||||
def set_ios_app_display_name(self, app_id, new_display_name):
|
||||
self._set_display_name(
|
||||
app_id=app_id,
|
||||
new_display_name=new_display_name,
|
||||
platform_resource_name=_ProjectManagementService.IOS_APPS_RESOURCE_NAME)
|
||||
|
||||
def _set_display_name(self, app_id, new_display_name, platform_resource_name):
|
||||
"""Sets the display name of an Android or iOS app."""
|
||||
path = '/v1beta1/projects/-/{0}/{1}?updateMask=displayName'.format(
|
||||
platform_resource_name, app_id)
|
||||
request_body = {'displayName': new_display_name}
|
||||
self._make_request('patch', path, json=request_body)
|
||||
|
||||
def list_android_apps(self):
|
||||
return self._list_apps(
|
||||
platform_resource_name=_ProjectManagementService.ANDROID_APPS_RESOURCE_NAME,
|
||||
app_class=AndroidApp)
|
||||
|
||||
def list_ios_apps(self):
|
||||
return self._list_apps(
|
||||
platform_resource_name=_ProjectManagementService.IOS_APPS_RESOURCE_NAME,
|
||||
app_class=IOSApp)
|
||||
|
||||
def _list_apps(self, platform_resource_name, app_class):
|
||||
"""Lists all the Android or iOS apps within the Firebase project."""
|
||||
path = '/v1beta1/projects/{0}/{1}?pageSize={2}'.format(
|
||||
self._project_id,
|
||||
platform_resource_name,
|
||||
_ProjectManagementService.MAXIMUM_LIST_APPS_PAGE_SIZE)
|
||||
response = self._make_request('get', path)
|
||||
apps_list = []
|
||||
while True:
|
||||
apps = response.get('apps')
|
||||
if not apps:
|
||||
break
|
||||
apps_list.extend(app_class(app_id=app['appId'], service=self) for app in apps)
|
||||
next_page_token = response.get('nextPageToken')
|
||||
if not next_page_token:
|
||||
break
|
||||
# Retrieve the next page of apps.
|
||||
path = '/v1beta1/projects/{0}/{1}?pageToken={2}&pageSize={3}'.format(
|
||||
self._project_id,
|
||||
platform_resource_name,
|
||||
next_page_token,
|
||||
_ProjectManagementService.MAXIMUM_LIST_APPS_PAGE_SIZE)
|
||||
response = self._make_request('get', path)
|
||||
return apps_list
|
||||
|
||||
def create_android_app(self, package_name, display_name=None):
|
||||
return self._create_app(
|
||||
platform_resource_name=_ProjectManagementService.ANDROID_APPS_RESOURCE_NAME,
|
||||
identifier_name=_ProjectManagementService.ANDROID_APP_IDENTIFIER_NAME,
|
||||
identifier=package_name,
|
||||
display_name=display_name,
|
||||
app_class=AndroidApp)
|
||||
|
||||
def create_ios_app(self, bundle_id, display_name=None):
|
||||
return self._create_app(
|
||||
platform_resource_name=_ProjectManagementService.IOS_APPS_RESOURCE_NAME,
|
||||
identifier_name=_ProjectManagementService.IOS_APP_IDENTIFIER_NAME,
|
||||
identifier=bundle_id,
|
||||
display_name=display_name,
|
||||
app_class=IOSApp)
|
||||
|
||||
def _create_app(
|
||||
self,
|
||||
platform_resource_name,
|
||||
identifier_name,
|
||||
identifier,
|
||||
display_name,
|
||||
app_class):
|
||||
"""Creates an Android or iOS app."""
|
||||
_check_is_string_or_none(display_name, 'display_name')
|
||||
path = '/v1beta1/projects/{0}/{1}'.format(self._project_id, platform_resource_name)
|
||||
request_body = {identifier_name: identifier}
|
||||
if display_name:
|
||||
request_body['displayName'] = display_name
|
||||
response = self._make_request('post', path, json=request_body)
|
||||
operation_name = response['name']
|
||||
poll_response = self._poll_app_creation(operation_name)
|
||||
return app_class(app_id=poll_response['appId'], service=self)
|
||||
|
||||
def _poll_app_creation(self, operation_name):
|
||||
"""Polls the Long-Running Operation repeatedly until it is done with exponential backoff."""
|
||||
for current_attempt in range(_ProjectManagementService.MAXIMUM_POLLING_ATTEMPTS):
|
||||
delay_factor = pow(
|
||||
_ProjectManagementService.POLL_EXPONENTIAL_BACKOFF_FACTOR, current_attempt)
|
||||
wait_time_seconds = delay_factor * _ProjectManagementService.POLL_BASE_WAIT_TIME_SECONDS
|
||||
time.sleep(wait_time_seconds)
|
||||
path = '/v1/{0}'.format(operation_name)
|
||||
poll_response, http_response = self._body_and_response('get', path)
|
||||
done = poll_response.get('done')
|
||||
if done:
|
||||
response = poll_response.get('response')
|
||||
if response:
|
||||
return response
|
||||
|
||||
raise exceptions.UnknownError(
|
||||
'Polling finished, but the operation terminated in an error.',
|
||||
http_response=http_response)
|
||||
raise exceptions.DeadlineExceededError('Polling deadline exceeded.')
|
||||
|
||||
def get_android_app_config(self, app_id):
|
||||
return self._get_app_config(
|
||||
platform_resource_name=_ProjectManagementService.ANDROID_APPS_RESOURCE_NAME,
|
||||
app_id=app_id)
|
||||
|
||||
def get_ios_app_config(self, app_id):
|
||||
return self._get_app_config(
|
||||
platform_resource_name=_ProjectManagementService.IOS_APPS_RESOURCE_NAME, app_id=app_id)
|
||||
|
||||
def _get_app_config(self, platform_resource_name, app_id):
|
||||
path = '/v1beta1/projects/-/{0}/{1}/config'.format(platform_resource_name, app_id)
|
||||
response = self._make_request('get', path)
|
||||
# In Python 2.7, the base64 module works with strings, while in Python 3, it works with
|
||||
# bytes objects. This line works in both versions.
|
||||
return base64.standard_b64decode(response['configFileContents']).decode(encoding='utf-8')
|
||||
|
||||
def get_sha_certificates(self, app_id):
|
||||
path = '/v1beta1/projects/-/androidApps/{0}/sha'.format(app_id)
|
||||
response = self._make_request('get', path)
|
||||
cert_list = response.get('certificates') or []
|
||||
return [SHACertificate(sha_hash=cert['shaHash'], name=cert['name']) for cert in cert_list]
|
||||
|
||||
def add_sha_certificate(self, app_id, certificate_to_add):
|
||||
path = '/v1beta1/projects/-/androidApps/{0}/sha'.format(app_id)
|
||||
sha_hash = _check_not_none(certificate_to_add, 'certificate_to_add').sha_hash
|
||||
cert_type = certificate_to_add.cert_type
|
||||
request_body = {'shaHash': sha_hash, 'certType': cert_type}
|
||||
self._make_request('post', path, json=request_body)
|
||||
|
||||
def delete_sha_certificate(self, certificate_to_delete):
|
||||
name = _check_not_none(certificate_to_delete, 'certificate_to_delete').name
|
||||
path = '/v1beta1/{0}'.format(name)
|
||||
self._make_request('delete', path)
|
||||
|
||||
def _make_request(self, method, url, json=None):
|
||||
body, _ = self._body_and_response(method, url, json)
|
||||
return body
|
||||
|
||||
def _body_and_response(self, method, url, json=None):
|
||||
try:
|
||||
return self._client.body_and_response(method=method, url=url, json=json)
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise _utils.handle_platform_error_from_requests(error)
|
82
venv/Lib/site-packages/firebase_admin/storage.py
Normal file
82
venv/Lib/site-packages/firebase_admin/storage.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
# 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 Cloud Storage module.
|
||||
|
||||
This module contains utilities for accessing Google Cloud Storage buckets associated with
|
||||
Firebase apps. This requires the ``google-cloud-storage`` Python module.
|
||||
"""
|
||||
|
||||
# pylint: disable=import-error,no-name-in-module
|
||||
try:
|
||||
from google.cloud import storage
|
||||
except ImportError:
|
||||
raise ImportError('Failed to import the Cloud Storage library for Python. Make sure '
|
||||
'to install the "google-cloud-storage" module.')
|
||||
|
||||
from firebase_admin import _utils
|
||||
|
||||
|
||||
_STORAGE_ATTRIBUTE = '_storage'
|
||||
|
||||
def bucket(name=None, app=None):
|
||||
"""Returns a handle to a Google Cloud Storage bucket.
|
||||
|
||||
If the name argument is not provided, uses the 'storageBucket' option specified when
|
||||
initializing the App. If that is also not available raises an error. This function
|
||||
does not make any RPC calls.
|
||||
|
||||
Args:
|
||||
name: Name of a cloud storage bucket (optional).
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
google.cloud.storage.Bucket: A handle to the specified bucket.
|
||||
|
||||
Raises:
|
||||
ValueError: If a bucket name is not specified either via options or method arguments,
|
||||
or if the specified bucket name is not a valid string.
|
||||
"""
|
||||
client = _utils.get_app_service(app, _STORAGE_ATTRIBUTE, _StorageClient.from_app)
|
||||
return client.bucket(name)
|
||||
|
||||
|
||||
class _StorageClient:
|
||||
"""Holds a Google Cloud Storage client instance."""
|
||||
|
||||
def __init__(self, credentials, project, default_bucket):
|
||||
self._client = storage.Client(credentials=credentials, project=project)
|
||||
self._default_bucket = default_bucket
|
||||
|
||||
@classmethod
|
||||
def from_app(cls, app):
|
||||
credentials = app.credential.get_credential()
|
||||
default_bucket = app.options.get('storageBucket')
|
||||
# Specifying project ID is not required, but providing it when available
|
||||
# significantly speeds up the initialization of the storage client.
|
||||
return _StorageClient(credentials, app.project_id, default_bucket)
|
||||
|
||||
def bucket(self, name=None):
|
||||
"""Returns a handle to the specified Cloud Storage Bucket."""
|
||||
bucket_name = name if name is not None else self._default_bucket
|
||||
if bucket_name is None:
|
||||
raise ValueError(
|
||||
'Storage bucket name not specified. Specify the bucket name via the '
|
||||
'"storageBucket" option when initializing the App, or specify the bucket '
|
||||
'name explicitly when calling the storage.bucket() function.')
|
||||
if not bucket_name or not isinstance(bucket_name, str):
|
||||
raise ValueError(
|
||||
'Invalid storage bucket name: "{0}". Bucket name must be a non-empty '
|
||||
'string.'.format(bucket_name))
|
||||
return self._client.bucket(bucket_name)
|
445
venv/Lib/site-packages/firebase_admin/tenant_mgt.py
Normal file
445
venv/Lib/site-packages/firebase_admin/tenant_mgt.py
Normal file
|
@ -0,0 +1,445 @@
|
|||
# Copyright 2020 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 tenant management module.
|
||||
|
||||
This module contains functions for creating and configuring authentication tenants within a
|
||||
Google Cloud Identity Platform (GCIP) instance.
|
||||
"""
|
||||
|
||||
import re
|
||||
import threading
|
||||
|
||||
import requests
|
||||
|
||||
import firebase_admin
|
||||
from firebase_admin import auth
|
||||
from firebase_admin import _auth_utils
|
||||
from firebase_admin import _http_client
|
||||
from firebase_admin import _utils
|
||||
|
||||
|
||||
_TENANT_MGT_ATTRIBUTE = '_tenant_mgt'
|
||||
_MAX_LIST_TENANTS_RESULTS = 100
|
||||
_DISPLAY_NAME_PATTERN = re.compile('^[a-zA-Z][a-zA-Z0-9-]{3,19}$')
|
||||
|
||||
|
||||
__all__ = [
|
||||
'ListTenantsPage',
|
||||
'Tenant',
|
||||
'TenantIdMismatchError',
|
||||
'TenantNotFoundError',
|
||||
|
||||
'auth_for_tenant',
|
||||
'create_tenant',
|
||||
'delete_tenant',
|
||||
'get_tenant',
|
||||
'list_tenants',
|
||||
'update_tenant',
|
||||
]
|
||||
|
||||
|
||||
TenantIdMismatchError = _auth_utils.TenantIdMismatchError
|
||||
TenantNotFoundError = _auth_utils.TenantNotFoundError
|
||||
|
||||
|
||||
def auth_for_tenant(tenant_id, app=None):
|
||||
"""Gets an Auth Client instance scoped to the given tenant ID.
|
||||
|
||||
Args:
|
||||
tenant_id: A tenant ID string.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
auth.Client: An ``auth.Client`` object.
|
||||
|
||||
Raises:
|
||||
ValueError: If the tenant ID is None, empty or not a string.
|
||||
"""
|
||||
tenant_mgt_service = _get_tenant_mgt_service(app)
|
||||
return tenant_mgt_service.auth_for_tenant(tenant_id)
|
||||
|
||||
|
||||
def get_tenant(tenant_id, app=None):
|
||||
"""Gets the tenant corresponding to the given ``tenant_id``.
|
||||
|
||||
Args:
|
||||
tenant_id: A tenant ID string.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
Tenant: A tenant object.
|
||||
|
||||
Raises:
|
||||
ValueError: If the tenant ID is None, empty or not a string.
|
||||
TenantNotFoundError: If no tenant exists by the given ID.
|
||||
FirebaseError: If an error occurs while retrieving the tenant.
|
||||
"""
|
||||
tenant_mgt_service = _get_tenant_mgt_service(app)
|
||||
return tenant_mgt_service.get_tenant(tenant_id)
|
||||
|
||||
|
||||
def create_tenant(
|
||||
display_name, allow_password_sign_up=None, enable_email_link_sign_in=None, app=None):
|
||||
"""Creates a new tenant from the given options.
|
||||
|
||||
Args:
|
||||
display_name: Display name string for the new tenant. Must begin with a letter and contain
|
||||
only letters, digits and hyphens. Length must be between 4 and 20.
|
||||
allow_password_sign_up: A boolean indicating whether to enable or disable the email sign-in
|
||||
provider (optional).
|
||||
enable_email_link_sign_in: A boolean indicating whether to enable or disable email link
|
||||
sign-in (optional). Disabling this makes the password required for email sign-in.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
Tenant: A tenant object.
|
||||
|
||||
Raises:
|
||||
ValueError: If any of the given arguments are invalid.
|
||||
FirebaseError: If an error occurs while creating the tenant.
|
||||
"""
|
||||
tenant_mgt_service = _get_tenant_mgt_service(app)
|
||||
return tenant_mgt_service.create_tenant(
|
||||
display_name=display_name, allow_password_sign_up=allow_password_sign_up,
|
||||
enable_email_link_sign_in=enable_email_link_sign_in)
|
||||
|
||||
|
||||
def update_tenant(
|
||||
tenant_id, display_name=None, allow_password_sign_up=None, enable_email_link_sign_in=None,
|
||||
app=None):
|
||||
"""Updates an existing tenant with the given options.
|
||||
|
||||
Args:
|
||||
tenant_id: ID of the tenant to update.
|
||||
display_name: Updated display name string for the tenant (optional).
|
||||
allow_password_sign_up: A boolean indicating whether to enable or disable the email sign-in
|
||||
provider.
|
||||
enable_email_link_sign_in: A boolean indicating whether to enable or disable email link
|
||||
sign-in. Disabling this makes the password required for email sign-in.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
Tenant: The updated tenant object.
|
||||
|
||||
Raises:
|
||||
ValueError: If any of the given arguments are invalid.
|
||||
TenantNotFoundError: If no tenant exists by the given ID.
|
||||
FirebaseError: If an error occurs while creating the tenant.
|
||||
"""
|
||||
tenant_mgt_service = _get_tenant_mgt_service(app)
|
||||
return tenant_mgt_service.update_tenant(
|
||||
tenant_id, display_name=display_name, allow_password_sign_up=allow_password_sign_up,
|
||||
enable_email_link_sign_in=enable_email_link_sign_in)
|
||||
|
||||
|
||||
def delete_tenant(tenant_id, app=None):
|
||||
"""Deletes the tenant corresponding to the given ``tenant_id``.
|
||||
|
||||
Args:
|
||||
tenant_id: A tenant ID string.
|
||||
app: An App instance (optional).
|
||||
|
||||
Raises:
|
||||
ValueError: If the tenant ID is None, empty or not a string.
|
||||
TenantNotFoundError: If no tenant exists by the given ID.
|
||||
FirebaseError: If an error occurs while retrieving the tenant.
|
||||
"""
|
||||
tenant_mgt_service = _get_tenant_mgt_service(app)
|
||||
tenant_mgt_service.delete_tenant(tenant_id)
|
||||
|
||||
|
||||
def list_tenants(page_token=None, max_results=_MAX_LIST_TENANTS_RESULTS, app=None):
|
||||
"""Retrieves a page of tenants from a Firebase project.
|
||||
|
||||
The ``page_token`` argument governs the starting point of the page. The ``max_results``
|
||||
argument governs the maximum number of tenants that may be included in the returned page.
|
||||
This function never returns None. If there are no user accounts in the Firebase project, this
|
||||
returns an empty page.
|
||||
|
||||
Args:
|
||||
page_token: A non-empty page token string, which indicates the starting point of the page
|
||||
(optional). Defaults to ``None``, which will retrieve the first page of users.
|
||||
max_results: A positive integer indicating the maximum number of users to include in the
|
||||
returned page (optional). Defaults to 100, which is also the maximum number allowed.
|
||||
app: An App instance (optional).
|
||||
|
||||
Returns:
|
||||
ListTenantsPage: A page of tenants.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``max_results`` or ``page_token`` are invalid.
|
||||
FirebaseError: If an error occurs while retrieving the user accounts.
|
||||
"""
|
||||
tenant_mgt_service = _get_tenant_mgt_service(app)
|
||||
def download(page_token, max_results):
|
||||
return tenant_mgt_service.list_tenants(page_token, max_results)
|
||||
return ListTenantsPage(download, page_token, max_results)
|
||||
|
||||
|
||||
def _get_tenant_mgt_service(app):
|
||||
return _utils.get_app_service(app, _TENANT_MGT_ATTRIBUTE, _TenantManagementService)
|
||||
|
||||
|
||||
class Tenant:
|
||||
"""Represents a tenant in a multi-tenant application.
|
||||
|
||||
Multi-tenancy support requires Google Cloud Identity Platform (GCIP). To learn more about
|
||||
GCIP including pricing and features, see https://cloud.google.com/identity-platform.
|
||||
|
||||
Before multi-tenancy can be used in a Google Cloud Identity Platform project, tenants must be
|
||||
enabled in that project via the Cloud Console UI. A Tenant instance provides information
|
||||
such as the display name, tenant identifier and email authentication configuration.
|
||||
"""
|
||||
|
||||
def __init__(self, data):
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError('Invalid data argument in Tenant constructor: {0}'.format(data))
|
||||
if not 'name' in data:
|
||||
raise ValueError('Tenant response missing required keys.')
|
||||
|
||||
self._data = data
|
||||
|
||||
@property
|
||||
def tenant_id(self):
|
||||
name = self._data['name']
|
||||
return name.split('/')[-1]
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return self._data.get('displayName')
|
||||
|
||||
@property
|
||||
def allow_password_sign_up(self):
|
||||
return self._data.get('allowPasswordSignup', False)
|
||||
|
||||
@property
|
||||
def enable_email_link_sign_in(self):
|
||||
return self._data.get('enableEmailLinkSignin', False)
|
||||
|
||||
|
||||
class _TenantManagementService:
|
||||
"""Firebase tenant management service."""
|
||||
|
||||
TENANT_MGT_URL = 'https://identitytoolkit.googleapis.com/v2beta1'
|
||||
|
||||
def __init__(self, app):
|
||||
credential = app.credential.get_credential()
|
||||
version_header = 'Python/Admin/{0}'.format(firebase_admin.__version__)
|
||||
base_url = '{0}/projects/{1}'.format(self.TENANT_MGT_URL, app.project_id)
|
||||
self.app = app
|
||||
self.client = _http_client.JsonHttpClient(
|
||||
credential=credential, base_url=base_url, headers={'X-Client-Version': version_header})
|
||||
self.tenant_clients = {}
|
||||
self.lock = threading.RLock()
|
||||
|
||||
def auth_for_tenant(self, tenant_id):
|
||||
"""Gets an Auth Client instance scoped to the given tenant ID."""
|
||||
if not isinstance(tenant_id, str) or not tenant_id:
|
||||
raise ValueError(
|
||||
'Invalid tenant ID: {0}. Tenant ID must be a non-empty string.'.format(tenant_id))
|
||||
|
||||
with self.lock:
|
||||
if tenant_id in self.tenant_clients:
|
||||
return self.tenant_clients[tenant_id]
|
||||
|
||||
client = auth.Client(self.app, tenant_id=tenant_id)
|
||||
self.tenant_clients[tenant_id] = client
|
||||
return client
|
||||
|
||||
def get_tenant(self, tenant_id):
|
||||
"""Gets the tenant corresponding to the given ``tenant_id``."""
|
||||
if not isinstance(tenant_id, str) or not tenant_id:
|
||||
raise ValueError(
|
||||
'Invalid tenant ID: {0}. Tenant ID must be a non-empty string.'.format(tenant_id))
|
||||
|
||||
try:
|
||||
body = self.client.body('get', '/tenants/{0}'.format(tenant_id))
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise _auth_utils.handle_auth_backend_error(error)
|
||||
else:
|
||||
return Tenant(body)
|
||||
|
||||
def create_tenant(
|
||||
self, display_name, allow_password_sign_up=None, enable_email_link_sign_in=None):
|
||||
"""Creates a new tenant from the given parameters."""
|
||||
|
||||
payload = {'displayName': _validate_display_name(display_name)}
|
||||
if allow_password_sign_up is not None:
|
||||
payload['allowPasswordSignup'] = _auth_utils.validate_boolean(
|
||||
allow_password_sign_up, 'allowPasswordSignup')
|
||||
if enable_email_link_sign_in is not None:
|
||||
payload['enableEmailLinkSignin'] = _auth_utils.validate_boolean(
|
||||
enable_email_link_sign_in, 'enableEmailLinkSignin')
|
||||
|
||||
try:
|
||||
body = self.client.body('post', '/tenants', json=payload)
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise _auth_utils.handle_auth_backend_error(error)
|
||||
else:
|
||||
return Tenant(body)
|
||||
|
||||
def update_tenant(
|
||||
self, tenant_id, display_name=None, allow_password_sign_up=None,
|
||||
enable_email_link_sign_in=None):
|
||||
"""Updates the specified tenant with the given parameters."""
|
||||
if not isinstance(tenant_id, str) or not tenant_id:
|
||||
raise ValueError('Tenant ID must be a non-empty string.')
|
||||
|
||||
payload = {}
|
||||
if display_name is not None:
|
||||
payload['displayName'] = _validate_display_name(display_name)
|
||||
if allow_password_sign_up is not None:
|
||||
payload['allowPasswordSignup'] = _auth_utils.validate_boolean(
|
||||
allow_password_sign_up, 'allowPasswordSignup')
|
||||
if enable_email_link_sign_in is not None:
|
||||
payload['enableEmailLinkSignin'] = _auth_utils.validate_boolean(
|
||||
enable_email_link_sign_in, 'enableEmailLinkSignin')
|
||||
|
||||
if not payload:
|
||||
raise ValueError('At least one parameter must be specified for update.')
|
||||
|
||||
url = '/tenants/{0}'.format(tenant_id)
|
||||
update_mask = ','.join(_auth_utils.build_update_mask(payload))
|
||||
params = 'updateMask={0}'.format(update_mask)
|
||||
try:
|
||||
body = self.client.body('patch', url, json=payload, params=params)
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise _auth_utils.handle_auth_backend_error(error)
|
||||
else:
|
||||
return Tenant(body)
|
||||
|
||||
def delete_tenant(self, tenant_id):
|
||||
"""Deletes the tenant corresponding to the given ``tenant_id``."""
|
||||
if not isinstance(tenant_id, str) or not tenant_id:
|
||||
raise ValueError(
|
||||
'Invalid tenant ID: {0}. Tenant ID must be a non-empty string.'.format(tenant_id))
|
||||
|
||||
try:
|
||||
self.client.request('delete', '/tenants/{0}'.format(tenant_id))
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise _auth_utils.handle_auth_backend_error(error)
|
||||
|
||||
def list_tenants(self, page_token=None, max_results=_MAX_LIST_TENANTS_RESULTS):
|
||||
"""Retrieves a batch of tenants."""
|
||||
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_TENANTS_RESULTS:
|
||||
raise ValueError(
|
||||
'Max results must be a positive integer less than or equal to '
|
||||
'{0}.'.format(_MAX_LIST_TENANTS_RESULTS))
|
||||
|
||||
payload = {'pageSize': max_results}
|
||||
if page_token:
|
||||
payload['pageToken'] = page_token
|
||||
try:
|
||||
return self.client.body('get', '/tenants', params=payload)
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise _auth_utils.handle_auth_backend_error(error)
|
||||
|
||||
|
||||
class ListTenantsPage:
|
||||
"""Represents a page of tenants fetched from a Firebase project.
|
||||
|
||||
Provides methods for traversing tenants included in this page, as well as retrieving
|
||||
subsequent pages of tenants. The iterator returned by ``iterate_all()`` can be used to iterate
|
||||
through all tenants 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 tenants(self):
|
||||
"""A list of ``ExportedUserRecord`` instances available in this page."""
|
||||
return [Tenant(data) for data in self._current.get('tenants', [])]
|
||||
|
||||
@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 tenants, if available.
|
||||
|
||||
Returns:
|
||||
ListTenantsPage: Next page of tenants, or None if this is the last page.
|
||||
"""
|
||||
if self.has_next_page:
|
||||
return ListTenantsPage(self._download, self.next_page_token, self._max_results)
|
||||
return None
|
||||
|
||||
def iterate_all(self):
|
||||
"""Retrieves an iterator for tenants.
|
||||
|
||||
Returned iterator will iterate through all the tenants in the Firebase project
|
||||
starting from this page. The iterator will never buffer more than one page of tenants
|
||||
in memory at a time.
|
||||
|
||||
Returns:
|
||||
iterator: An iterator of Tenant instances.
|
||||
"""
|
||||
return _TenantIterator(self)
|
||||
|
||||
|
||||
class _TenantIterator:
|
||||
"""An iterator that allows iterating over tenants.
|
||||
|
||||
This implementation loads a page of tenants into memory, and iterates on them. When the whole
|
||||
page has been traversed, it loads another page. This class never keeps more than one page
|
||||
of entries in memory.
|
||||
"""
|
||||
|
||||
def __init__(self, current_page):
|
||||
if not current_page:
|
||||
raise ValueError('Current page must not be None.')
|
||||
self._current_page = current_page
|
||||
self._index = 0
|
||||
|
||||
def next(self):
|
||||
if self._index == len(self._current_page.tenants):
|
||||
if self._current_page.has_next_page:
|
||||
self._current_page = self._current_page.get_next_page()
|
||||
self._index = 0
|
||||
if self._index < len(self._current_page.tenants):
|
||||
result = self._current_page.tenants[self._index]
|
||||
self._index += 1
|
||||
return result
|
||||
raise StopIteration
|
||||
|
||||
def __next__(self):
|
||||
return self.next()
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
|
||||
def _validate_display_name(display_name):
|
||||
if not isinstance(display_name, str):
|
||||
raise ValueError('Invalid type for displayName')
|
||||
if not _DISPLAY_NAME_PATTERN.search(display_name):
|
||||
raise ValueError(
|
||||
'displayName must start with a letter and only consist of letters, digits and '
|
||||
'hyphens with 4-20 characters.')
|
||||
return display_name
|
Loading…
Add table
Add a link
Reference in a new issue