From 44586d710bb4c37264d190f0e7881b653e80a15d Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 15 Mar 2023 11:55:12 -0700 Subject: [PATCH 01/21] 1. Adding Multi Factor Auth Configuration 2. Added TOTP Support for Tenant 3. Added Unit Tests for MFA Config 4. Added Unit Tests for Tenant Config with respect to MFA --- firebase_admin/multi_factor_config_mgt.py | 223 ++++++++++++++++++++++ firebase_admin/tenant_mgt.py | 38 +++- tests/test_multi_factor_config.py | 181 ++++++++++++++++++ tests/test_tenant_mgt.py | 91 ++++++++- 4 files changed, 522 insertions(+), 11 deletions(-) create mode 100644 firebase_admin/multi_factor_config_mgt.py create mode 100644 tests/test_multi_factor_config.py diff --git a/firebase_admin/multi_factor_config_mgt.py b/firebase_admin/multi_factor_config_mgt.py new file mode 100644 index 000000000..badb39e51 --- /dev/null +++ b/firebase_admin/multi_factor_config_mgt.py @@ -0,0 +1,223 @@ +# Copyright 2023 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 multifactor configuration management module. + +This module contains functions for managing various multifactor configurations at +the project and tenant level. +""" +from enum import Enum + +__all__ = [ + 'validate_keys', + 'MultiFactorServerConfig', + 'TOTPProviderConfig', + 'ProviderConfig', + 'MultiFactorConfig', +] + + +def validate_keys(keys, valid_keys, config_name): + for key in keys: + if key not in valid_keys: + raise ValueError( + '"{0}" is not a valid "{1}" parameter.'.format( + key, config_name)) + + +class MultiFactorServerConfig: + """Represents multi factor configuration response received from the server and + converts it to user format. + """ + + def __init__(self, data): + if not isinstance(data, dict): + raise ValueError( + 'Invalid data argument in MultiFactorConfig constructor: {0}'.format(data)) + self._data = data + + @property + def provider_configs(self): + data = self._data.get('providerConfigs', None) + if data is not None: + return [self.ProviderConfigServerConfig(d) for d in data] + return None + + class ProviderConfigServerConfig: + """Represents provider configuration response received from the server and converts + it to user format. + """ + + def __init__(self, data): + if not isinstance(data, dict): + raise ValueError( + 'Invalid data argument in ProviderConfig constructor: {0}'.format(data)) + self._data = data + + @property + def state(self): + return self._data.get('state', None) + + @property + def totp_provider_config(self): + data = self._data.get('totpProviderConfig', None) + if data is not None: + return self.TOTPProviderServerConfig(data) + return None + + class TOTPProviderServerConfig: + """Represents TOTP provider configuration response received from the server and converts + it to user format. + """ + + def __init__(self, data): + if not isinstance(data, dict): + raise ValueError( + 'Invalid data argument in TOTPProviderConfig constructor: {0}'.format(data)) + self._data = data + + @property + def adjacent_intervals(self): + return self._data.get('adjacentIntervals', None) + + +class TOTPProviderConfig: + """Represents a TOTP Provider Configuration to be specified for a tenant or project.""" + + def __init__(self, adjacent_intervals: int = None): + self.adjacent_intervals: int = adjacent_intervals + + def to_dict(self) -> dict: + data = {} + if self.adjacent_intervals is not None: + data['adjacentIntervals'] = self.adjacent_intervals + return data + + def validate(self): + """Validates a given totp_provider_config object. + + Raises: + ValueError: In case of an unsuccessful validation. + """ + validate_keys( + keys=vars(self).keys(), + valid_keys={'adjacent_intervals'}, + config_name='TOTPProviderConfig') + if self.adjacent_intervals is not None: + # Because bool types get converted to int here + # pylint: disable=C0123 + if type(self.adjacent_intervals) is not int: + raise ValueError( + 'totp_provider_config.adjacent_intervals must be an integer between' + ' 1 and 10 (inclusive).') + if not 1 <= self.adjacent_intervals <= 10: + raise ValueError( + 'totp_provider_config.adjacent_intervals must be an integer between' + ' 1 and 10 (inclusive).') + + def build_server_request(self): + self.validate() + return self.to_dict() + + +class ProviderConfig: + """Represents a provider configuration for tenant or project. + Currently only TOTP can be configured""" + + class State(Enum): + ENABLED = 'ENABLED' + DISABLED = 'DISABLED' + + def __init__(self, + state: State = None, + totp_provider_config: TOTPProviderConfig = None): + self.state: self.State = state + self.totp_provider_config: TOTPProviderConfig = totp_provider_config + + def to_dict(self) -> dict: + data = {} + if self.state: + data['state'] = self.state.value + if self.totp_provider_config: + data['totpProviderConfig'] = self.totp_provider_config.to_dict() + return data + + def validate(self): + """Validates a provider_config object. + + Raises: + ValueError: In case of an unsuccessful validation. + """ + validate_keys( + keys=vars(self).keys(), + valid_keys={ + 'state', + 'totp_provider_config'}, + config_name='ProviderConfig') + if self.state is None: + raise ValueError('provider_config.state must be defined.') + if not isinstance(self.state, ProviderConfig.State): + raise ValueError( + 'provider_config.state must be of type ProviderConfig.State.') + if self.totp_provider_config is None: + raise ValueError( + 'provider_config.totp_provider_config must be defined.') + if not isinstance(self.totp_provider_config, TOTPProviderConfig): + raise ValueError( + 'provider_configs.totp_provider_config must be of type TOTPProviderConfig.') + + def build_server_request(self): + self.validate() + return self.to_dict() + + +class MultiFactorConfig: + """Represents a multi factor configuration for tenant or project + """ + + def __init__(self, + provider_configs: list[ProviderConfig] = None): + self.provider_configs: list[ProviderConfig] = provider_configs + + def to_dict(self) -> dict: + data = {} + if self.provider_configs is not None: + data['providerConfigs'] = [d.to_dict() + for d in self.provider_configs] + return data + + def validate(self): + """Validates a given multi_factor_config object. + + Raises: + ValueError: In case of an unsuccessful validation. + """ + validate_keys( + keys=vars(self).keys(), + valid_keys={'provider_configs'}, + config_name='MultiFactorConfig') + if self.provider_configs is None: + raise ValueError( + 'multi_factor_config.provider_configs must be specified') + if not isinstance(self.provider_configs, list) or not self.provider_configs: + raise ValueError( + 'provider_configs must be an array of type ProviderConfigs.') + for provider_config in self.provider_configs: + if not isinstance(provider_config, ProviderConfig): + raise ValueError( + 'provider_configs must be an array of type ProviderConfigs.') + provider_config.validate() + + def build_server_request(self): + self.validate() + return self.to_dict() diff --git a/firebase_admin/tenant_mgt.py b/firebase_admin/tenant_mgt.py index 396a819fb..374cb30eb 100644 --- a/firebase_admin/tenant_mgt.py +++ b/firebase_admin/tenant_mgt.py @@ -28,6 +28,7 @@ from firebase_admin import _auth_utils from firebase_admin import _http_client from firebase_admin import _utils +from firebase_admin.multi_factor_config_mgt import MultiFactorConfig, MultiFactorServerConfig _TENANT_MGT_ATTRIBUTE = '_tenant_mgt' @@ -91,7 +92,8 @@ def get_tenant(tenant_id, app=None): def create_tenant( - display_name, allow_password_sign_up=None, enable_email_link_sign_in=None, app=None): + display_name, allow_password_sign_up=None, enable_email_link_sign_in=None, + multi_factor_config: MultiFactorConfig = None, app=None): """Creates a new tenant from the given options. Args: @@ -101,6 +103,7 @@ def create_tenant( 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. + multi_factor_config : A multi factor configuration to add to the tenant (optional). app: An App instance (optional). Returns: @@ -113,12 +116,13 @@ def create_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) + enable_email_link_sign_in=enable_email_link_sign_in, + multi_factor_config=multi_factor_config,) def update_tenant( tenant_id, display_name=None, allow_password_sign_up=None, enable_email_link_sign_in=None, - app=None): + multi_factor_config: MultiFactorConfig = None, app=None): """Updates an existing tenant with the given options. Args: @@ -128,6 +132,7 @@ def update_tenant( 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. + multi_factor_config : A multi factor configuration to update for the tenant (optional). app: An App instance (optional). Returns: @@ -141,7 +146,8 @@ def update_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) + enable_email_link_sign_in=enable_email_link_sign_in, + multi_factor_config=multi_factor_config) def delete_tenant(tenant_id, app=None): @@ -228,6 +234,13 @@ def allow_password_sign_up(self): def enable_email_link_sign_in(self): return self._data.get('enableEmailLinkSignin', False) + @property + def multi_factor_config(self): + data = self._data.get('mfaConfig', None) + if data is not None: + return MultiFactorServerConfig(data) + return None + class _TenantManagementService: """Firebase tenant management service.""" @@ -272,7 +285,8 @@ def get_tenant(self, tenant_id): return Tenant(body) def create_tenant( - self, display_name, allow_password_sign_up=None, enable_email_link_sign_in=None): + self, display_name, allow_password_sign_up=None, enable_email_link_sign_in=None, + multi_factor_config: MultiFactorConfig = None): """Creates a new tenant from the given parameters.""" payload = {'displayName': _validate_display_name(display_name)} @@ -282,7 +296,11 @@ def create_tenant( if enable_email_link_sign_in is not None: payload['enableEmailLinkSignin'] = _auth_utils.validate_boolean( enable_email_link_sign_in, 'enableEmailLinkSignin') - + if multi_factor_config is not None: + if not isinstance(multi_factor_config, MultiFactorConfig): + raise ValueError( + 'multi_factor_config must be of type MultiFactorConfig.') + payload['mfaConfig'] = multi_factor_config.build_server_request() try: body = self.client.body('post', '/tenants', json=payload) except requests.exceptions.RequestException as error: @@ -292,7 +310,8 @@ def create_tenant( def update_tenant( self, tenant_id, display_name=None, allow_password_sign_up=None, - enable_email_link_sign_in=None): + enable_email_link_sign_in=None, + multi_factor_config: MultiFactorConfig = 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.') @@ -306,6 +325,11 @@ def update_tenant( if enable_email_link_sign_in is not None: payload['enableEmailLinkSignin'] = _auth_utils.validate_boolean( enable_email_link_sign_in, 'enableEmailLinkSignin') + if multi_factor_config is not None: + if not isinstance(multi_factor_config, MultiFactorConfig): + raise ValueError( + 'multi_factor_config must be of type MultiFactorConfig.') + payload['mfaConfig'] = multi_factor_config.build_server_request() if not payload: raise ValueError('At least one parameter must be specified for update.') diff --git a/tests/test_multi_factor_config.py b/tests/test_multi_factor_config.py new file mode 100644 index 000000000..a387a57dc --- /dev/null +++ b/tests/test_multi_factor_config.py @@ -0,0 +1,181 @@ +# Copyright 2023 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. +from copy import copy +import pytest +from firebase_admin.multi_factor_config_mgt import MultiFactorConfig +from firebase_admin.multi_factor_config_mgt import ProviderConfig +from firebase_admin.multi_factor_config_mgt import TOTPProviderConfig +from firebase_admin.multi_factor_config_mgt import MultiFactorServerConfig + +sample_mfa_config = MultiFactorConfig( + provider_configs=[ProviderConfig( + state=ProviderConfig.State.ENABLED, + totp_provider_config=TOTPProviderConfig( + adjacent_intervals=5 + ) + )] +) + + +class TestMultiFactorConfig: + def test_invalid_mfa_config_params(self): + test_config = copy(sample_mfa_config) + test_config.invalid_parameter = 'invalid' + with pytest.raises(ValueError) as excinfo: + test_config.build_server_request() + assert str(excinfo.value).startswith('"invalid_parameter" is not a valid' + ' "MultiFactorConfig" parameter.') + + @pytest.mark.parametrize('provider_configs', + [True, False, 1, 0, list(), tuple(), dict()]) + def test_invalid_provider_configs_type(self, provider_configs): + test_config = copy(sample_mfa_config) + test_config.provider_configs = provider_configs + with pytest.raises(ValueError) as excinfo: + test_config.build_server_request() + assert str(excinfo.value).startswith('provider_configs must be an array of type' + ' ProviderConfigs.') + + @pytest.mark.parametrize('provider_configs', + [[True], [1, 2], + [{'state': 'DISABLED', 'totpProviderConfig': {}}, "foo"]]) + def test_invalid_mfa_config_provider_config(self, provider_configs): + test_config = copy(sample_mfa_config) + test_config.provider_configs = provider_configs + with pytest.raises(ValueError) as excinfo: + test_config.build_server_request() + assert str(excinfo.value).startswith('provider_configs must be an array of type' + ' ProviderConfigs.') + + +class TestProviderConfig: + def test_invalid_provider_config_params(self): + test_config = copy(sample_mfa_config.provider_configs[0]) + test_config.invalid_parameter = 'invalid' + with pytest.raises(ValueError) as excinfo: + test_config.build_server_request() + assert str(excinfo.value).startswith('"invalid_parameter" is not a valid "ProviderConfig"' + ' parameter.') + + def test_undefined_provider_config_state(self): + test_config = copy(sample_mfa_config.provider_configs[0]) + test_config.state = None + with pytest.raises(ValueError) as excinfo: + test_config.build_server_request() + assert str(excinfo.value).startswith( + 'provider_config.state must be defined.') + + @pytest.mark.parametrize('state', + ['', 1, True, False, [], (), {}, "foo", 'ENABLED']) + def test_invalid_provider_config_state(self, state): + test_config = ProviderConfig( + state=state + ) + with pytest.raises(ValueError) as excinfo: + test_config.build_server_request() + assert str(excinfo.value).startswith('provider_config.state must be of type' + ' ProviderConfig.State.') + + @pytest.mark.parametrize('state', + [ProviderConfig.State.ENABLED, ProviderConfig.State.DISABLED]) + def test_undefined_totp_provider_config(self, state): + test_config = ProviderConfig(state=state) + with pytest.raises(ValueError) as excinfo: + test_config.build_server_request() + assert str(excinfo.value).startswith('provider_config.totp_provider_config must be' + ' defined.') + + @pytest.mark.parametrize('totp_provider_config', + [True, False, 1, 0, list(), tuple(), dict()]) + def test_invalid_totp_provider_config_type(self, totp_provider_config): + test_config = copy(sample_mfa_config.provider_configs[0]) + test_config.totp_provider_config = totp_provider_config + with pytest.raises(ValueError) as excinfo: + test_config.build_server_request() + assert str(excinfo.value).startswith('provider_configs.totp_provider_config must be of type' + ' TOTPProviderConfig.') + + +class TestTOTPProviderConfig: + + def test_invalid_totp_provider_config_params(self): + test_config = copy( + sample_mfa_config.provider_configs[0].totp_provider_config) + test_config.invalid_parameter = 'invalid' + with pytest.raises(ValueError) as excinfo: + test_config.build_server_request() + assert str(excinfo.value).startswith('"invalid_parameter" is not a valid' + ' "TOTPProviderConfig" parameter.') + + @pytest.mark.parametrize('adjacent_intervals', + ['', -1, True, False, [], (), {}, "foo", 11, 1.1]) + def test_invalid_adjacent_intervals_type(self, adjacent_intervals): + test_config = copy( + sample_mfa_config.provider_configs[0].totp_provider_config) + test_config.adjacent_intervals = adjacent_intervals + with pytest.raises(ValueError) as excinfo: + test_config.build_server_request() + assert str(excinfo.value).startswith('totp_provider_config.adjacent_intervals must be an' + ' integer between 1 and 10 (inclusive).') + + +class TestMultiFactorServerConfig: + def test_invalid_multi_factor_config_response(self): + test_config = 'invalid' + with pytest.raises(ValueError) as excinfo: + MultiFactorServerConfig(test_config) + assert str(excinfo.value).startswith('Invalid data argument in MultiFactorConfig' + ' constructor: {0}'.format(test_config)) + + def test_invalid_provider_config_response(self): + test_config = 'invalid' + with pytest.raises(ValueError) as excinfo: + MultiFactorServerConfig.ProviderConfigServerConfig(test_config) + assert str(excinfo.value).startswith('Invalid data argument in ProviderConfig' + ' constructor: {0}'.format(test_config)) + + def test_invalid_totp_provider_config_response(self): + test_config = 'invalid' + with pytest.raises(ValueError) as excinfo: + MultiFactorServerConfig.ProviderConfigServerConfig.TOTPProviderServerConfig( + test_config) + assert str(excinfo.value).startswith('Invalid data argument in TOTPProviderConfig' + ' constructor: {0}'.format(test_config)) + + def test_valid_server_response(self): + response = { + 'providerConfigs': [{ + 'state': 'ENABLED', + 'totpProviderConfig': { + 'adjacentIntervals': 5 + } + }] + } + mfa_config = MultiFactorServerConfig(response) + _assert_multi_factor_config(mfa_config) + + +def _assert_multi_factor_config(mfa_config): + assert isinstance(mfa_config, MultiFactorServerConfig) + assert len(mfa_config.provider_configs) == 1 + assert isinstance(mfa_config.provider_configs, list) + for provider_config in mfa_config.provider_configs: + assert isinstance( + provider_config, + MultiFactorServerConfig.ProviderConfigServerConfig) + assert provider_config.state == 'ENABLED' + assert isinstance(provider_config.totp_provider_config, + MultiFactorServerConfig.ProviderConfigServerConfig + .TOTPProviderServerConfig) + assert provider_config.totp_provider_config.adjacent_intervals == 5 diff --git a/tests/test_tenant_mgt.py b/tests/test_tenant_mgt.py index f92dd2a83..6732eb3de 100644 --- a/tests/test_tenant_mgt.py +++ b/tests/test_tenant_mgt.py @@ -26,6 +26,10 @@ from firebase_admin import tenant_mgt from firebase_admin import _auth_providers from firebase_admin import _user_mgt +from firebase_admin.multi_factor_config_mgt import MultiFactorConfig +from firebase_admin.multi_factor_config_mgt import MultiFactorServerConfig +from firebase_admin.multi_factor_config_mgt import ProviderConfig +from firebase_admin.multi_factor_config_mgt import TOTPProviderConfig from tests import testutils from tests import test_token_gen @@ -34,7 +38,15 @@ "name": "projects/mock-project-id/tenants/tenant-id", "displayName": "Test Tenant", "allowPasswordSignup": true, - "enableEmailLinkSignin": true + "enableEmailLinkSignin": true, + "mfaConfig": { + "providerConfigs": [{ + "state":"ENABLED", + "totpProviderConfig": { + "adjacentIntervals": 5 + } + }] + } }""" TENANT_NOT_FOUND_RESPONSE = """{ @@ -236,17 +248,45 @@ def test_invalid_enable_email_link_sign_in(self, enable, tenant_mgt_app): display_name='test', enable_email_link_sign_in=enable, app=tenant_mgt_app) assert str(excinfo.value).startswith('Invalid type for enableEmailLinkSignin') + @pytest.mark.parametrize('multi_factor_config', ['a', 1, True, {}, dict(), list(), tuple()]) + def test_invalid_multi_factor_configs(self, multi_factor_config, tenant_mgt_app): + with pytest.raises(ValueError) as excinfo: + tenant_mgt.create_tenant( + display_name='test', multi_factor_config=multi_factor_config, app=tenant_mgt_app) + assert str(excinfo.value).startswith('multi_factor_config must be of type' + ' MultiFactorConfig.') + def test_create_tenant(self, tenant_mgt_app): _, recorder = _instrument_tenant_mgt(tenant_mgt_app, 200, GET_TENANT_RESPONSE) + mfa_object = MultiFactorConfig( + provider_configs=[ + ProviderConfig( + state=ProviderConfig.State.ENABLED, + totp_provider_config=TOTPProviderConfig( + adjacent_intervals=5 + ) + ) + ] + ) tenant = tenant_mgt.create_tenant( display_name='My-Tenant', allow_password_sign_up=True, enable_email_link_sign_in=True, - app=tenant_mgt_app) + multi_factor_config=mfa_object, app=tenant_mgt_app) _assert_tenant(tenant) self._assert_request(recorder, { 'displayName': 'My-Tenant', 'allowPasswordSignup': True, 'enableEmailLinkSignin': True, + 'mfaConfig': { + 'providerConfigs': [ + { + 'state': 'ENABLED', + 'totpProviderConfig': { + 'adjacentIntervals': 5 + } + } + ] + }, }) def test_create_tenant_false_values(self, tenant_mgt_app): @@ -322,6 +362,14 @@ def test_invalid_enable_email_link_sign_in(self, enable, tenant_mgt_app): 'tenant-id', enable_email_link_sign_in=enable, app=tenant_mgt_app) assert str(excinfo.value).startswith('Invalid type for enableEmailLinkSignin') + @pytest.mark.parametrize('multi_factor_config', ['a', 1, True, {}, dict(), list(), tuple()]) + def test_invalid_multi_factor_configs(self, multi_factor_config, tenant_mgt_app): + with pytest.raises(ValueError) as excinfo: + tenant_mgt.update_tenant( + 'tenant-id', multi_factor_config=multi_factor_config, app=tenant_mgt_app) + assert str(excinfo.value).startswith('multi_factor_config must be of type' + ' MultiFactorConfig.') + def test_update_tenant_no_args(self, tenant_mgt_app): with pytest.raises(ValueError) as excinfo: tenant_mgt.update_tenant('tenant-id', app=tenant_mgt_app) @@ -329,17 +377,39 @@ def test_update_tenant_no_args(self, tenant_mgt_app): def test_update_tenant(self, tenant_mgt_app): _, recorder = _instrument_tenant_mgt(tenant_mgt_app, 200, GET_TENANT_RESPONSE) + mfa_object = MultiFactorConfig( + provider_configs=[ + ProviderConfig( + state=ProviderConfig.State.ENABLED, + totp_provider_config=TOTPProviderConfig( + adjacent_intervals=5 + ) + ) + ] + ) 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) + enable_email_link_sign_in=True, + multi_factor_config=mfa_object, app=tenant_mgt_app) _assert_tenant(tenant) body = { 'displayName': 'My-Tenant', 'allowPasswordSignup': True, 'enableEmailLinkSignin': True, + 'mfaConfig': { + 'providerConfigs': [ + { + 'state': 'ENABLED', + 'totpProviderConfig': { + 'adjacentIntervals': 5 + } + } + ] + } } - mask = ['allowPasswordSignup', 'displayName', 'enableEmailLinkSignin'] + mask = ['allowPasswordSignup', 'displayName', 'enableEmailLinkSignin', + 'mfaConfig.providerConfigs'] self._assert_request(recorder, body, mask) def test_update_tenant_false_values(self, tenant_mgt_app): @@ -995,6 +1065,17 @@ def test_custom_token_with_claims(self, tenant_aware_custom_token_app): test_token_gen.verify_custom_token( custom_token, expected_claims=claims, tenant_id='test-tenant') +def _assert_multi_factor_config(mfa_config): + assert isinstance(mfa_config, MultiFactorServerConfig) + assert len(mfa_config.provider_configs) == 1 + assert isinstance(mfa_config.provider_configs, list) + for provider_config in mfa_config.provider_configs: + assert isinstance(provider_config, MultiFactorServerConfig.ProviderConfigServerConfig) + assert provider_config.state == 'ENABLED' + assert isinstance(provider_config.totp_provider_config, + MultiFactorServerConfig.ProviderConfigServerConfig + .TOTPProviderServerConfig) + assert provider_config.totp_provider_config.adjacent_intervals == 5 def _assert_tenant(tenant, tenant_id='tenant-id'): assert isinstance(tenant, tenant_mgt.Tenant) @@ -1002,3 +1083,5 @@ def _assert_tenant(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 + if tenant.multi_factor_config is not None: + _assert_multi_factor_config(mfa_config=tenant.multi_factor_config) From b625e139aa1fbba751a3ac1de604dcbe9842685b Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 15 Mar 2023 12:07:10 -0700 Subject: [PATCH 02/21] Updating list[ProviderConfig] to List[ProviderConfig] --- firebase_admin/multi_factor_config_mgt.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/firebase_admin/multi_factor_config_mgt.py b/firebase_admin/multi_factor_config_mgt.py index badb39e51..3f3153bc6 100644 --- a/firebase_admin/multi_factor_config_mgt.py +++ b/firebase_admin/multi_factor_config_mgt.py @@ -17,6 +17,7 @@ the project and tenant level. """ from enum import Enum +from typing import List __all__ = [ 'validate_keys', @@ -186,8 +187,8 @@ class MultiFactorConfig: """ def __init__(self, - provider_configs: list[ProviderConfig] = None): - self.provider_configs: list[ProviderConfig] = provider_configs + provider_configs: List[ProviderConfig] = None): + self.provider_configs: List[ProviderConfig] = provider_configs def to_dict(self) -> dict: data = {} From c002232c65b18837881a37fd326362f1021a1731 Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 15 Mar 2023 13:08:00 -0700 Subject: [PATCH 03/21] 1. Added a project_config_mgt file 2. Unit tests for project_config_mgt 3. Formatting changes --- firebase_admin/project_config_mgt.py | 135 ++++++++++++++++++++ firebase_admin/tenant_mgt.py | 19 ++- tests/test_multi_factor_config.py | 40 +++--- tests/test_project_config_mgt.py | 180 +++++++++++++++++++++++++++ 4 files changed, 344 insertions(+), 30 deletions(-) create mode 100644 firebase_admin/project_config_mgt.py create mode 100644 tests/test_project_config_mgt.py diff --git a/firebase_admin/project_config_mgt.py b/firebase_admin/project_config_mgt.py new file mode 100644 index 000000000..df123e261 --- /dev/null +++ b/firebase_admin/project_config_mgt.py @@ -0,0 +1,135 @@ +# Copyright 2023 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 configuration management module. + +This module contains functions for managing various project operations like update and create +""" + +import requests + +import firebase_admin +from firebase_admin import _auth_utils +from firebase_admin import _http_client +from firebase_admin import _utils +from firebase_admin.multi_factor_config_mgt import MultiFactorConfig +from firebase_admin.multi_factor_config_mgt import MultiFactorServerConfig + +_PROJECT_CONFIG_MGT_ATTRIBUTE = '_project_config_mgt' + +__all__ = [ + 'ProjectConfig', + + 'get_project_config', + 'update_project_config', +] + + +def get_project_config(app=None): + """Gets the project config corresponding to the given project_id. + + Args: + app: An App instance (optional). + + Returns: + Project: A project object. + + Raises: + ValueError: If the project ID is None, empty or not a string. + ProjectNotFoundError: If no project exists by the given ID. + FirebaseError: If an error occurs while retrieving the project. + """ + project_config_mgt_service = _get_project_config_mgt_service(app) + return project_config_mgt_service.get_project_config() + +def update_project_config(multi_factor_config: MultiFactorConfig = None, app=None): + """Update the Project Config with the given options. + Args: + multi_factor_config: Updated Multi Factor Authentication configuration + (optional) + app: An App instance (optional). + Returns: + Project: An updated ProjectConfig object. + Raises: + ValueError: If any of the given arguments are invalid. + FirebaseError: If an error occurs while updating the project. + """ + project_config_mgt_service = _get_project_config_mgt_service(app) + return project_config_mgt_service.update_project_config(multi_factor_config=multi_factor_config) + + +def _get_project_config_mgt_service(app): + return _utils.get_app_service(app, _PROJECT_CONFIG_MGT_ATTRIBUTE, + _ProjectConfigManagementService) + +class ProjectConfig: + """Represents a project config in an application. + """ + + def __init__(self, data): + if not isinstance(data, dict): + raise ValueError( + 'Invalid data argument in Project constructor: {0}'.format(data)) + self._data = data + + @property + def multi_factor_config(self): + data = self._data.get('mfa') + if data: + return MultiFactorServerConfig(data) + return None + +class _ProjectConfigManagementService: + """Firebase project management service.""" + + PROJECT_CONFIG_MGT_URL = 'https://identitytoolkit.googleapis.com/v2/projects' + + def __init__(self, app): + credential = app.credential.get_credential() + version_header = 'Python/Admin/{0}'.format(firebase_admin.__version__) + base_url = '{0}/{1}/config'.format( + self.PROJECT_CONFIG_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}) + + def get_project_config(self) -> ProjectConfig: + """Gets the project config""" + try: + body = self.client.body('get', url='') + except requests.exceptions.RequestException as error: + raise _auth_utils.handle_auth_backend_error(error) + else: + return ProjectConfig(body) + + def update_project_config(self, multi_factor_config: MultiFactorConfig = None) -> ProjectConfig: + """Updates the specified project with the given parameters.""" + + payload = {} + if multi_factor_config is not None: + if not isinstance(multi_factor_config, MultiFactorConfig): + raise ValueError('multi_factor_config must be of type MultiFactorConfig.') + payload['mfa'] = multi_factor_config.build_server_request() + if not payload: + raise ValueError( + 'At least one parameter must be specified for update.') + + 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 ProjectConfig(body) diff --git a/firebase_admin/tenant_mgt.py b/firebase_admin/tenant_mgt.py index 374cb30eb..e9a6ef563 100644 --- a/firebase_admin/tenant_mgt.py +++ b/firebase_admin/tenant_mgt.py @@ -25,10 +25,10 @@ import firebase_admin from firebase_admin import auth +from firebase_admin import multi_factor_config_mgt from firebase_admin import _auth_utils from firebase_admin import _http_client from firebase_admin import _utils -from firebase_admin.multi_factor_config_mgt import MultiFactorConfig, MultiFactorServerConfig _TENANT_MGT_ATTRIBUTE = '_tenant_mgt' @@ -93,7 +93,7 @@ def get_tenant(tenant_id, app=None): def create_tenant( display_name, allow_password_sign_up=None, enable_email_link_sign_in=None, - multi_factor_config: MultiFactorConfig = None, app=None): + multi_factor_config: multi_factor_config_mgt.MultiFactorConfig = None, app=None): """Creates a new tenant from the given options. Args: @@ -122,7 +122,7 @@ def create_tenant( def update_tenant( tenant_id, display_name=None, allow_password_sign_up=None, enable_email_link_sign_in=None, - multi_factor_config: MultiFactorConfig = None, app=None): + multi_factor_config: multi_factor_config_mgt.MultiFactorConfig = None, app=None): """Updates an existing tenant with the given options. Args: @@ -238,7 +238,7 @@ def enable_email_link_sign_in(self): def multi_factor_config(self): data = self._data.get('mfaConfig', None) if data is not None: - return MultiFactorServerConfig(data) + return multi_factor_config_mgt.MultiFactorServerConfig(data) return None @@ -286,7 +286,7 @@ def get_tenant(self, tenant_id): def create_tenant( self, display_name, allow_password_sign_up=None, enable_email_link_sign_in=None, - multi_factor_config: MultiFactorConfig = None): + multi_factor_config: multi_factor_config_mgt.MultiFactorConfig = None): """Creates a new tenant from the given parameters.""" payload = {'displayName': _validate_display_name(display_name)} @@ -297,7 +297,7 @@ def create_tenant( payload['enableEmailLinkSignin'] = _auth_utils.validate_boolean( enable_email_link_sign_in, 'enableEmailLinkSignin') if multi_factor_config is not None: - if not isinstance(multi_factor_config, MultiFactorConfig): + if not isinstance(multi_factor_config, multi_factor_config_mgt.MultiFactorConfig): raise ValueError( 'multi_factor_config must be of type MultiFactorConfig.') payload['mfaConfig'] = multi_factor_config.build_server_request() @@ -311,7 +311,7 @@ def create_tenant( def update_tenant( self, tenant_id, display_name=None, allow_password_sign_up=None, enable_email_link_sign_in=None, - multi_factor_config: MultiFactorConfig = None): + multi_factor_config: multi_factor_config_mgt.MultiFactorConfig = 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.') @@ -326,9 +326,8 @@ def update_tenant( payload['enableEmailLinkSignin'] = _auth_utils.validate_boolean( enable_email_link_sign_in, 'enableEmailLinkSignin') if multi_factor_config is not None: - if not isinstance(multi_factor_config, MultiFactorConfig): - raise ValueError( - 'multi_factor_config must be of type MultiFactorConfig.') + if not isinstance(multi_factor_config, multi_factor_config_mgt.MultiFactorConfig): + raise ValueError('multi_factor_config must be of type MultiFactorConfig.') payload['mfaConfig'] = multi_factor_config.build_server_request() if not payload: diff --git a/tests/test_multi_factor_config.py b/tests/test_multi_factor_config.py index a387a57dc..7e4190ba8 100644 --- a/tests/test_multi_factor_config.py +++ b/tests/test_multi_factor_config.py @@ -12,16 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. from copy import copy + import pytest -from firebase_admin.multi_factor_config_mgt import MultiFactorConfig -from firebase_admin.multi_factor_config_mgt import ProviderConfig -from firebase_admin.multi_factor_config_mgt import TOTPProviderConfig -from firebase_admin.multi_factor_config_mgt import MultiFactorServerConfig - -sample_mfa_config = MultiFactorConfig( - provider_configs=[ProviderConfig( - state=ProviderConfig.State.ENABLED, - totp_provider_config=TOTPProviderConfig( + +from firebase_admin import multi_factor_config_mgt + +sample_mfa_config = multi_factor_config_mgt.MultiFactorConfig( + provider_configs=[multi_factor_config_mgt.ProviderConfig( + state=multi_factor_config_mgt.ProviderConfig.State.ENABLED, + totp_provider_config=multi_factor_config_mgt.TOTPProviderConfig( adjacent_intervals=5 ) )] @@ -79,7 +78,7 @@ def test_undefined_provider_config_state(self): @pytest.mark.parametrize('state', ['', 1, True, False, [], (), {}, "foo", 'ENABLED']) def test_invalid_provider_config_state(self, state): - test_config = ProviderConfig( + test_config = multi_factor_config_mgt.ProviderConfig( state=state ) with pytest.raises(ValueError) as excinfo: @@ -88,9 +87,10 @@ def test_invalid_provider_config_state(self, state): ' ProviderConfig.State.') @pytest.mark.parametrize('state', - [ProviderConfig.State.ENABLED, ProviderConfig.State.DISABLED]) + [multi_factor_config_mgt.ProviderConfig.State.ENABLED, + multi_factor_config_mgt.ProviderConfig.State.DISABLED]) def test_undefined_totp_provider_config(self, state): - test_config = ProviderConfig(state=state) + test_config = multi_factor_config_mgt.ProviderConfig(state=state) with pytest.raises(ValueError) as excinfo: test_config.build_server_request() assert str(excinfo.value).startswith('provider_config.totp_provider_config must be' @@ -134,22 +134,22 @@ class TestMultiFactorServerConfig: def test_invalid_multi_factor_config_response(self): test_config = 'invalid' with pytest.raises(ValueError) as excinfo: - MultiFactorServerConfig(test_config) + multi_factor_config_mgt.MultiFactorServerConfig(test_config) assert str(excinfo.value).startswith('Invalid data argument in MultiFactorConfig' ' constructor: {0}'.format(test_config)) def test_invalid_provider_config_response(self): test_config = 'invalid' with pytest.raises(ValueError) as excinfo: - MultiFactorServerConfig.ProviderConfigServerConfig(test_config) + multi_factor_config_mgt.MultiFactorServerConfig.ProviderConfigServerConfig(test_config) assert str(excinfo.value).startswith('Invalid data argument in ProviderConfig' ' constructor: {0}'.format(test_config)) def test_invalid_totp_provider_config_response(self): test_config = 'invalid' with pytest.raises(ValueError) as excinfo: - MultiFactorServerConfig.ProviderConfigServerConfig.TOTPProviderServerConfig( - test_config) + multi_factor_config_mgt.MultiFactorServerConfig.ProviderConfigServerConfig.\ + TOTPProviderServerConfig(test_config) assert str(excinfo.value).startswith('Invalid data argument in TOTPProviderConfig' ' constructor: {0}'.format(test_config)) @@ -162,20 +162,20 @@ def test_valid_server_response(self): } }] } - mfa_config = MultiFactorServerConfig(response) + mfa_config = multi_factor_config_mgt.MultiFactorServerConfig(response) _assert_multi_factor_config(mfa_config) def _assert_multi_factor_config(mfa_config): - assert isinstance(mfa_config, MultiFactorServerConfig) + assert isinstance(mfa_config, multi_factor_config_mgt.MultiFactorServerConfig) assert len(mfa_config.provider_configs) == 1 assert isinstance(mfa_config.provider_configs, list) for provider_config in mfa_config.provider_configs: assert isinstance( provider_config, - MultiFactorServerConfig.ProviderConfigServerConfig) + multi_factor_config_mgt.MultiFactorServerConfig.ProviderConfigServerConfig) assert provider_config.state == 'ENABLED' assert isinstance(provider_config.totp_provider_config, - MultiFactorServerConfig.ProviderConfigServerConfig + multi_factor_config_mgt.MultiFactorServerConfig.ProviderConfigServerConfig .TOTPProviderServerConfig) assert provider_config.totp_provider_config.adjacent_intervals == 5 diff --git a/tests/test_project_config_mgt.py b/tests/test_project_config_mgt.py new file mode 100644 index 000000000..6cba66483 --- /dev/null +++ b/tests/test_project_config_mgt.py @@ -0,0 +1,180 @@ +# Copyright 2023 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. + +"""Test cases for the firebase_admin.project_config_mgt module.""" + +import json + +import pytest + +from tests import testutils + +import firebase_admin +from firebase_admin import project_config_mgt +from firebase_admin import multi_factor_config_mgt + + +GET_PROJECT_RESPONSE = """{ + "mfaConfig":{ + "providerConfigs":[ + { + "state":"ENABLED", + "totpProviderConfig": { + "adjacentIntervals": 5 + } + } + ] + } +}""" + +MOCK_GET_USER_RESPONSE = testutils.resource('get_user.json') +INVALID_BOOLEANS = ['', 1, 0, list(), tuple(), dict()] + +PROJECT_CONFIG_MGT_URL_PREFIX = 'https://identitytoolkit.googleapis.com/v2/projects' + + +@pytest.fixture(scope='module') +def project_config_mgt_app(): + app = firebase_admin.initialize_app( + testutils.MockCredential(), name='projectMgt', options={'projectId': 'project-id'}) + yield app + firebase_admin.delete_app(app) + + +def _instrument_project_config_mgt(app, status, payload): + service = project_config_mgt._get_project_config_mgt_service(app) + recorder = [] + service.client.session.mount( + project_config_mgt._ProjectConfigManagementService.PROJECT_CONFIG_MGT_URL, + testutils.MockAdapter(payload, status, recorder)) + return service, recorder + + +class TestProjectConfig: + + @pytest.mark.parametrize('data', [None, 'foo', 0, 1, True, False, list(), tuple()]) + def test_invalid_data(self, data): + with pytest.raises(ValueError): + project_config_mgt.ProjectConfig(data) + + def test_project_config(self): + data = { + 'mfa': { + 'providerConfigs': [ + { + 'state': 'ENABLED', + 'totpProviderConfig': { + 'adjacentIntervals': 5, + } + } + ] + } + } + project_config = project_config_mgt.ProjectConfig(data) + _assert_project_config(project_config) + + def test_project_optional_params(self): + data = { + 'name': 'test-project', + } + project = project_config_mgt.ProjectConfig(data) + assert project.multi_factor_config is None + + +class TestGetProjectConfig: + + def test_get_project_config(self, project_config_mgt_app): + _, recorder = _instrument_project_config_mgt( + project_config_mgt_app, 200, GET_PROJECT_RESPONSE) + project_config = project_config_mgt.get_project_config(app=project_config_mgt_app) + + _assert_project_config(project_config) + assert len(recorder) == 1 + req = recorder[0] + assert req.method == 'GET' + assert req.url == '{0}/project-id/config'.format(PROJECT_CONFIG_MGT_URL_PREFIX) + + +class TestUpdateProjectConfig: + + def test_update_project_no_args(self, project_config_mgt_app): + with pytest.raises(ValueError) as excinfo: + project_config_mgt.update_project_config(app=project_config_mgt_app) + assert str(excinfo.value).startswith('At least one parameter must be specified for update') + + @pytest.mark.parametrize('multi_factor_config', ['foo', 0, 1, True, False, list(), tuple()]) + def test_invalid_multi_factor_config_type(self, multi_factor_config, project_config_mgt_app): + with pytest.raises(ValueError) as excinfo: + project_config_mgt.update_project_config(multi_factor_config=multi_factor_config, + app=project_config_mgt_app) + assert str(excinfo.value).startswith( + 'multi_factor_config must be of type MultiFactorConfig.') + + def test_update_project_config(self, project_config_mgt_app): + _, recorder = _instrument_project_config_mgt( + project_config_mgt_app, 200, GET_PROJECT_RESPONSE) + mfa_object = multi_factor_config_mgt.MultiFactorConfig( + provider_configs=[ + multi_factor_config_mgt.ProviderConfig( + state=multi_factor_config_mgt.ProviderConfig.State.ENABLED, + totp_provider_config=multi_factor_config_mgt.TOTPProviderConfig( + adjacent_intervals=5 + ) + ) + ] + ) + project_config = project_config_mgt.update_project_config( + multi_factor_config=mfa_object, app=project_config_mgt_app) + + mask = ['mfa.providerConfigs'] + + _assert_project_config(project_config) + self._assert_request(recorder, { + 'mfa': { + 'providerConfigs': [ + { + 'state': 'ENABLED', + 'totpProviderConfig': { + 'adjacentIntervals': 5, + } + } + ] + } + }, mask) + + def _assert_request(self, recorder, body, mask): + assert len(recorder) == 1 + req = recorder[0] + assert req.method == 'PATCH' + assert req.url == '{0}/project-id/config?updateMask={1}'.format( + PROJECT_CONFIG_MGT_URL_PREFIX, ','.join(mask)) + got = json.loads(req.body.decode()) + assert got == body + +def _assert_multi_factor_config(multi_factor_config): + assert isinstance(multi_factor_config, multi_factor_config_mgt.MultiFactorServerConfig) + assert len(multi_factor_config.provider_configs) == 1 + assert isinstance(multi_factor_config.provider_configs, list) + for provider_config in multi_factor_config.provider_configs: + assert isinstance(provider_config, multi_factor_config_mgt.MultiFactorServerConfig + .ProviderConfigServerConfig) + assert provider_config.state == 'ENABLED' + assert isinstance(provider_config.totp_provider_config, + multi_factor_config_mgt.MultiFactorServerConfig.ProviderConfigServerConfig + .TOTPProviderServerConfig) + assert provider_config.totp_provider_config.adjacent_intervals == 5 + +def _assert_project_config(project_config): + if project_config.multi_factor_config is not None: + _assert_multi_factor_config(project_config.multi_factor_config) From 2a73f415ef3a0b6a5cfe0ba6607b67d1304ba5ee Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 15 Mar 2023 13:37:08 -0700 Subject: [PATCH 04/21] Import changes --- tests/test_tenant_mgt.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/tests/test_tenant_mgt.py b/tests/test_tenant_mgt.py index 6732eb3de..bac9fe33d 100644 --- a/tests/test_tenant_mgt.py +++ b/tests/test_tenant_mgt.py @@ -26,10 +26,7 @@ from firebase_admin import tenant_mgt from firebase_admin import _auth_providers from firebase_admin import _user_mgt -from firebase_admin.multi_factor_config_mgt import MultiFactorConfig -from firebase_admin.multi_factor_config_mgt import MultiFactorServerConfig -from firebase_admin.multi_factor_config_mgt import ProviderConfig -from firebase_admin.multi_factor_config_mgt import TOTPProviderConfig +from firebase_admin import multi_factor_config_mgt from tests import testutils from tests import test_token_gen @@ -258,11 +255,11 @@ def test_invalid_multi_factor_configs(self, multi_factor_config, tenant_mgt_app) def test_create_tenant(self, tenant_mgt_app): _, recorder = _instrument_tenant_mgt(tenant_mgt_app, 200, GET_TENANT_RESPONSE) - mfa_object = MultiFactorConfig( + mfa_object = multi_factor_config_mgt.MultiFactorConfig( provider_configs=[ - ProviderConfig( - state=ProviderConfig.State.ENABLED, - totp_provider_config=TOTPProviderConfig( + multi_factor_config_mgt.ProviderConfig( + state=multi_factor_config_mgt.ProviderConfig.State.ENABLED, + totp_provider_config=multi_factor_config_mgt.TOTPProviderConfig( adjacent_intervals=5 ) ) @@ -377,11 +374,11 @@ def test_update_tenant_no_args(self, tenant_mgt_app): def test_update_tenant(self, tenant_mgt_app): _, recorder = _instrument_tenant_mgt(tenant_mgt_app, 200, GET_TENANT_RESPONSE) - mfa_object = MultiFactorConfig( + mfa_object = multi_factor_config_mgt.MultiFactorConfig( provider_configs=[ - ProviderConfig( - state=ProviderConfig.State.ENABLED, - totp_provider_config=TOTPProviderConfig( + multi_factor_config_mgt.ProviderConfig( + state=multi_factor_config_mgt.ProviderConfig.State.ENABLED, + totp_provider_config=multi_factor_config_mgt.TOTPProviderConfig( adjacent_intervals=5 ) ) @@ -1066,14 +1063,15 @@ def test_custom_token_with_claims(self, tenant_aware_custom_token_app): custom_token, expected_claims=claims, tenant_id='test-tenant') def _assert_multi_factor_config(mfa_config): - assert isinstance(mfa_config, MultiFactorServerConfig) + assert isinstance(mfa_config, multi_factor_config_mgt.MultiFactorServerConfig) assert len(mfa_config.provider_configs) == 1 assert isinstance(mfa_config.provider_configs, list) for provider_config in mfa_config.provider_configs: - assert isinstance(provider_config, MultiFactorServerConfig.ProviderConfigServerConfig) + assert isinstance(provider_config, multi_factor_config_mgt.MultiFactorServerConfig.\ + ProviderConfigServerConfig) assert provider_config.state == 'ENABLED' assert isinstance(provider_config.totp_provider_config, - MultiFactorServerConfig.ProviderConfigServerConfig + multi_factor_config_mgt.MultiFactorServerConfig.ProviderConfigServerConfig .TOTPProviderServerConfig) assert provider_config.totp_provider_config.adjacent_intervals == 5 From b5e31b14ea5cc82888bbb34403ba9804e4298915 Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 15 Mar 2023 15:15:03 -0700 Subject: [PATCH 05/21] 1. Changing auth url from `v2beta1` to `v2` 2. Integration tests for project and tenant wrt MFA config --- firebase_admin/_auth_client.py | 4 +- firebase_admin/_auth_providers.py | 2 +- firebase_admin/tenant_mgt.py | 2 +- integration/test_project_config_mgt.py | 69 ++++++++++++++++++++++++++ integration/test_tenant_mgt.py | 37 +++++++++++++- 5 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 integration/test_project_config_mgt.py diff --git a/firebase_admin/_auth_client.py b/firebase_admin/_auth_client.py index eaf491f32..52f8bdfc8 100644 --- a/firebase_admin/_auth_client.py +++ b/firebase_admin/_auth_client.py @@ -50,7 +50,7 @@ def __init__(self, app, tenant_id=None): if emulator_host: base_url = 'http://{0}/identitytoolkit.googleapis.com'.format(emulator_host) endpoint_urls['v1'] = base_url + '/v1' - endpoint_urls['v2beta1'] = base_url + '/v2beta1' + endpoint_urls['v2beta1'] = base_url + '/v2' credential = _utils.EmulatorAdminCredentials() self.emulated = True else: @@ -67,7 +67,7 @@ def __init__(self, app, tenant_id=None): self._user_manager = _user_mgt.UserManager( http_client, app.project_id, tenant_id, url_override=endpoint_urls.get('v1')) self._provider_manager = _auth_providers.ProviderConfigClient( - http_client, app.project_id, tenant_id, url_override=endpoint_urls.get('v2beta1')) + http_client, app.project_id, tenant_id, url_override=endpoint_urls.get('v2')) @property def tenant_id(self): diff --git a/firebase_admin/_auth_providers.py b/firebase_admin/_auth_providers.py index 31511f3c5..31894a4dc 100644 --- a/firebase_admin/_auth_providers.py +++ b/firebase_admin/_auth_providers.py @@ -176,7 +176,7 @@ def items(self): class ProviderConfigClient: """Client for managing Auth provider configurations.""" - PROVIDER_CONFIG_URL = 'https://identitytoolkit.googleapis.com/v2beta1' + PROVIDER_CONFIG_URL = 'https://identitytoolkit.googleapis.com/v2' def __init__(self, http_client, project_id, tenant_id=None, url_override=None): self.http_client = http_client diff --git a/firebase_admin/tenant_mgt.py b/firebase_admin/tenant_mgt.py index e9a6ef563..af222ff02 100644 --- a/firebase_admin/tenant_mgt.py +++ b/firebase_admin/tenant_mgt.py @@ -245,7 +245,7 @@ def multi_factor_config(self): class _TenantManagementService: """Firebase tenant management service.""" - TENANT_MGT_URL = 'https://identitytoolkit.googleapis.com/v2beta1' + TENANT_MGT_URL = 'https://identitytoolkit.googleapis.com/v2' def __init__(self, app): credential = app.credential.get_credential() diff --git a/integration/test_project_config_mgt.py b/integration/test_project_config_mgt.py new file mode 100644 index 000000000..e61a4c7f9 --- /dev/null +++ b/integration/test_project_config_mgt.py @@ -0,0 +1,69 @@ +# Copyright 2023 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. + +"""Integration tests for firebase_admin.project_config_mgt module.""" + +import pytest + +from firebase_admin import project_config_mgt +from firebase_admin import multi_factor_config_mgt + + +@pytest.fixture(scope='module') +def sample_mfa_config(): + mfa_config = { + 'providerConfigs': [ + { + 'state': 'ENABLED', + 'totpProviderConfig': { + 'adjacentIntervals': 5 + } + } + ] + } + return mfa_config + + +def test_update_project_config(): + mfa_object = multi_factor_config_mgt.MultiFactorConfig( + provider_configs=[ + multi_factor_config_mgt.ProviderConfig( + state=multi_factor_config_mgt.ProviderConfig.State.ENABLED, + totp_provider_config=multi_factor_config_mgt.TOTPProviderConfig( + adjacent_intervals=5 + ) + ) + ] + ) + project_config = project_config_mgt.update_project_config(multi_factor_config=mfa_object) + _assert_multi_factor_config(project_config.multi_factor_config) + + +def test_get_project(): + project_config = project_config_mgt.get_project_config() + assert isinstance(project_config, project_config_mgt.ProjectConfig) + _assert_multi_factor_config(project_config.multi_factor_config) + +def _assert_multi_factor_config(multi_factor_config): + assert isinstance(multi_factor_config, multi_factor_config_mgt.MultiFactorServerConfig) + assert len(multi_factor_config.provider_configs) == 1 + assert isinstance(multi_factor_config.provider_configs, list) + for provider_config in multi_factor_config.provider_configs: + assert isinstance(provider_config, multi_factor_config_mgt.MultiFactorServerConfig + .ProviderConfigServerConfig) + assert provider_config.state == 'ENABLED' + assert isinstance(provider_config.totp_provider_config, + multi_factor_config_mgt.MultiFactorServerConfig.ProviderConfigServerConfig + .TOTPProviderServerConfig) + assert provider_config.totp_provider_config.adjacent_intervals == 5 \ No newline at end of file diff --git a/integration/test_tenant_mgt.py b/integration/test_tenant_mgt.py index c9eefd96e..96578d1b4 100644 --- a/integration/test_tenant_mgt.py +++ b/integration/test_tenant_mgt.py @@ -25,6 +25,7 @@ from firebase_admin import auth from firebase_admin import tenant_mgt +from firebase_admin import multi_factor_config_mgt from integration import test_auth @@ -35,13 +36,34 @@ @pytest.fixture(scope='module') def sample_tenant(): + mfa_object = multi_factor_config_mgt.MultiFactorConfig( + provider_configs=[multi_factor_config_mgt.ProviderConfig( + state=multi_factor_config_mgt.ProviderConfig.State.ENABLED, + totp_provider_config=multi_factor_config_mgt.TOTPProviderConfig( + adjacent_intervals=5 + ) + )] + ) tenant = tenant_mgt.create_tenant( display_name='admin-python-tenant', allow_password_sign_up=True, - enable_email_link_sign_in=True) + enable_email_link_sign_in=True, + multi_factor_config=mfa_object) yield tenant tenant_mgt.delete_tenant(tenant.tenant_id) +def _assert_multi_factor_config(mfa_config): + assert isinstance(mfa_config, multi_factor_config_mgt.MultiFactorServerConfig) + assert len(mfa_config.provider_configs) == 1 + assert isinstance(mfa_config.provider_configs, list) + for provider_config in mfa_config.provider_configs: + assert isinstance(provider_config, multi_factor_config_mgt.MultiFactorServerConfig.\ + ProviderConfigServerConfig) + assert provider_config.state == 'ENABLED' + assert isinstance(provider_config.totp_provider_config, + multi_factor_config_mgt.MultiFactorServerConfig.ProviderConfigServerConfig + .TOTPProviderServerConfig) + assert provider_config.totp_provider_config.adjacent_intervals == 5 @pytest.fixture(scope='module') def tenant_user(sample_tenant): @@ -59,6 +81,7 @@ def test_get_tenant(sample_tenant): assert tenant.display_name == 'admin-python-tenant' assert tenant.allow_password_sign_up is True assert tenant.enable_email_link_sign_in is True + _assert_multi_factor_config(tenant.multi_factor_config) def test_list_tenants(sample_tenant): @@ -76,8 +99,17 @@ def test_list_tenants(sample_tenant): def test_update_tenant(): + mfa_object = multi_factor_config_mgt.MultiFactorConfig( + provider_configs=[multi_factor_config_mgt.ProviderConfig( + state=multi_factor_config_mgt.ProviderConfig.State.ENABLED, + totp_provider_config=multi_factor_config_mgt.TOTPProviderConfig( + adjacent_intervals=5 + ) + )] + ) tenant = tenant_mgt.create_tenant( - display_name='py-update-test', allow_password_sign_up=True, enable_email_link_sign_in=True) + display_name='py-update-test', allow_password_sign_up=True, enable_email_link_sign_in=True, + multi_factor_config=mfa_object) try: tenant = tenant_mgt.update_tenant( tenant.tenant_id, display_name='updated-py-tenant', allow_password_sign_up=False, @@ -87,6 +119,7 @@ def test_update_tenant(): assert tenant.display_name == 'updated-py-tenant' assert tenant.allow_password_sign_up is False assert tenant.enable_email_link_sign_in is False + _assert_multi_factor_config(tenant.multi_factor_config) finally: tenant_mgt.delete_tenant(tenant.tenant_id) From 7ff00893a5fb70df0e7f1e2e02a141c38861fe18 Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 15 Mar 2023 15:32:15 -0700 Subject: [PATCH 06/21] Add new line (lint fix) --- integration/test_project_config_mgt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/test_project_config_mgt.py b/integration/test_project_config_mgt.py index e61a4c7f9..e7c79a07d 100644 --- a/integration/test_project_config_mgt.py +++ b/integration/test_project_config_mgt.py @@ -66,4 +66,4 @@ def _assert_multi_factor_config(multi_factor_config): assert isinstance(provider_config.totp_provider_config, multi_factor_config_mgt.MultiFactorServerConfig.ProviderConfigServerConfig .TOTPProviderServerConfig) - assert provider_config.totp_provider_config.adjacent_intervals == 5 \ No newline at end of file + assert provider_config.totp_provider_config.adjacent_intervals == 5 From 81b1fa3ee34156deeafb850521816cbae7831e87 Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 15 Mar 2023 15:37:23 -0700 Subject: [PATCH 07/21] `v2beta1` -> `v2` --- firebase_admin/_auth_client.py | 2 +- tests/test_auth_providers.py | 4 ++-- tests/test_tenant_mgt.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/firebase_admin/_auth_client.py b/firebase_admin/_auth_client.py index 52f8bdfc8..0fc9d2bee 100644 --- a/firebase_admin/_auth_client.py +++ b/firebase_admin/_auth_client.py @@ -50,7 +50,7 @@ def __init__(self, app, tenant_id=None): if emulator_host: base_url = 'http://{0}/identitytoolkit.googleapis.com'.format(emulator_host) endpoint_urls['v1'] = base_url + '/v1' - endpoint_urls['v2beta1'] = base_url + '/v2' + endpoint_urls['v2'] = base_url + '/v2' credential = _utils.EmulatorAdminCredentials() self.emulated = True else: diff --git a/tests/test_auth_providers.py b/tests/test_auth_providers.py index b67a8eb96..f3601e16a 100644 --- a/tests/test_auth_providers.py +++ b/tests/test_auth_providers.py @@ -23,10 +23,10 @@ from firebase_admin import exceptions from tests import testutils -ID_TOOLKIT_URL = 'https://identitytoolkit.googleapis.com/v2beta1' +ID_TOOLKIT_URL = 'https://identitytoolkit.googleapis.com/v2b' EMULATOR_HOST_ENV_VAR = 'FIREBASE_AUTH_EMULATOR_HOST' AUTH_EMULATOR_HOST = 'localhost:9099' -EMULATED_ID_TOOLKIT_URL = 'http://{}/identitytoolkit.googleapis.com/v2beta1'.format( +EMULATED_ID_TOOLKIT_URL = 'http://{}/identitytoolkit.googleapis.com/v2b'.format( AUTH_EMULATOR_HOST) URL_PROJECT_SUFFIX = '/projects/mock-project-id' USER_MGT_URLS = { diff --git a/tests/test_tenant_mgt.py b/tests/test_tenant_mgt.py index bac9fe33d..e964c540d 100644 --- a/tests/test_tenant_mgt.py +++ b/tests/test_tenant_mgt.py @@ -117,8 +117,8 @@ INVALID_BOOLEANS = ['', 1, 0, list(), tuple(), dict()] USER_MGT_URL_PREFIX = 'https://identitytoolkit.googleapis.com/v1/projects/mock-project-id' -PROVIDER_MGT_URL_PREFIX = 'https://identitytoolkit.googleapis.com/v2beta1/projects/mock-project-id' -TENANT_MGT_URL_PREFIX = 'https://identitytoolkit.googleapis.com/v2beta1/projects/mock-project-id' +PROVIDER_MGT_URL_PREFIX = 'https://identitytoolkit.googleapis.com/v2/projects/mock-project-id' +TENANT_MGT_URL_PREFIX = 'https://identitytoolkit.googleapis.com/v2/projects/mock-project-id' @pytest.fixture(scope='module') From 1f5c3665687f2e5e889d6e585a57c20e25ec056e Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 15 Mar 2023 15:38:38 -0700 Subject: [PATCH 08/21] corrections --- tests/test_auth_providers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_auth_providers.py b/tests/test_auth_providers.py index f3601e16a..a5716266c 100644 --- a/tests/test_auth_providers.py +++ b/tests/test_auth_providers.py @@ -23,10 +23,10 @@ from firebase_admin import exceptions from tests import testutils -ID_TOOLKIT_URL = 'https://identitytoolkit.googleapis.com/v2b' +ID_TOOLKIT_URL = 'https://identitytoolkit.googleapis.com/v2' EMULATOR_HOST_ENV_VAR = 'FIREBASE_AUTH_EMULATOR_HOST' AUTH_EMULATOR_HOST = 'localhost:9099' -EMULATED_ID_TOOLKIT_URL = 'http://{}/identitytoolkit.googleapis.com/v2b'.format( +EMULATED_ID_TOOLKIT_URL = 'http://{}/identitytoolkit.googleapis.com/v2'.format( AUTH_EMULATOR_HOST) URL_PROJECT_SUFFIX = '/projects/mock-project-id' USER_MGT_URLS = { From 7e53e611c1d5a7af203e0896d3851d0f9777b637 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 5 Apr 2023 19:55:51 -0400 Subject: [PATCH 09/21] chore: Fix pypy tests (#694) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2129720b..6612efe55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['3.7', '3.8', '3.9', '3.10', 'pypy3.7'] + python: ['3.7', '3.8', '3.9', '3.10', 'pypy3.8'] steps: - uses: actions/checkout@v3 From 6a4f45139639beb145f9c231f319507e0878e3e1 Mon Sep 17 00:00:00 2001 From: pragatimodi <110490169+pragatimodi@users.noreply.github.com> Date: Wed, 5 Apr 2023 17:00:16 -0700 Subject: [PATCH 10/21] chore(auth): Update Auth API to `v2` (#691) * `v2beta1` -> `v2` * Reverting auto formatting changes * undo auto formatting From 06849158db41fd53237ac985eff7f40c40c5c2a6 Mon Sep 17 00:00:00 2001 From: Samuel Dion-Girardeau Date: Wed, 5 Apr 2023 20:08:45 -0400 Subject: [PATCH 11/21] Add release notes to project URLs in PyPI (#679) It's useful to be able to navigate to the release notes easily from the package index when upgrading. "Release Notes" is a special keyword that will have the scroll icon in the project page. A random example: * https://pypi.org/project/streamlit/ * https://github.com/streamlit/streamlit/blob/815a3ea6fa3e7f9099b479e8365bd3a5874ddc35/lib/setup.py#L111 Co-authored-by: Lahiru Maramba --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 1ba2ffa92..a82bc47f3 100644 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ long_description=long_description, url=about['__url__'], project_urls={ + 'Release Notes': 'https://firebase.google.com/support/release-notes/admin/python', 'Source': 'https://github.com/firebase/firebase-admin-python', }, author=about['__author__'], From 8b87e102f5829b95d883f768b111dc12420027b6 Mon Sep 17 00:00:00 2001 From: Pragati Date: Mon, 12 Jun 2023 13:57:33 -0700 Subject: [PATCH 12/21] Addressing feedback Importing configs directly fix warnings fix warnings fix warnings --- firebase_admin/multi_factor_config_mgt.py | 11 ++++---- firebase_admin/project_config_mgt.py | 1 - firebase_admin/tenant_mgt.py | 32 ++++++++++++++--------- integration/test_project_config_mgt.py | 9 ++++--- integration/test_tenant_mgt.py | 4 +-- tests/test_multi_factor_config.py | 14 +++++----- tests/test_project_config_mgt.py | 14 +++++----- tests/test_tenant_mgt.py | 16 +++++++----- 8 files changed, 56 insertions(+), 45 deletions(-) diff --git a/firebase_admin/multi_factor_config_mgt.py b/firebase_admin/multi_factor_config_mgt.py index 3f3153bc6..61d4a134d 100644 --- a/firebase_admin/multi_factor_config_mgt.py +++ b/firebase_admin/multi_factor_config_mgt.py @@ -44,17 +44,17 @@ class MultiFactorServerConfig: def __init__(self, data): if not isinstance(data, dict): raise ValueError( - 'Invalid data argument in MultiFactorConfig constructor: {0}'.format(data)) + 'Invalid data argument in MultiFactorServerConfig constructor: {0}'.format(data)) self._data = data @property def provider_configs(self): data = self._data.get('providerConfigs', None) if data is not None: - return [self.ProviderConfigServerConfig(d) for d in data] + return [self.ProviderServerConfig(d) for d in data] return None - class ProviderConfigServerConfig: + class ProviderServerConfig: """Represents provider configuration response received from the server and converts it to user format. """ @@ -62,7 +62,7 @@ class ProviderConfigServerConfig: def __init__(self, data): if not isinstance(data, dict): raise ValueError( - 'Invalid data argument in ProviderConfig constructor: {0}'.format(data)) + 'Invalid data argument in ProviderServerConfig constructor: {0}'.format(data)) self._data = data @property @@ -84,7 +84,8 @@ class TOTPProviderServerConfig: def __init__(self, data): if not isinstance(data, dict): raise ValueError( - 'Invalid data argument in TOTPProviderConfig constructor: {0}'.format(data)) + 'Invalid data argument in TOTPProviderServerConfig' + ' constructor: {0}'.format(data)) self._data = data @property diff --git a/firebase_admin/project_config_mgt.py b/firebase_admin/project_config_mgt.py index df123e261..851a3499c 100644 --- a/firebase_admin/project_config_mgt.py +++ b/firebase_admin/project_config_mgt.py @@ -29,7 +29,6 @@ __all__ = [ 'ProjectConfig', - 'get_project_config', 'update_project_config', ] diff --git a/firebase_admin/tenant_mgt.py b/firebase_admin/tenant_mgt.py index af222ff02..4f943b3e3 100644 --- a/firebase_admin/tenant_mgt.py +++ b/firebase_admin/tenant_mgt.py @@ -25,10 +25,11 @@ import firebase_admin from firebase_admin import auth -from firebase_admin import multi_factor_config_mgt from firebase_admin import _auth_utils from firebase_admin import _http_client from firebase_admin import _utils +from firebase_admin.multi_factor_config_mgt import MultiFactorConfig +from firebase_admin.multi_factor_config_mgt import MultiFactorServerConfig _TENANT_MGT_ATTRIBUTE = '_tenant_mgt' @@ -93,7 +94,7 @@ def get_tenant(tenant_id, app=None): def create_tenant( display_name, allow_password_sign_up=None, enable_email_link_sign_in=None, - multi_factor_config: multi_factor_config_mgt.MultiFactorConfig = None, app=None): + multi_factor_config: MultiFactorConfig = None, app=None): """Creates a new tenant from the given options. Args: @@ -122,7 +123,7 @@ def create_tenant( def update_tenant( tenant_id, display_name=None, allow_password_sign_up=None, enable_email_link_sign_in=None, - multi_factor_config: multi_factor_config_mgt.MultiFactorConfig = None, app=None): + multi_factor_config: MultiFactorConfig = None, app=None): """Updates an existing tenant with the given options. Args: @@ -189,6 +190,7 @@ def list_tenants(page_token=None, max_results=_MAX_LIST_TENANTS_RESULTS, app=Non 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) @@ -211,7 +213,8 @@ class Tenant: def __init__(self, data): if not isinstance(data, dict): - raise ValueError('Invalid data argument in Tenant constructor: {0}'.format(data)) + raise ValueError( + 'Invalid data argument in Tenant constructor: {0}'.format(data)) if not 'name' in data: raise ValueError('Tenant response missing required keys.') @@ -238,7 +241,7 @@ def enable_email_link_sign_in(self): def multi_factor_config(self): data = self._data.get('mfaConfig', None) if data is not None: - return multi_factor_config_mgt.MultiFactorServerConfig(data) + return MultiFactorServerConfig(data) return None @@ -250,7 +253,8 @@ class _TenantManagementService: 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) + 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}) @@ -269,7 +273,7 @@ def auth_for_tenant(self, tenant_id): client = auth.Client(self.app, tenant_id=tenant_id) self.tenant_clients[tenant_id] = client - return client + return client def get_tenant(self, tenant_id): """Gets the tenant corresponding to the given ``tenant_id``.""" @@ -286,7 +290,7 @@ def get_tenant(self, tenant_id): def create_tenant( self, display_name, allow_password_sign_up=None, enable_email_link_sign_in=None, - multi_factor_config: multi_factor_config_mgt.MultiFactorConfig = None): + multi_factor_config: MultiFactorConfig = None): """Creates a new tenant from the given parameters.""" payload = {'displayName': _validate_display_name(display_name)} @@ -297,7 +301,7 @@ def create_tenant( payload['enableEmailLinkSignin'] = _auth_utils.validate_boolean( enable_email_link_sign_in, 'enableEmailLinkSignin') if multi_factor_config is not None: - if not isinstance(multi_factor_config, multi_factor_config_mgt.MultiFactorConfig): + if not isinstance(multi_factor_config, MultiFactorConfig): raise ValueError( 'multi_factor_config must be of type MultiFactorConfig.') payload['mfaConfig'] = multi_factor_config.build_server_request() @@ -311,7 +315,7 @@ def create_tenant( def update_tenant( self, tenant_id, display_name=None, allow_password_sign_up=None, enable_email_link_sign_in=None, - multi_factor_config: multi_factor_config_mgt.MultiFactorConfig = None): + multi_factor_config: MultiFactorConfig = 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.') @@ -326,12 +330,14 @@ def update_tenant( payload['enableEmailLinkSignin'] = _auth_utils.validate_boolean( enable_email_link_sign_in, 'enableEmailLinkSignin') if multi_factor_config is not None: - if not isinstance(multi_factor_config, multi_factor_config_mgt.MultiFactorConfig): - raise ValueError('multi_factor_config must be of type MultiFactorConfig.') + if not isinstance(multi_factor_config, MultiFactorConfig): + raise ValueError( + 'multi_factor_config must be of type MultiFactorConfig.') payload['mfaConfig'] = multi_factor_config.build_server_request() if not payload: - raise ValueError('At least one parameter must be specified for update.') + 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)) diff --git a/integration/test_project_config_mgt.py b/integration/test_project_config_mgt.py index e7c79a07d..a7ca2c5f1 100644 --- a/integration/test_project_config_mgt.py +++ b/integration/test_project_config_mgt.py @@ -19,6 +19,7 @@ from firebase_admin import project_config_mgt from firebase_admin import multi_factor_config_mgt +ADJACENT_INTERVALS = 5 @pytest.fixture(scope='module') def sample_mfa_config(): @@ -27,7 +28,7 @@ def sample_mfa_config(): { 'state': 'ENABLED', 'totpProviderConfig': { - 'adjacentIntervals': 5 + 'adjacentIntervals': ADJACENT_INTERVALS } } ] @@ -61,9 +62,9 @@ def _assert_multi_factor_config(multi_factor_config): assert isinstance(multi_factor_config.provider_configs, list) for provider_config in multi_factor_config.provider_configs: assert isinstance(provider_config, multi_factor_config_mgt.MultiFactorServerConfig - .ProviderConfigServerConfig) + .ProviderServerConfig) assert provider_config.state == 'ENABLED' assert isinstance(provider_config.totp_provider_config, - multi_factor_config_mgt.MultiFactorServerConfig.ProviderConfigServerConfig + multi_factor_config_mgt.MultiFactorServerConfig.ProviderServerConfig .TOTPProviderServerConfig) - assert provider_config.totp_provider_config.adjacent_intervals == 5 + assert provider_config.totp_provider_config.adjacent_intervals == ADJACENT_INTERVALS diff --git a/integration/test_tenant_mgt.py b/integration/test_tenant_mgt.py index 96578d1b4..1766b2b1a 100644 --- a/integration/test_tenant_mgt.py +++ b/integration/test_tenant_mgt.py @@ -58,10 +58,10 @@ def _assert_multi_factor_config(mfa_config): assert isinstance(mfa_config.provider_configs, list) for provider_config in mfa_config.provider_configs: assert isinstance(provider_config, multi_factor_config_mgt.MultiFactorServerConfig.\ - ProviderConfigServerConfig) + ProviderServerConfig) assert provider_config.state == 'ENABLED' assert isinstance(provider_config.totp_provider_config, - multi_factor_config_mgt.MultiFactorServerConfig.ProviderConfigServerConfig + multi_factor_config_mgt.MultiFactorServerConfig.ProviderServerConfig .TOTPProviderServerConfig) assert provider_config.totp_provider_config.adjacent_intervals == 5 diff --git a/tests/test_multi_factor_config.py b/tests/test_multi_factor_config.py index 7e4190ba8..b14b395fc 100644 --- a/tests/test_multi_factor_config.py +++ b/tests/test_multi_factor_config.py @@ -135,22 +135,22 @@ def test_invalid_multi_factor_config_response(self): test_config = 'invalid' with pytest.raises(ValueError) as excinfo: multi_factor_config_mgt.MultiFactorServerConfig(test_config) - assert str(excinfo.value).startswith('Invalid data argument in MultiFactorConfig' + assert str(excinfo.value).startswith('Invalid data argument in MultiFactorServerConfig' ' constructor: {0}'.format(test_config)) def test_invalid_provider_config_response(self): test_config = 'invalid' with pytest.raises(ValueError) as excinfo: - multi_factor_config_mgt.MultiFactorServerConfig.ProviderConfigServerConfig(test_config) - assert str(excinfo.value).startswith('Invalid data argument in ProviderConfig' + multi_factor_config_mgt.MultiFactorServerConfig.ProviderServerConfig(test_config) + assert str(excinfo.value).startswith('Invalid data argument in ProviderServerConfig' ' constructor: {0}'.format(test_config)) def test_invalid_totp_provider_config_response(self): test_config = 'invalid' with pytest.raises(ValueError) as excinfo: - multi_factor_config_mgt.MultiFactorServerConfig.ProviderConfigServerConfig.\ + multi_factor_config_mgt.MultiFactorServerConfig.ProviderServerConfig.\ TOTPProviderServerConfig(test_config) - assert str(excinfo.value).startswith('Invalid data argument in TOTPProviderConfig' + assert str(excinfo.value).startswith('Invalid data argument in TOTPProviderServerConfig' ' constructor: {0}'.format(test_config)) def test_valid_server_response(self): @@ -173,9 +173,9 @@ def _assert_multi_factor_config(mfa_config): for provider_config in mfa_config.provider_configs: assert isinstance( provider_config, - multi_factor_config_mgt.MultiFactorServerConfig.ProviderConfigServerConfig) + multi_factor_config_mgt.MultiFactorServerConfig.ProviderServerConfig) assert provider_config.state == 'ENABLED' assert isinstance(provider_config.totp_provider_config, - multi_factor_config_mgt.MultiFactorServerConfig.ProviderConfigServerConfig + multi_factor_config_mgt.MultiFactorServerConfig.ProviderServerConfig .TOTPProviderServerConfig) assert provider_config.totp_provider_config.adjacent_intervals == 5 diff --git a/tests/test_project_config_mgt.py b/tests/test_project_config_mgt.py index 6cba66483..8d4775315 100644 --- a/tests/test_project_config_mgt.py +++ b/tests/test_project_config_mgt.py @@ -25,6 +25,8 @@ from firebase_admin import multi_factor_config_mgt +ADJACENT_INTERVALS = 5 + GET_PROJECT_RESPONSE = """{ "mfaConfig":{ "providerConfigs":[ @@ -75,7 +77,7 @@ def test_project_config(self): { 'state': 'ENABLED', 'totpProviderConfig': { - 'adjacentIntervals': 5, + 'adjacentIntervals': ADJACENT_INTERVALS, } } ] @@ -129,7 +131,7 @@ def test_update_project_config(self, project_config_mgt_app): multi_factor_config_mgt.ProviderConfig( state=multi_factor_config_mgt.ProviderConfig.State.ENABLED, totp_provider_config=multi_factor_config_mgt.TOTPProviderConfig( - adjacent_intervals=5 + adjacent_intervals=ADJACENT_INTERVALS ) ) ] @@ -146,7 +148,7 @@ def test_update_project_config(self, project_config_mgt_app): { 'state': 'ENABLED', 'totpProviderConfig': { - 'adjacentIntervals': 5, + 'adjacentIntervals': ADJACENT_INTERVALS, } } ] @@ -168,12 +170,12 @@ def _assert_multi_factor_config(multi_factor_config): assert isinstance(multi_factor_config.provider_configs, list) for provider_config in multi_factor_config.provider_configs: assert isinstance(provider_config, multi_factor_config_mgt.MultiFactorServerConfig - .ProviderConfigServerConfig) + .ProviderServerConfig) assert provider_config.state == 'ENABLED' assert isinstance(provider_config.totp_provider_config, - multi_factor_config_mgt.MultiFactorServerConfig.ProviderConfigServerConfig + multi_factor_config_mgt.MultiFactorServerConfig.ProviderServerConfig .TOTPProviderServerConfig) - assert provider_config.totp_provider_config.adjacent_intervals == 5 + assert provider_config.totp_provider_config.adjacent_intervals == ADJACENT_INTERVALS def _assert_project_config(project_config): if project_config.multi_factor_config is not None: diff --git a/tests/test_tenant_mgt.py b/tests/test_tenant_mgt.py index e964c540d..1a4a0372a 100644 --- a/tests/test_tenant_mgt.py +++ b/tests/test_tenant_mgt.py @@ -31,6 +31,8 @@ from tests import test_token_gen +ADJACENT_INTERVALS = 5 + GET_TENANT_RESPONSE = """{ "name": "projects/mock-project-id/tenants/tenant-id", "displayName": "Test Tenant", @@ -260,7 +262,7 @@ def test_create_tenant(self, tenant_mgt_app): multi_factor_config_mgt.ProviderConfig( state=multi_factor_config_mgt.ProviderConfig.State.ENABLED, totp_provider_config=multi_factor_config_mgt.TOTPProviderConfig( - adjacent_intervals=5 + adjacent_intervals=ADJACENT_INTERVALS ) ) ] @@ -279,7 +281,7 @@ def test_create_tenant(self, tenant_mgt_app): { 'state': 'ENABLED', 'totpProviderConfig': { - 'adjacentIntervals': 5 + 'adjacentIntervals': ADJACENT_INTERVALS } } ] @@ -379,7 +381,7 @@ def test_update_tenant(self, tenant_mgt_app): multi_factor_config_mgt.ProviderConfig( state=multi_factor_config_mgt.ProviderConfig.State.ENABLED, totp_provider_config=multi_factor_config_mgt.TOTPProviderConfig( - adjacent_intervals=5 + adjacent_intervals=ADJACENT_INTERVALS ) ) ] @@ -399,7 +401,7 @@ def test_update_tenant(self, tenant_mgt_app): { 'state': 'ENABLED', 'totpProviderConfig': { - 'adjacentIntervals': 5 + 'adjacentIntervals': ADJACENT_INTERVALS } } ] @@ -1068,12 +1070,12 @@ def _assert_multi_factor_config(mfa_config): assert isinstance(mfa_config.provider_configs, list) for provider_config in mfa_config.provider_configs: assert isinstance(provider_config, multi_factor_config_mgt.MultiFactorServerConfig.\ - ProviderConfigServerConfig) + ProviderServerConfig) assert provider_config.state == 'ENABLED' assert isinstance(provider_config.totp_provider_config, - multi_factor_config_mgt.MultiFactorServerConfig.ProviderConfigServerConfig + multi_factor_config_mgt.MultiFactorServerConfig.ProviderServerConfig .TOTPProviderServerConfig) - assert provider_config.totp_provider_config.adjacent_intervals == 5 + assert provider_config.totp_provider_config.adjacent_intervals == ADJACENT_INTERVALS def _assert_tenant(tenant, tenant_id='tenant-id'): assert isinstance(tenant, tenant_mgt.Tenant) From 4bc2c83d1256a7e9e682d9e4eaf9e69e4b43d46d Mon Sep 17 00:00:00 2001 From: pragatimodi <110490169+pragatimodi@users.noreply.github.com> Date: Wed, 14 Jun 2023 20:43:57 -0700 Subject: [PATCH 13/21] Apply suggestions from code review Co-authored-by: Kevin Cheung --- firebase_admin/multi_factor_config_mgt.py | 17 ++++++++--------- firebase_admin/project_config_mgt.py | 7 ++++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/firebase_admin/multi_factor_config_mgt.py b/firebase_admin/multi_factor_config_mgt.py index 61d4a134d..d7538a762 100644 --- a/firebase_admin/multi_factor_config_mgt.py +++ b/firebase_admin/multi_factor_config_mgt.py @@ -13,7 +13,7 @@ # limitations under the License. """Firebase multifactor configuration management module. -This module contains functions for managing various multifactor configurations at +This module contains functions for managing multifactor auth configuration at the project and tenant level. """ from enum import Enum @@ -94,7 +94,7 @@ def adjacent_intervals(self): class TOTPProviderConfig: - """Represents a TOTP Provider Configuration to be specified for a tenant or project.""" + """A tenant or project's TOTP provider configuration.""" def __init__(self, adjacent_intervals: int = None): self.adjacent_intervals: int = adjacent_intervals @@ -106,7 +106,7 @@ def to_dict(self) -> dict: return data def validate(self): - """Validates a given totp_provider_config object. + """Validates the configuration. Raises: ValueError: In case of an unsuccessful validation. @@ -133,8 +133,8 @@ def build_server_request(self): class ProviderConfig: - """Represents a provider configuration for tenant or project. - Currently only TOTP can be configured""" + """A tenant or project's multifactor provider configuration. + Currently, only TOTP can be configured.""" class State(Enum): ENABLED = 'ENABLED' @@ -155,7 +155,7 @@ def to_dict(self) -> dict: return data def validate(self): - """Validates a provider_config object. + """Validates the provider configuration. Raises: ValueError: In case of an unsuccessful validation. @@ -184,8 +184,7 @@ def build_server_request(self): class MultiFactorConfig: - """Represents a multi factor configuration for tenant or project - """ + """A tenant or project's multi factor configuration.""" def __init__(self, provider_configs: List[ProviderConfig] = None): @@ -199,7 +198,7 @@ def to_dict(self) -> dict: return data def validate(self): - """Validates a given multi_factor_config object. + """Validates the configuration. Raises: ValueError: In case of an unsuccessful validation. diff --git a/firebase_admin/project_config_mgt.py b/firebase_admin/project_config_mgt.py index 851a3499c..8ea9dabf8 100644 --- a/firebase_admin/project_config_mgt.py +++ b/firebase_admin/project_config_mgt.py @@ -13,7 +13,7 @@ # limitations under the License. """Firebase project configuration management module. -This module contains functions for managing various project operations like update and create +This module contains functions for managing projects. """ import requests @@ -52,9 +52,10 @@ def get_project_config(app=None): return project_config_mgt_service.get_project_config() def update_project_config(multi_factor_config: MultiFactorConfig = None, app=None): - """Update the Project Config with the given options. + """Update the project config with the given options. + Args: - multi_factor_config: Updated Multi Factor Authentication configuration + multi_factor_config: Updated multi-factor authentication configuration (optional) app: An App instance (optional). Returns: From e542f91009859b4407eab1f8b37c5d688262be5a Mon Sep 17 00:00:00 2001 From: Pragati Date: Thu, 6 Jul 2023 09:59:45 -0700 Subject: [PATCH 14/21] Addressing PR feedback --- firebase_admin/multi_factor_config_mgt.py | 12 ++++++------ firebase_admin/project_config_mgt.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/firebase_admin/multi_factor_config_mgt.py b/firebase_admin/multi_factor_config_mgt.py index 61d4a134d..593e3a7fc 100644 --- a/firebase_admin/multi_factor_config_mgt.py +++ b/firebase_admin/multi_factor_config_mgt.py @@ -44,7 +44,7 @@ class MultiFactorServerConfig: def __init__(self, data): if not isinstance(data, dict): raise ValueError( - 'Invalid data argument in MultiFactorServerConfig constructor: {0}'.format(data)) + 'Invalid data argument in MultiFactorServerConfig constructor: {0}, must be a valid dict'.format(data)) self._data = data @property @@ -120,7 +120,7 @@ def validate(self): # pylint: disable=C0123 if type(self.adjacent_intervals) is not int: raise ValueError( - 'totp_provider_config.adjacent_intervals must be an integer between' + 'TOTPProviderConfig.adjacent_intervals must be an integer between' ' 1 and 10 (inclusive).') if not 1 <= self.adjacent_intervals <= 10: raise ValueError( @@ -167,16 +167,16 @@ def validate(self): 'totp_provider_config'}, config_name='ProviderConfig') if self.state is None: - raise ValueError('provider_config.state must be defined.') + raise ValueError('ProviderConfig.state must be defined.') if not isinstance(self.state, ProviderConfig.State): raise ValueError( - 'provider_config.state must be of type ProviderConfig.State.') + 'ProviderConfig.state must be of type ProviderConfig.State.') if self.totp_provider_config is None: raise ValueError( - 'provider_config.totp_provider_config must be defined.') + 'ProviderConfig.totp_provider_config must be defined.') if not isinstance(self.totp_provider_config, TOTPProviderConfig): raise ValueError( - 'provider_configs.totp_provider_config must be of type TOTPProviderConfig.') + 'ProviderConfig.totp_provider_config must be of type TOTPProviderConfig.') def build_server_request(self): self.validate() diff --git a/firebase_admin/project_config_mgt.py b/firebase_admin/project_config_mgt.py index 851a3499c..a66247aa7 100644 --- a/firebase_admin/project_config_mgt.py +++ b/firebase_admin/project_config_mgt.py @@ -35,7 +35,7 @@ def get_project_config(app=None): - """Gets the project config corresponding to the given project_id. + """Gets the project config corresponding to the current project_id. Args: app: An App instance (optional). From b7091487d87f568d5af24fcb9173d1cef828bcd1 Mon Sep 17 00:00:00 2001 From: pragatimodi <110490169+pragatimodi@users.noreply.github.com> Date: Wed, 27 Sep 2023 13:37:44 -0400 Subject: [PATCH 15/21] Apply suggestions from code review Co-authored-by: Kevin Cheung --- firebase_admin/multi_factor_config_mgt.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/firebase_admin/multi_factor_config_mgt.py b/firebase_admin/multi_factor_config_mgt.py index e0d867ea5..6fb06ec8b 100644 --- a/firebase_admin/multi_factor_config_mgt.py +++ b/firebase_admin/multi_factor_config_mgt.py @@ -55,8 +55,7 @@ def provider_configs(self): return None class ProviderServerConfig: - """Represents provider configuration response received from the server and converts - it to user format. + """Represents the provider configuration response received from the server. """ def __init__(self, data): @@ -77,8 +76,7 @@ def totp_provider_config(self): return None class TOTPProviderServerConfig: - """Represents TOTP provider configuration response received from the server and converts - it to user format. + """Represents the TOTP provider configuration response received from the server. """ def __init__(self, data): From 083d670e7ed54f10104728115fd0ffba2c1af3ff Mon Sep 17 00:00:00 2001 From: pragatimodi <110490169+pragatimodi@users.noreply.github.com> Date: Wed, 27 Sep 2023 13:38:18 -0400 Subject: [PATCH 16/21] Update firebase_admin/multi_factor_config_mgt.py Co-authored-by: Kevin Cheung --- firebase_admin/multi_factor_config_mgt.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/firebase_admin/multi_factor_config_mgt.py b/firebase_admin/multi_factor_config_mgt.py index 6fb06ec8b..3db4486d6 100644 --- a/firebase_admin/multi_factor_config_mgt.py +++ b/firebase_admin/multi_factor_config_mgt.py @@ -37,8 +37,7 @@ def validate_keys(keys, valid_keys, config_name): class MultiFactorServerConfig: - """Represents multi factor configuration response received from the server and - converts it to user format. + """Represents the multi-factor configuration response received from the server. """ def __init__(self, data): From c67e04733a17fc2e8b10eb97423f948349875449 Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 20 Dec 2023 11:57:32 -0800 Subject: [PATCH 17/21] fix test error messages --- firebase_admin/multi_factor_config_mgt.py | 6 +++--- tests/test_multi_factor_config.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/firebase_admin/multi_factor_config_mgt.py b/firebase_admin/multi_factor_config_mgt.py index 3db4486d6..f965be8a3 100644 --- a/firebase_admin/multi_factor_config_mgt.py +++ b/firebase_admin/multi_factor_config_mgt.py @@ -117,7 +117,7 @@ def validate(self): # pylint: disable=C0123 if type(self.adjacent_intervals) is not int: raise ValueError( - 'TOTPProviderConfig.adjacent_intervals must be an integer between' + 'totp_provider_config.adjacent_intervals must be an integer between' ' 1 and 10 (inclusive).') if not 1 <= self.adjacent_intervals <= 10: raise ValueError( @@ -209,11 +209,11 @@ def validate(self): 'multi_factor_config.provider_configs must be specified') if not isinstance(self.provider_configs, list) or not self.provider_configs: raise ValueError( - 'provider_configs must be an array of type ProviderConfigs.') + 'provider_configs must be an array of type ProviderConfig.') for provider_config in self.provider_configs: if not isinstance(provider_config, ProviderConfig): raise ValueError( - 'provider_configs must be an array of type ProviderConfigs.') + 'provider_configs must be an array of type ProviderConfig.') provider_config.validate() def build_server_request(self): diff --git a/tests/test_multi_factor_config.py b/tests/test_multi_factor_config.py index b14b395fc..eaf0dcb25 100644 --- a/tests/test_multi_factor_config.py +++ b/tests/test_multi_factor_config.py @@ -44,7 +44,7 @@ def test_invalid_provider_configs_type(self, provider_configs): with pytest.raises(ValueError) as excinfo: test_config.build_server_request() assert str(excinfo.value).startswith('provider_configs must be an array of type' - ' ProviderConfigs.') + ' ProviderConfig.') @pytest.mark.parametrize('provider_configs', [[True], [1, 2], @@ -55,7 +55,7 @@ def test_invalid_mfa_config_provider_config(self, provider_configs): with pytest.raises(ValueError) as excinfo: test_config.build_server_request() assert str(excinfo.value).startswith('provider_configs must be an array of type' - ' ProviderConfigs.') + ' ProviderConfig.') class TestProviderConfig: @@ -73,7 +73,7 @@ def test_undefined_provider_config_state(self): with pytest.raises(ValueError) as excinfo: test_config.build_server_request() assert str(excinfo.value).startswith( - 'provider_config.state must be defined.') + 'ProviderConfig.state must be defined.') @pytest.mark.parametrize('state', ['', 1, True, False, [], (), {}, "foo", 'ENABLED']) @@ -83,7 +83,7 @@ def test_invalid_provider_config_state(self, state): ) with pytest.raises(ValueError) as excinfo: test_config.build_server_request() - assert str(excinfo.value).startswith('provider_config.state must be of type' + assert str(excinfo.value).startswith('ProviderConfig.state must be of type' ' ProviderConfig.State.') @pytest.mark.parametrize('state', @@ -93,7 +93,7 @@ def test_undefined_totp_provider_config(self, state): test_config = multi_factor_config_mgt.ProviderConfig(state=state) with pytest.raises(ValueError) as excinfo: test_config.build_server_request() - assert str(excinfo.value).startswith('provider_config.totp_provider_config must be' + assert str(excinfo.value).startswith('ProviderConfig.totp_provider_config must be' ' defined.') @pytest.mark.parametrize('totp_provider_config', @@ -103,7 +103,7 @@ def test_invalid_totp_provider_config_type(self, totp_provider_config): test_config.totp_provider_config = totp_provider_config with pytest.raises(ValueError) as excinfo: test_config.build_server_request() - assert str(excinfo.value).startswith('provider_configs.totp_provider_config must be of type' + assert str(excinfo.value).startswith('ProviderConfig.totp_provider_config must be of type' ' TOTPProviderConfig.') From 3245d6d1e7ce25ddf21842ee0323f5eccda7c1fc Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 20 Dec 2023 12:04:06 -0800 Subject: [PATCH 18/21] lint fixes --- firebase_admin/multi_factor_config_mgt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/firebase_admin/multi_factor_config_mgt.py b/firebase_admin/multi_factor_config_mgt.py index f965be8a3..7e191ffce 100644 --- a/firebase_admin/multi_factor_config_mgt.py +++ b/firebase_admin/multi_factor_config_mgt.py @@ -43,7 +43,8 @@ class MultiFactorServerConfig: def __init__(self, data): if not isinstance(data, dict): raise ValueError( - 'Invalid data argument in MultiFactorServerConfig constructor: {0}, must be a valid dict'.format(data)) + 'Invalid data argument in MultiFactorServerConfig constructor: {0}, must be a valid' + ' dict'.format(data)) self._data = data @property From b7e9ebad15b8bd1fc4148a41c4c2cf3257a00d39 Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 20 Dec 2023 12:05:52 -0800 Subject: [PATCH 19/21] fix indent --- firebase_admin/multi_factor_config_mgt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase_admin/multi_factor_config_mgt.py b/firebase_admin/multi_factor_config_mgt.py index 7e191ffce..db05c5567 100644 --- a/firebase_admin/multi_factor_config_mgt.py +++ b/firebase_admin/multi_factor_config_mgt.py @@ -44,7 +44,7 @@ def __init__(self, data): if not isinstance(data, dict): raise ValueError( 'Invalid data argument in MultiFactorServerConfig constructor: {0}, must be a valid' - ' dict'.format(data)) + ' dict'.format(data)) self._data = data @property From 6725d4109768f3ee236faeba1d3671f89331118a Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 20 Dec 2023 16:16:39 -0800 Subject: [PATCH 20/21] succinct import types --- integration/test_project_config_mgt.py | 29 +++++++++++--------- tests/test_multi_factor_config.py | 37 ++++++++++++++------------ tests/test_tenant_mgt.py | 27 ++++++++++--------- 3 files changed, 52 insertions(+), 41 deletions(-) diff --git a/integration/test_project_config_mgt.py b/integration/test_project_config_mgt.py index a7ca2c5f1..52b86841e 100644 --- a/integration/test_project_config_mgt.py +++ b/integration/test_project_config_mgt.py @@ -16,8 +16,13 @@ import pytest -from firebase_admin import project_config_mgt -from firebase_admin import multi_factor_config_mgt +from firebase_admin.project_config_mgt import ProjectConfig +from firebase_admin.project_config_mgt import get_project_config +from firebase_admin.project_config_mgt import update_project_config +from firebase_admin.multi_factor_config_mgt import MultiFactorConfig +from firebase_admin.multi_factor_config_mgt import MultiFactorServerConfig +from firebase_admin.multi_factor_config_mgt import ProviderConfig +from firebase_admin.multi_factor_config_mgt import TOTPProviderConfig ADJACENT_INTERVALS = 5 @@ -37,34 +42,34 @@ def sample_mfa_config(): def test_update_project_config(): - mfa_object = multi_factor_config_mgt.MultiFactorConfig( + mfa_object = MultiFactorConfig( provider_configs=[ - multi_factor_config_mgt.ProviderConfig( - state=multi_factor_config_mgt.ProviderConfig.State.ENABLED, - totp_provider_config=multi_factor_config_mgt.TOTPProviderConfig( + ProviderConfig( + state=ProviderConfig.State.ENABLED, + totp_provider_config=TOTPProviderConfig( adjacent_intervals=5 ) ) ] ) - project_config = project_config_mgt.update_project_config(multi_factor_config=mfa_object) + project_config = update_project_config(multi_factor_config=mfa_object) _assert_multi_factor_config(project_config.multi_factor_config) def test_get_project(): - project_config = project_config_mgt.get_project_config() - assert isinstance(project_config, project_config_mgt.ProjectConfig) + project_config = get_project_config() + assert isinstance(project_config, ProjectConfig) _assert_multi_factor_config(project_config.multi_factor_config) def _assert_multi_factor_config(multi_factor_config): - assert isinstance(multi_factor_config, multi_factor_config_mgt.MultiFactorServerConfig) + assert isinstance(multi_factor_config, MultiFactorServerConfig) assert len(multi_factor_config.provider_configs) == 1 assert isinstance(multi_factor_config.provider_configs, list) for provider_config in multi_factor_config.provider_configs: - assert isinstance(provider_config, multi_factor_config_mgt.MultiFactorServerConfig + assert isinstance(provider_config, MultiFactorServerConfig .ProviderServerConfig) assert provider_config.state == 'ENABLED' assert isinstance(provider_config.totp_provider_config, - multi_factor_config_mgt.MultiFactorServerConfig.ProviderServerConfig + MultiFactorServerConfig.ProviderServerConfig .TOTPProviderServerConfig) assert provider_config.totp_provider_config.adjacent_intervals == ADJACENT_INTERVALS diff --git a/tests/test_multi_factor_config.py b/tests/test_multi_factor_config.py index eaf0dcb25..88a6f6652 100644 --- a/tests/test_multi_factor_config.py +++ b/tests/test_multi_factor_config.py @@ -15,12 +15,15 @@ import pytest -from firebase_admin import multi_factor_config_mgt - -sample_mfa_config = multi_factor_config_mgt.MultiFactorConfig( - provider_configs=[multi_factor_config_mgt.ProviderConfig( - state=multi_factor_config_mgt.ProviderConfig.State.ENABLED, - totp_provider_config=multi_factor_config_mgt.TOTPProviderConfig( +from firebase_admin.multi_factor_config_mgt import MultiFactorConfig +from firebase_admin.multi_factor_config_mgt import MultiFactorServerConfig +from firebase_admin.multi_factor_config_mgt import TOTPProviderConfig +from firebase_admin.multi_factor_config_mgt import ProviderConfig + +sample_mfa_config = MultiFactorConfig( + provider_configs=[ProviderConfig( + state=ProviderConfig.State.ENABLED, + totp_provider_config=TOTPProviderConfig( adjacent_intervals=5 ) )] @@ -78,7 +81,7 @@ def test_undefined_provider_config_state(self): @pytest.mark.parametrize('state', ['', 1, True, False, [], (), {}, "foo", 'ENABLED']) def test_invalid_provider_config_state(self, state): - test_config = multi_factor_config_mgt.ProviderConfig( + test_config = ProviderConfig( state=state ) with pytest.raises(ValueError) as excinfo: @@ -87,10 +90,10 @@ def test_invalid_provider_config_state(self, state): ' ProviderConfig.State.') @pytest.mark.parametrize('state', - [multi_factor_config_mgt.ProviderConfig.State.ENABLED, - multi_factor_config_mgt.ProviderConfig.State.DISABLED]) + [ProviderConfig.State.ENABLED, + ProviderConfig.State.DISABLED]) def test_undefined_totp_provider_config(self, state): - test_config = multi_factor_config_mgt.ProviderConfig(state=state) + test_config = ProviderConfig(state=state) with pytest.raises(ValueError) as excinfo: test_config.build_server_request() assert str(excinfo.value).startswith('ProviderConfig.totp_provider_config must be' @@ -134,21 +137,21 @@ class TestMultiFactorServerConfig: def test_invalid_multi_factor_config_response(self): test_config = 'invalid' with pytest.raises(ValueError) as excinfo: - multi_factor_config_mgt.MultiFactorServerConfig(test_config) + MultiFactorServerConfig(test_config) assert str(excinfo.value).startswith('Invalid data argument in MultiFactorServerConfig' ' constructor: {0}'.format(test_config)) def test_invalid_provider_config_response(self): test_config = 'invalid' with pytest.raises(ValueError) as excinfo: - multi_factor_config_mgt.MultiFactorServerConfig.ProviderServerConfig(test_config) + MultiFactorServerConfig.ProviderServerConfig(test_config) assert str(excinfo.value).startswith('Invalid data argument in ProviderServerConfig' ' constructor: {0}'.format(test_config)) def test_invalid_totp_provider_config_response(self): test_config = 'invalid' with pytest.raises(ValueError) as excinfo: - multi_factor_config_mgt.MultiFactorServerConfig.ProviderServerConfig.\ + MultiFactorServerConfig.ProviderServerConfig.\ TOTPProviderServerConfig(test_config) assert str(excinfo.value).startswith('Invalid data argument in TOTPProviderServerConfig' ' constructor: {0}'.format(test_config)) @@ -162,20 +165,20 @@ def test_valid_server_response(self): } }] } - mfa_config = multi_factor_config_mgt.MultiFactorServerConfig(response) + mfa_config = MultiFactorServerConfig(response) _assert_multi_factor_config(mfa_config) def _assert_multi_factor_config(mfa_config): - assert isinstance(mfa_config, multi_factor_config_mgt.MultiFactorServerConfig) + assert isinstance(mfa_config, MultiFactorServerConfig) assert len(mfa_config.provider_configs) == 1 assert isinstance(mfa_config.provider_configs, list) for provider_config in mfa_config.provider_configs: assert isinstance( provider_config, - multi_factor_config_mgt.MultiFactorServerConfig.ProviderServerConfig) + MultiFactorServerConfig.ProviderServerConfig) assert provider_config.state == 'ENABLED' assert isinstance(provider_config.totp_provider_config, - multi_factor_config_mgt.MultiFactorServerConfig.ProviderServerConfig + MultiFactorServerConfig.ProviderServerConfig .TOTPProviderServerConfig) assert provider_config.totp_provider_config.adjacent_intervals == 5 diff --git a/tests/test_tenant_mgt.py b/tests/test_tenant_mgt.py index 1a4a0372a..5e9963fec 100644 --- a/tests/test_tenant_mgt.py +++ b/tests/test_tenant_mgt.py @@ -26,7 +26,10 @@ from firebase_admin import tenant_mgt from firebase_admin import _auth_providers from firebase_admin import _user_mgt -from firebase_admin import multi_factor_config_mgt +from firebase_admin.multi_factor_config_mgt import MultiFactorConfig +from firebase_admin.multi_factor_config_mgt import MultiFactorServerConfig +from firebase_admin.multi_factor_config_mgt import ProviderConfig +from firebase_admin.multi_factor_config_mgt import TOTPProviderConfig from tests import testutils from tests import test_token_gen @@ -257,11 +260,11 @@ def test_invalid_multi_factor_configs(self, multi_factor_config, tenant_mgt_app) def test_create_tenant(self, tenant_mgt_app): _, recorder = _instrument_tenant_mgt(tenant_mgt_app, 200, GET_TENANT_RESPONSE) - mfa_object = multi_factor_config_mgt.MultiFactorConfig( + mfa_object = MultiFactorConfig( provider_configs=[ - multi_factor_config_mgt.ProviderConfig( - state=multi_factor_config_mgt.ProviderConfig.State.ENABLED, - totp_provider_config=multi_factor_config_mgt.TOTPProviderConfig( + ProviderConfig( + state=ProviderConfig.State.ENABLED, + totp_provider_config=TOTPProviderConfig( adjacent_intervals=ADJACENT_INTERVALS ) ) @@ -376,11 +379,11 @@ def test_update_tenant_no_args(self, tenant_mgt_app): def test_update_tenant(self, tenant_mgt_app): _, recorder = _instrument_tenant_mgt(tenant_mgt_app, 200, GET_TENANT_RESPONSE) - mfa_object = multi_factor_config_mgt.MultiFactorConfig( + mfa_object = MultiFactorConfig( provider_configs=[ - multi_factor_config_mgt.ProviderConfig( - state=multi_factor_config_mgt.ProviderConfig.State.ENABLED, - totp_provider_config=multi_factor_config_mgt.TOTPProviderConfig( + ProviderConfig( + state=ProviderConfig.State.ENABLED, + totp_provider_config=TOTPProviderConfig( adjacent_intervals=ADJACENT_INTERVALS ) ) @@ -1065,15 +1068,15 @@ def test_custom_token_with_claims(self, tenant_aware_custom_token_app): custom_token, expected_claims=claims, tenant_id='test-tenant') def _assert_multi_factor_config(mfa_config): - assert isinstance(mfa_config, multi_factor_config_mgt.MultiFactorServerConfig) + assert isinstance(mfa_config, MultiFactorServerConfig) assert len(mfa_config.provider_configs) == 1 assert isinstance(mfa_config.provider_configs, list) for provider_config in mfa_config.provider_configs: - assert isinstance(provider_config, multi_factor_config_mgt.MultiFactorServerConfig.\ + assert isinstance(provider_config, MultiFactorServerConfig.\ ProviderServerConfig) assert provider_config.state == 'ENABLED' assert isinstance(provider_config.totp_provider_config, - multi_factor_config_mgt.MultiFactorServerConfig.ProviderServerConfig + MultiFactorServerConfig.ProviderServerConfig .TOTPProviderServerConfig) assert provider_config.totp_provider_config.adjacent_intervals == ADJACENT_INTERVALS From 907bf2d5df76d0ceb4c1d9264d79575c6770df73 Mon Sep 17 00:00:00 2001 From: pragatimodi <110490169+pragatimodi@users.noreply.github.com> Date: Wed, 10 Jan 2024 11:06:13 +0000 Subject: [PATCH 21/21] change copyright year --- firebase_admin/multi_factor_config_mgt.py | 2 +- firebase_admin/project_config_mgt.py | 2 +- integration/test_project_config_mgt.py | 2 +- tests/test_multi_factor_config.py | 2 +- tests/test_project_config_mgt.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/firebase_admin/multi_factor_config_mgt.py b/firebase_admin/multi_factor_config_mgt.py index db05c5567..6196568a5 100644 --- a/firebase_admin/multi_factor_config_mgt.py +++ b/firebase_admin/multi_factor_config_mgt.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google Inc. +# Copyright 2024 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/firebase_admin/project_config_mgt.py b/firebase_admin/project_config_mgt.py index 329ded69f..4ae65958a 100644 --- a/firebase_admin/project_config_mgt.py +++ b/firebase_admin/project_config_mgt.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google Inc. +# Copyright 2024 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/integration/test_project_config_mgt.py b/integration/test_project_config_mgt.py index 52b86841e..bdfd4ea34 100644 --- a/integration/test_project_config_mgt.py +++ b/integration/test_project_config_mgt.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google Inc. +# Copyright 2024 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/test_multi_factor_config.py b/tests/test_multi_factor_config.py index 88a6f6652..50eaeec9a 100644 --- a/tests/test_multi_factor_config.py +++ b/tests/test_multi_factor_config.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google Inc. +# Copyright 2024 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/test_project_config_mgt.py b/tests/test_project_config_mgt.py index 8d4775315..7abfa552a 100644 --- a/tests/test_project_config_mgt.py +++ b/tests/test_project_config_mgt.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google Inc. +# Copyright 2024 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License.