Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(auth): Add email enumeration support in Project and Tenant config #683

Draft
wants to merge 10 commits into
base: mfa-totp
Choose a base branch
from
83 changes: 83 additions & 0 deletions firebase_admin/email_privacy_config_mgt.py
Original file line number Diff line number Diff 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()
30 changes: 25 additions & 5 deletions firebase_admin/project_config_mgt.py
Original file line number Diff line number Diff 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 Down Expand 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
39 changes: 31 additions & 8 deletions firebase_admin/tenant_mgt.py
Original file line number Diff line number Diff 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 Down Expand 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 Down Expand 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 Down Expand 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
35 changes: 14 additions & 21 deletions integration/test_project_config_mgt.py
Original file line number Diff line number Diff 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