Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fixed review comments
  • Loading branch information
hardikns committed Feb 8, 2019
commit 8e7df34fb01f489a08ec5ef470a4d0f515e9f679
67 changes: 1 addition & 66 deletions firebase_admin/_auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,73 +183,8 @@ def validate_custom_claims(custom_claims, required=False):
'Claim "{0}" is reserved, and must not be set.'.format(invalid_claims.pop()))
return claims_str

def validate_action_code_settings(settings):
""" Validates the provided action code settings for email link generation and
populates the REST api parameters.

settings - dict provided to build the ActionCodeSettings object
returns - dict of parameters to be passed for link gereration.
"""
if not isinstance(settings, dict):
raise ValueError('Invalid data argument: {0}. Must be a dictionary.'.format(settings))

parameters = {}
# Validate url
url = settings.get('url', None)
if url:
try:
parsed = urllib.parse.urlparse(url)
if not parsed.netloc:
raise ValueError('Malformed photo URL: "{0}".'.format(url))
parameters['continueUrl'] = url
except Exception:
raise ValueError('Malformed photo URL: "{0}".'.format(url))

# Validate boolean types
for field in ['handle_code_in_app', 'android_install_app']:
if not isinstance(settings.get(field, False), bool):
raise ValueError('Invalid value provided for {0}: {1}'.format(
field, settings.get(field, False)))

# Validate string types
for field in ['dynamic_link_domain', 'ios_bundle_id',
'android_package_name', 'android_minimum_version']:
if not isinstance(settings.get(field, ''), six.string_types):
raise ValueError('Invalid value provided for {0}: {1}'.format(
field, settings.get(field, False)))

# handle_code_in_app
handle_code_in_app = settings.get('handle_code_in_app', False)
if handle_code_in_app:
parameters['canHandleCodeInApp'] = handle_code_in_app

# dynamic_link_domain
dynamic_link_domain = settings.get('dynamic_link_domain', None)
if dynamic_link_domain:
parameters['dynamicLinkDomain'] = dynamic_link_domain

# ios_bundle_id
ios_bundle_id = settings.get('ios_bundle_id', None)
if ios_bundle_id:
parameters['iosBundleId'] = ios_bundle_id

# android_* attributes
android_package_name = settings.get('android_package_name', None)
android_minimum_version = settings.get('android_minimum_version', None)
android_install_app = settings.get('android_install_app', False)
if (android_minimum_version or android_install_app) and not android_package_name:
raise ValueError("Android package name is required when specifying other Android settings")

if android_package_name:
parameters['androidPackageName'] = android_package_name
if android_minimum_version:
parameters['androidMinimumVersion'] = android_minimum_version
if android_install_app:
parameters['androidInstallApp'] = android_install_app
return parameters

def validate_action_type(action_type):
if not action_type in VALID_ACTION_TYPE:
if action_type not in VALID_ACTION_TYPE:
raise ValueError('Invalid action type provided action_type: {0}. \
Valid values are {1}'.format(action_type, VALID_ACTION_TYPE))
return action_type
154 changes: 76 additions & 78 deletions firebase_admin/_user_mgt.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@

"""Firebase user management sub module."""

import copy
import json

import requests
import six
from six.moves import urllib

from firebase_admin import _auth_utils
from firebase_admin import _user_import
Expand Down Expand Up @@ -378,77 +378,75 @@ class ActionCodeSettings(object):
"""Contains required continue/state URL with optional Android and iOS settings.
Used when invoking the email action link generation APIs.
"""
KEYS = set(['url', 'handle_code_in_app', 'dynamic_link_domain', 'ios_bundle_id',
'android_package_name', 'android_minimum_version', 'android_install_app'])

def __init__(self, data=None):
super(ActionCodeSettings, self).__init__()
data = data or {}
if not isinstance(data, dict):
raise ValueError('Invalid data argument: {0}. Must be a dictionary.'.format(data))
if len(set(six.iterkeys(data)) - self.KEYS):
raise ValueError('Invalid settings provided: {0}. Valid dictionary \
have following keys {1}'.format(data, self.KEYS))
self._data = copy.deepcopy(data)

@property
def url(self):
return self._data.get('url', None)

@url.setter
def url(self, url):
self._data['url'] = url

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

@handle_code_in_app.setter
def handle_code_in_app(self, handle_code_in_app):
self._data['handle_code_in_app'] = handle_code_in_app

@property
def dynamic_link_domain(self):
return self._data.get('dynamic_link_domain', None)

@dynamic_link_domain.setter
def dynamic_link_domain(self, dynamic_link_domain):
self._data['dynamic_link_domain'] = dynamic_link_domain

@property
def ios_bundle_id(self):
return self._data.get('ios_bundle_id', None)

@ios_bundle_id.setter
def ios_bundle_id(self, ios_bundle_id):
self._data['ios_bundle_id'] = ios_bundle_id

@property
def android_package_name(self):
return self._data.get('android_package_name', None)

@android_package_name.setter
def android_package_name(self, android_package_name):
self._data['android_package_name'] = android_package_name

@property
def android_minimum_version(self):
return self._data.get('android_minimum_version', None)

@android_minimum_version.setter
def android_minimum_version(self, android_minimum_version):
self._data['android_minimum_version'] = android_minimum_version

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

@android_install_app.setter
def android_install_app(self, android_install_app):
self._data['android_install_app'] = android_install_app
def __init__(self, url, handle_code_in_app=None, dynamic_link_domain=None, ios_bundle_id=None,
android_package_name=None, android_install_app=None, android_minimum_version=None):
self.url = url
self.handle_code_in_app = handle_code_in_app
self.dynamic_link_domain = dynamic_link_domain
self.ios_bundle_id = ios_bundle_id
self.android_package_name = android_package_name
self.android_install_app = android_install_app
self.android_minimum_version = android_minimum_version

def encode_action_code_settings(settings):
""" Validates the provided action code settings for email link generation and
populates the REST api parameters.

settings - ``ActionCodeSettings`` object provided to be encoded
returns - dict of parameters to be passed for link gereration.
"""
if not isinstance(settings, ActionCodeSettings):
raise ValueError('Invalid data argument: {0}. Must be a dictionary.'.format(settings))

def to_parameters_dict(self):
return _auth_utils.validate_action_code_settings(self._data)
parameters = {}
# Validate url
if settings.url:
try:
parsed = urllib.parse.urlparse(settings.url)
if not parsed.netloc:
raise ValueError('Malformed dynamic action links url: "{0}".'.format(settings.url))
parameters['continueUrl'] = settings.url
except Exception:
raise ValueError('Malformed dynamic action links url: "{0}".'.format(settings.url))

# Validate boolean types
for field in ['handle_code_in_app', 'android_install_app']:
value = getattr(settings, field, None)
if value != None and not isinstance(value, bool):
raise ValueError('Invalid value provided for {0}: {1}'.format(field, value))

# Validate string types
for field in ['dynamic_link_domain', 'ios_bundle_id',
'android_package_name', 'android_minimum_version']:
value = getattr(settings, field, None)
if value != None and not isinstance(value, six.string_types):
raise ValueError('Invalid value provided for {0}: {1}'.format(field, value))

# handle_code_in_app
if settings.handle_code_in_app != None:
parameters['canHandleCodeInApp'] = settings.handle_code_in_app

# dynamic_link_domain
if settings.dynamic_link_domain != None:
parameters['dynamicLinkDomain'] = settings.dynamic_link_domain

# ios_bundle_id
if settings.ios_bundle_id:
parameters['iosBundleId'] = settings.ios_bundle_id

# android_* attributes
if (settings.android_minimum_version or settings.android_install_app) \
and not settings.android_package_name:
raise ValueError("Android package name is required when specifying other Android settings")

if settings.android_package_name:
parameters['androidPackageName'] = settings.android_package_name
if settings.android_minimum_version:
parameters['androidMinimumVersion'] = settings.android_minimum_version
if settings.android_install_app:
parameters['androidInstallApp'] = settings.android_install_app
return parameters

class UserManager(object):
"""Provides methods for interacting with the Google Identity Toolkit."""
Expand Down Expand Up @@ -614,33 +612,33 @@ def import_users(self, users, hash_alg=None):
raise ApiCallError(USER_IMPORT_ERROR, 'Failed to import users.')
return response

def generate_email_action_link(self, action_type, email, settings=None):
def generate_email_action_link(self, action_type, email, action_code_settings=None):
"""Fetches the email action links for types

Args:
action_type: String. Valid values ['VERIFY_EMAIL', 'EMAIL_SIGNIN', 'PASSWORD_RESET']
email: Email of the user for which the action is performed
settings: ``ActionCodeSettings`` object or dict (optional). Defines whether
action_code_settings: ``ActionCodeSettings`` object or dict (optional). Defines whether
the link is to be handled by a mobile app and the additional state information to be
passed in the deep link, etc.
Returns:
link_url: action url to be emailed to the user

Raises:
ApiCallError: If an error occurs while generating the link

ValueError: If the provided arguments are invalid
"""
payload = {
'requestType': _auth_utils.validate_action_type(action_type),
'email': _auth_utils.validate_email(email),
'returnOobLink': True
}

if settings and not isinstance(settings, ActionCodeSettings):
settings = ActionCodeSettings(settings)

if settings and isinstance(settings, ActionCodeSettings):
payload.update(settings.to_parameters_dict())
if action_code_settings:
if not isinstance(action_code_settings, ActionCodeSettings):
raise ValueError("'action_code_settings' parameter should be " + \
"of type ActionCodeSettings")
payload.update(encode_action_code_settings(action_code_settings))

try:
response = self._client.body('post', '/accounts:sendOobCode', json=payload)
Expand Down
23 changes: 13 additions & 10 deletions firebase_admin/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
'delete_user',
'generate_password_reset_link',
'generate_email_verification_link',
'generate_email_sign_in_link',
'generate_sign_in_with_email_link',
'get_user',
'get_user_by_email',
'get_user_by_phone_number',
Expand Down Expand Up @@ -453,13 +453,13 @@ def import_users(users, hash_alg=None, app=None):
except _user_mgt.ApiCallError as error:
raise AuthError(error.code, str(error), error.detail)

def generate_password_reset_link(email, settings=None, app=None):
def generate_password_reset_link(email, action_code_settings=None, app=None):
"""Generates the out-of-band email action link for password reset flows for the specified email
address.

Args:
email: The email of the user whose password is to be reset.
settings: dict or ``ActionCodeSettings`` instance (optional). Defines whether
action_code_settings: ``ActionCodeSettings`` instance (optional). Defines whether
the link is to be handled by a mobile app and the additional state information to be
passed in the deep link, etc.
app: An App instance (optional).
Expand All @@ -472,17 +472,18 @@ def generate_password_reset_link(email, settings=None, app=None):
"""
user_manager = _get_auth_service(app).user_manager
try:
return user_manager.generate_email_action_link('PASSWORD_RESET', email, settings)
return user_manager.generate_email_action_link('PASSWORD_RESET', email,
action_code_settings=action_code_settings)
except _user_mgt.ApiCallError as error:
raise AuthError(error.code, str(error), error.detail)

def generate_email_verification_link(email, settings=None, app=None):
def generate_email_verification_link(email, action_code_settings=None, app=None):
"""Generates the out-of-band email action link for email verification flows for the specified
email address.

Args:
email: The email of the user to be verified.
settings: dict or ``ActionCodeSettings`` instance (optional). Defines whether
action_code_settings: ``ActionCodeSettings`` instance (optional). Defines whether
the link is to be handled by a mobile app and the additional state information to be
passed in the deep link, etc.
app: An App instance (optional).
Expand All @@ -495,17 +496,18 @@ def generate_email_verification_link(email, settings=None, app=None):
"""
user_manager = _get_auth_service(app).user_manager
try:
return user_manager.generate_email_action_link('VERIFY_EMAIL', email, settings)
return user_manager.generate_email_action_link('VERIFY_EMAIL', email,
action_code_settings=action_code_settings)
except _user_mgt.ApiCallError as error:
raise AuthError(error.code, str(error), error.detail)

def generate_email_sign_in_link(email, settings=None, app=None):
def generate_sign_in_with_email_link(email, action_code_settings, app=None):
"""Generates the out-of-band email action link for email link sign-in flows, using the action
code settings provided.

Args:
email: The email of the user signing in.
settings: dict or ``ActionCodeSettings`` instance (optional). Defines whether
action_code_settings: ``ActionCodeSettings`` instance. Defines whether
the link is to be handled by a mobile app and the additional state information to be
passed in the deep link, etc.
app: An App instance (optional).
Expand All @@ -518,7 +520,8 @@ def generate_email_sign_in_link(email, settings=None, app=None):
"""
user_manager = _get_auth_service(app).user_manager
try:
return user_manager.generate_email_action_link('EMAIL_SIGNIN', email, settings)
return user_manager.generate_email_action_link('EMAIL_SIGNIN', email,
action_code_settings=action_code_settings)
except _user_mgt.ApiCallError as error:
raise AuthError(error.code, str(error), error.detail)

Expand Down
17 changes: 5 additions & 12 deletions integration/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,10 +375,7 @@ def test_import_users_with_password(api_key):

@pytest.fixture
def action_code_settings():
data = {
'url': 'http://localhost',
}
return auth.ActionCodeSettings(data)
return auth.ActionCodeSettings('http://localhost')

def _validate_link_url(link):
assert isinstance(link, six.string_types)
Expand All @@ -392,23 +389,19 @@ def test_email_verification(new_user_with_params):
link = auth.generate_email_verification_link(new_user_with_params.email)
_validate_link_url(link)

def test_email_sign_in(new_user_with_params):
link = auth.generate_email_sign_in_link(new_user_with_params.email)
_validate_link_url(link)

def test_password_reset_with_settings(new_user_with_params, action_code_settings):
link = auth.generate_password_reset_link(new_user_with_params.email,
settings=action_code_settings)
action_code_settings=action_code_settings)
_validate_link_url(link)

def test_email_verification_with_settings(new_user_with_params, action_code_settings):
link = auth.generate_email_verification_link(new_user_with_params.email,
settings=action_code_settings)
action_code_settings=action_code_settings)
_validate_link_url(link)

def test_email_sign_in_with_settings(new_user_with_params, action_code_settings):
link = auth.generate_email_sign_in_link(new_user_with_params.email,
settings=action_code_settings)
link = auth.generate_sign_in_with_email_link(new_user_with_params.email,
action_code_settings=action_code_settings)
_validate_link_url(link)


Expand Down
Loading