8000 Added create_tenant() and update_tenant() APIs by hiranya911 · Pull Request #424 · firebase/firebase-admin-python · GitHub
[go: up one dir, main page]

Skip to content

Added create_tenant() and update_tenant() APIs #424

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions firebase_admin/_auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,18 @@ def validate_int(value, label, low=None, high=None):
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.

Expand Down Expand Up @@ -192,6 +204,19 @@ def validate_action_type(action_type):
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."""
Expand Down
103 changes: 103 additions & 0 deletions firebase_admin/tenant_mgt.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,59 @@ def get_tenant(tenant_id, app=None):
return tenant_mgt_service.get_tenant(tenant_id)


def create_tenant(
display_name=None, 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 (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: 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: Display name string for the new 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``.

Expand Down Expand Up @@ -141,6 +194,56 @@ def get_tenant(self, tenant_id):
else:
return Tenant(body)

def create_tenant(
self, display_name=None, allow_password_sign_up=None, enable_email_link_sign_in=None):
"""Creates a new tenant from the given parameters."""
payload = {}
if 8000 display_name is not None:
payload['displayName'] = _auth_utils.validate_string(display_name, 'displayName')
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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think isinstance(tenant_id, str) covers not tenant_id cases, right?
Also, did we choose not to use _auth_utils.validate_string here to provide a more specific error message?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't cover the case where tenant_id == ''.

It's the same reason why we can't use validate_string() here.

raise ValueError('Tenant ID must be a non-empty string.')

payload = {}
if display_name is not None:
payload['displayName'] = _auth_utils.validate_string(display_name, 'displayName')
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:
Expand Down
176 changes: 172 additions & 4 deletions tests/test_tenant_mgt.py
Original file line number Diff line number Diff line ch 8000 ange
Expand Up @@ -14,6 +14,8 @@

"""Test cases for the firebase_admin.tenant_mgt module."""

import json

import pytest

import firebase_admin
Expand All @@ -36,6 +38,7 @@
}"""

INVALID_TENANT_IDS = [None, '', 0, 1, True, False, list(), tuple(), dict()]
INVALID_BOOLEANS = ['', 1, 0, list(), tuple(), dict()]

TENANT_MGT_URL_PREFIX = 'https://identitytoolkit.googleapis.com/v2beta1/projects/mock-project-id'

Expand Down Expand Up @@ -98,11 +101,8 @@ def test_invalid_tenant_id(self, tenant_id):
def test_get_tenant(self, tenant_mgt_app):
_, recorder = _instrument_tenant_mgt(tenant_mgt_app, 200, GET_TENANT_RESPONSE)
tenant = tenant_mgt.get_tenant('tenant-id', app=tenant_mgt_app)
assert tenant.tenant_id == 'tenant-id'
assert tenant.display_name == 'Test Tenant'
assert tenant.allow_password_sign_up is True
assert tenant.enable_email_link_sign_in is True

_assert_tenant(tenant)
assert len(recorder) == 1
req = recorder[0]
assert req.method == 'GET'
Expand All @@ -120,6 +120,167 @@ def test_tenant_not_found(self, tenant_mgt_app):
assert excinfo.value.cause is not None


class TestCreateTenant:

@pytest.mark.parametrize('display_name', [True, False, 1, 0, list(), tuple(), dict()])
def test_invalid_display_name(self, display_name, tenant_mgt_app):
with pytest.raises(ValueError) as excinfo:
tenant_mgt.create_tenant(display_name=display_name, app=tenant_mgt_app)
assert str(excinfo.value).startswith('Invalid type for displayName')

@pytest.mark.parametrize('allow', INVALID_BOOLEANS)
def test_invalid_allow_password_sign_up(self, allow, tenant_mgt_app):
with pytest.raises(ValueError) as excinfo:
tenant_mgt.create_tenant(allow_password_sign_up=allow, app=tenant_mgt_app)
assert str(excinfo.value).startswith('Invalid type for allowPasswordSignup')

@pytest.mark.parametrize('enable', INVALID_BOOLEANS)
def test_invalid_enable_email_link_sign_in(self, enable, tenant_mgt_app):
with pytest.raises(ValueError) as excinfo:
tenant_mgt.create_tenant(enable_email_link_sign_in=enable, app=tenant_mgt_app)
assert str(excinfo.value).startswith('Invalid type for enableEmailLinkSignin')

def test_create_tenant(self, tenant_mgt_app):
_, recorder = _instrument_tenant_mgt(tenant_mgt_app, 200, GET_TENANT_RESPONSE)
tenant = tenant_mgt.create_tenant(
display_name='My Tenant', allow_password_sign_up=True, enable_email_link_sign_in=True,
app=tenant_mgt_app)

_assert_tenant(tenant)
self._assert_request(recorder, {
'displayName': 'My Tenant',
'allowPasswordSignup': True,
'enableEmailLinkSignin': True,
})

def test_create_tenant_false_values(self, tenant_mgt_app):
_, recorder = _instrument_tenant_mgt(tenant_mgt_app, 200, GET_TENANT_RESPONSE)
tenant = tenant_mgt.create_tenant(
display_name='', allow_password_sign_up=False, enable_email_link_sign_in=False,
app=tenant_mgt_app)

_assert_tenant(tenant)
self._assert_request(recorder, {
'displayName': '',
'allowPasswordSignup': False,
'enableEmailLinkSignin': False,
})

def test_create_tenant_minimal(self, tenant_mgt_app):
_, recorder = _instrument_tenant_mgt(tenant_mgt_app, 200, GET_TENANT_RESPONSE)
tenant = tenant_mgt.create_tenant(app=tenant_mgt_app)

_assert_tenant(tenant)
self._assert_request(recorder, {})

def test_error(self, tenant_mgt_app):
_instrument_tenant_mgt(tenant_mgt_app, 500, '{}')
with pytest.raises(exceptions.InternalError) as excinfo:
tenant_mgt.create_tenant(app=tenant_mgt_app)

error_msg = 'Unexpected error response: {}'
assert excinfo.value.code == exceptions.INTERNAL
assert str(excinfo.value) == error_msg
assert excinfo.value.http_response is not None
assert excinfo.value.cause is not None

def _assert_request(self, recorder, body):
assert len(recorder) == 1
req = recorder[0]
assert req.method == 'POST'
assert req.url == '{0}/tenants'.format(TENANT_MGT_URL_PREFIX)
got = json.loads(req.body.decode())
assert got == body


class TestUpdateTenant:

@pytest.mark.parametrize('tenant_id', INVALID_TENANT_IDS)
def test_invalid_tenant_id(self, tenant_id, tenant_mgt_app):
with pytest.raises(ValueError) as excinfo:
tenant_mgt.update_tenant(tenant_id, display_name='My Tenant', app=tenant_mgt_app)
assert str(excinfo.value).startswith('Tenant ID must be a non-empty string')

@pytest.mark.parametrize('display_name', [True, False, 1, 0, list(), tuple(), dict()])
def test_invalid_display_name(self, display_name, tenant_mgt_app):
with pytest.raises(ValueError) as excinfo:
tenant_mgt.update_tenant('tenant-id', display_name=display_name, app=tenant_mgt_app)
assert str(excinfo.value).startswith('Invalid type for displayName')

@pytest.mark.parametrize('allow', INVALID_BOOLEANS)
def test_invalid_allow_password_sign_up(self, allow, tenant_mgt_app):
with pytest.raises(ValueError) as excinfo:
tenant_mgt.update_tenant('tenant-id', allow_password_sign_up=allow, app=tenant_mgt_app)
assert str(excinfo.value).startswith('Invalid type for allowPasswordSignup')

@pytest.mark.parametrize('enable', INVALID_BOOLEANS)
def test_invalid_enable_email_link_sign_in(self, enable, tenant_mgt_app):
with pytest.raises(ValueError) as excinfo:
tenant_mgt.update_tenant(
'tenant-id', enable_email_link_sign_in=enable, app=tenant_mgt_app)
assert str(excinfo.value).startswith('Invalid type for enableEmailLinkSignin')

def test_update_tenant(self, tenant_mgt_app):
_, recorder = _instrument_tenant_mgt(tenant_mgt_app, 200, GET_TENANT_RESPONSE)
tenant = tenant_mgt.update_tenant(
'tenant-id', display_name='My Tenant', allow_password_sign_up=True,
enable_email_link_sign_in=True, app=tenant_mgt_app)

_assert_tenant(tenant)
body = {
'displayName': 'My Tenant',
'allowPasswordSignup': True,
'enableEmailLinkSignin': True,
}
mask = ['allowPasswordSignup', 'displayName', 'enableEmailLinkSignin']
self._assert_request(recorder, body, mask)

def test_update_tenant_false_values(self, tenant_mgt_app):
_, recorder = _instrument_tenant_mgt(tenant_mgt_app, 200, GET_TENANT_RESPONSE)
tenant = tenant_mgt.update_tenant(
'tenant-id', display_name='', allow_password_sign_up=False,
enable_email_link_sign_in=False, app=tenant_mgt_app)

_assert_tenant(tenant)
body = {
'displayName': '',
'allowPasswordSignup': False,
'enableEmailLinkSignin': False,
}
mask = ['allowPasswordSignup', 'displayName', 'enableEmailLinkSignin']
self._assert_request(recorder, body, mask)

def test_update_tenant_minimal(self, tenant_mgt_app):
_, recorder = _instrument_tenant_mgt(tenant_mgt_app, 200, GET_TENANT_RESPONSE)
tenant = tenant_mgt.update_tenant(
'tenant-id', display_name='My Tenant', app=tenant_mgt_app)

_assert_tenant(tenant)
body = {'displayName': 'My Tenant'}
mask = ['displayName']
self._assert_request(recorder, body, mask)

def test_tenant_not_found_error(self, tenant_mgt_app):
_instrument_tenant_mgt(tenant_mgt_app, 500, TENANT_NOT_FOUND_RESPONSE)
with pytest.raises(tenant_mgt.TenantNotFoundError) as excinfo:
tenant_mgt.update_tenant('tenant', display_name='My Tenant', app=tenant_mgt_app)

error_msg = 'No tenant found for the given identifier (TENANT_NOT_FOUND).'
assert excinfo.value.code == exceptions.NOT_FOUND
assert str(excinfo.value) == error_msg
assert excinfo.value.http_response is not None
assert excinfo.value.cause is not None

def _assert_request(self, recorder, body, mask):
assert len(recorder) == 1
req = recorder[0]
assert req.method == 'PATCH'
assert req.url == '{0}/tenants/tenant-id?updateMask={1}'.format(
TENANT_MGT_URL_PREFIX, ','.join(mask))
got = json.loads(req.body.decode())
assert got == body


class TestDeleteTenant:

@pytest.mark.parametrize('tenant_id', INVALID_TENANT_IDS)
Expand All @@ -146,3 +307,10 @@ def test_tenant_not_found(self, tenant_mgt_app):
assert str(excinfo.value) == error_msg
assert excinfo.value.http_response is not None
assert excinfo.value.cause is not None


def _assert_tenant(tenant):
assert tenant.tenant_id == 'tenant-id'
assert tenant.display_name == 'Test Tenant'
assert tenant.allow_password_sign_up is True
assert tenant.enable_email_link_sign_in is True
0