Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
# 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.
"""

__all__ = [
'validate_keys',
'EmailPrivacyServerConfig',
'EmailPrivacyConfig',
]


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 EmailPrivacyServerConfig:
"""Represents email privacy 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 EmailPrivacyConfig constructor: {0}'.format(data))
self._data = data

@property
def enable_improved_email_privacy(self):
return self._data.get('enableImprovedEmailPrivacy', False)

class EmailPrivacyConfig:
"""Represents a email privacy configuration for tenant or project
"""

def __init__(self,
enable_improved_email_privacy: bool = False):
self.enable_improved_email_privacy: bool = enable_improved_email_privacy

def to_dict(self) -> dict:
data = {}
if self.enable_improved_email_privacy:
data['enableImprovedEmailPrivacy'] = self.enable_improved_email_privacy
return data

def validate(self):
"""Validates a given email_privacy_config object.

Raises:
ValueError: In case of an unsuccessful validation.
"""
validate_keys(
keys=vars(self).keys(),
valid_keys={'enable_improved_email_privacy'},
config_name='EmailPrivacyConfig')
if self.enable_improved_email_privacy is None:
raise ValueError(
'email_privacy_config.enable_improved_email_privacy must be specified')
if not isinstance(self.enable_improved_email_privacy, bool):
raise ValueError(
'enable_improved_email_privacy must be a valid bool.')

def build_server_request(self):
self.validate()
return self.to_dict()
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,8 @@
from firebase_admin import _utils
from firebase_admin.multi_factor_config_mgt import MultiFactorConfig
from firebase_admin.multi_factor_config_mgt import MultiFactorServerConfig
from firebase_admin.email_privacy_config_mgt import EmailPrivacyConfig
from firebase_admin.email_privacy_config_mgt import EmailPrivacyServerConfig

_PROJECT_CONFIG_MGT_ATTRIBUTE = '_project_config_mgt'

Expand DownExpand Up@@ -51,12 +53,14 @@ def get_project_config(app=None):
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.

def update_project_config(multi_factor_config: MultiFactorConfig = None,
email_privacy_config: EmailPrivacyConfig = None,
app=None):
"""Update the Project Config with the given options.
Args:
multi_factor_config: Updated multi-factor authentication configuration
(optional)
email_privacy_config: Updated Email Privacy configuration (optional).
app: An App instance (optional).
Returns:
Project: An updated ProjectConfig object.
Expand All@@ -65,7 +69,9 @@ def update_project_config(multi_factor_config: MultiFactorConfig = None, app=Non
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)
return project_config_mgt_service.update_project_config(multi_factor_config=multi_factor_config,
email_privacy_config=
email_privacy_config)


def _get_project_config_mgt_service(app):
Expand All@@ -89,6 +95,13 @@ def multi_factor_config(self):
return MultiFactorServerConfig(data)
return None

@property
def email_privacy_config(self):
data = self._data.get('emailPrivacyConfig')
if data:
return EmailPrivacyServerConfig(data)
return None

class _ProjectConfigManagementService:
"""Firebase project management service."""

Expand All@@ -112,14 +125,21 @@ def get_project_config(self) -> ProjectConfig:
else:
return ProjectConfig(body)

def update_project_config(self, multi_factor_config: MultiFactorConfig = None) -> ProjectConfig:
def update_project_config(self, multi_factor_config: MultiFactorConfig = None,
email_privacy_config: EmailPrivacyConfig = 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 email_privacy_config is not None:
if not isinstance(email_privacy_config, EmailPrivacyConfig):
raise ValueError('email_privacy_config must be of type EmailPrivacyConfig.')
payload['emailPrivacyConfig'] = email_privacy_config.build_server_request()

if not payload:
raise ValueError(
'At least one parameter must be specified for update.')
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,6 +30,8 @@
from firebase_admin import _utils
from firebase_admin.multi_factor_config_mgt import MultiFactorConfig
from firebase_admin.multi_factor_config_mgt import MultiFactorServerConfig
from firebase_admin.email_privacy_config_mgt import EmailPrivacyConfig
from firebase_admin.email_privacy_config_mgt import EmailPrivacyServerConfig


_TENANT_MGT_ATTRIBUTE = '_tenant_mgt'
Expand DownExpand Up@@ -94,7 +96,8 @@ 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: MultiFactorConfig = None,
email_privacy_config: EmailPrivacyConfig = None, app=None):
"""Creates a new tenant from the given options.

Args:
Expand All@@ -105,6 +108,7 @@ def create_tenant(
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).
email_privacy_config: An email privacy configuration to add to the tenant (optional).
app: An App instance (optional).

Returns:
Expand All@@ -118,12 +122,14 @@ def create_tenant(
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,
multi_factor_config=multi_factor_config,)
multi_factor_config=multi_factor_config,
email_privacy_config=email_privacy_config)


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: MultiFactorConfig = None,
email_privacy_config: EmailPrivacyConfig = None, app=None):
"""Updates an existing tenant with the given options.

Args:
Expand All@@ -134,6 +140,7 @@ def update_tenant(
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).
email_privacy_config: An email privacy configuration to update for the tenant (optional).
app: An App instance (optional).

Returns:
Expand All@@ -148,7 +155,7 @@ def update_tenant(
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,
multi_factor_config=multi_factor_config)
multi_factor_config=multi_factor_config, email_privacy_config=email_privacy_config)


def delete_tenant(tenant_id, app=None):
Expand DownExpand Up@@ -244,6 +251,13 @@ def multi_factor_config(self):
return MultiFactorServerConfig(data)
return None

@property
def email_privacy_config(self):
data = self._data.get('emailPrivacyConfig')
if data:
return EmailPrivacyServerConfig(data)
return None


class _TenantManagementService:
"""Firebase tenant management service."""
Expand DownExpand Up@@ -290,7 +304,8 @@ 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: MultiFactorConfig = None,
email_privacy_config: EmailPrivacyConfig = None):
"""Creates a new tenant from the given parameters."""

payload = {'displayName': _validate_display_name(display_name)}
Expand All@@ -305,6 +320,10 @@ def create_tenant(
raise ValueError(
'multi_factor_config must be of type MultiFactorConfig.')
payload['mfaConfig'] = multi_factor_config.build_server_request()
if email_privacy_config is not None:
if not isinstance(email_privacy_config, EmailPrivacyConfig):
raise ValueError('email_privacy_config must be of type EmailPrivacyConfig.')
payload['emailPrivacyConfig'] = email_privacy_config.build_server_request()
try:
body = self.client.body('post', '/tenants', json=payload)
except requests.exceptions.RequestException as error:
Expand All@@ -315,7 +334,8 @@ 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: MultiFactorConfig = None,
email_privacy_config: EmailPrivacyConfig = 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.')
Expand All@@ -331,9 +351,12 @@ def update_tenant(
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.')
raise ValueError('multi_factor_config must be of type MultiFactorConfig.')
payload['mfaConfig'] = multi_factor_config.build_server_request()
if email_privacy_config is not None:
if not isinstance(email_privacy_config, EmailPrivacyConfig):
raise ValueError('email_privacy_config must be of type EmailPrivacyConfig.')
payload['emailPrivacyConfig'] = email_privacy_config.build_server_request()

if not payload:
raise ValueError(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,32 +14,15 @@

"""Integration tests for firebase_admin.project_config_mgt module."""

import pytest

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

@pytest.fixture(scope='module')
def sample_mfa_config():
mfa_config = {
'providerConfigs': [
{
'state': 'ENABLED',
'totpProviderConfig': {
'adjacentIntervals': ADJACENT_INTERVALS
}
}
]
}
return mfa_config

from firebase_admin.email_privacy_config_mgt import EmailPrivacyConfig
from firebase_admin.email_privacy_config_mgt import EmailPrivacyServerConfig

def test_update_project_config():
mfa_object = MultiFactorConfig(
Expand All@@ -52,14 +35,20 @@ def test_update_project_config():
)
]
)
project_config = update_project_config(multi_factor_config=mfa_object)
email_privacy_object = EmailPrivacyConfig(
enable_improved_email_privacy=True
)
project_config = update_project_config(
multi_factor_config=mfa_object, email_privacy_config=email_privacy_object)
_assert_multi_factor_config(project_config.multi_factor_config)
_assert_email_privacy_config(project_config.email_privacy_config)


def test_get_project():
project_config = get_project_config()
assert isinstance(project_config, ProjectConfig)
_assert_multi_factor_config(project_config.multi_factor_config)
_assert_email_privacy_config(project_config.email_privacy_config)

def _assert_multi_factor_config(multi_factor_config):
assert isinstance(multi_factor_config, MultiFactorServerConfig)
Expand All@@ -72,4 +61,8 @@ def _assert_multi_factor_config(multi_factor_config):
assert isinstance(provider_config.totp_provider_config,
MultiFactorServerConfig.ProviderServerConfig
.TOTPProviderServerConfig)
assert provider_config.totp_provider_config.adjacent_intervals == ADJACENT_INTERVALS
assert provider_config.totp_provider_config.adjacent_intervals == 5

def _assert_email_privacy_config(email_privacy_config):
assert isinstance(email_privacy_config, EmailPrivacyServerConfig)
assert email_privacy_config.enable_improved_email_privacy is True
Loading