Added delete option to database storage.
This commit is contained in:
parent
308604a33c
commit
963b5bc68b
1868 changed files with 192402 additions and 13278 deletions
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