From dcd9be5acdb2a50391f5b36c51032518905a2b12 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Tue, 14 Apr 2020 12:15:59 -0700 Subject: [PATCH 1/6] fix(auth): Integration tests for IdP management APIs --- integration/test_auth.py | 135 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/integration/test_auth.py b/integration/test_auth.py index 5d26dd9f1..bc384b25d 100644 --- a/integration/test_auth.py +++ b/integration/test_auth.py @@ -16,6 +16,7 @@ import base64 import datetime import random +import string import time from urllib import parse import uuid @@ -38,6 +39,30 @@ ACTION_LINK_CONTINUE_URL = 'http://localhost?a=1&b=5#f=1' +X509_CERTIFICATES = [ + ('-----BEGIN CERTIFICATE-----\nMIICZjCCAc+gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBQMQswCQYDVQQGEwJ1czE' + 'L\nMAkGA1UECAwCQ0ExDTALBgNVBAoMBEFjbWUxETAPBgNVBAMMCGFjbWUuY29tMRIw\nEAYDVQQHDAlTdW5ueXZhbGU' + 'wHhcNMTgxMjA2MDc1MTUxWhcNMjgxMjAzMDc1MTUx\nWjBQMQswCQYDVQQGEwJ1czELMAkGA1UECAwCQ0ExDTALBgNVB' + 'AoMBEFjbWUxETAP\nBgNVBAMMCGFjbWUuY29tMRIwEAYDVQQHDAlTdW5ueXZhbGUwgZ8wDQYJKoZIhvcN\nAQEBBQADg' + 'Y0AMIGJAoGBAKphmggjiVgqMLXyzvI7cKphscIIQ+wcv7Dld6MD4aKv\n7Jqr8ltujMxBUeY4LFEKw8Terb01snYpDot' + 'filaG6NxpF/GfVVmMalzwWp0mT8+H\nyzyPj89mRcozu17RwuooR6n1ofXjGcBE86lqC21UhA3WVgjPOLqB42rlE9gPn' + 'ZLB\nAgMBAAGjUDBOMB0GA1UdDgQWBBS0iM7WnbCNOnieOP1HIA+Oz/ML+zAfBgNVHSME\nGDAWgBS0iM7WnbCNOnieO' + 'P1HIA+Oz/ML+zAMBgNVHRMEBTADAQH/MA0GCSqGSIb3\nDQEBDQUAA4GBAF3jBgS+wP+K/jTupEQur6iaqS4UvXd//d4' + 'vo1MV06oTLQMTz+rP\nOSMDNwxzfaOn6vgYLKP/Dcy9dSTnSzgxLAxfKvDQZA0vE3udsw0Bd245MmX4+GOp\nlbrN99X' + 'P1u+lFxCSdMUzvQ/jW4ysw/Nq4JdJ0gPAyPvL6Qi/3mQdIQwx\n-----END CERTIFICATE-----\n'), + ('-----BEGIN CERTIFICATE-----\nMIICZjCCAc+gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBQMQswCQYDVQQGEwJ1czE' + 'L\nMAkGA1UECAwCQ0ExDTALBgNVBAoMBEFjbWUxETAPBgNVBAMMCGFjbWUuY29tMRIw\nEAYDVQQHDAlTdW5ueXZhbGU' + 'wHhcNMTgxMjA2MDc1ODE4WhcNMjgxMjAzMDc1ODE4\nWjBQMQswCQYDVQQGEwJ1czELMAkGA1UECAwCQ0ExDTALBgNVB' + 'AoMBEFjbWUxETAP\nBgNVBAMMCGFjbWUuY29tMRIwEAYDVQQHDAlTdW5ueXZhbGUwgZ8wDQYJKoZIhvcN\nAQEBBQADg' + 'Y0AMIGJAoGBAKuzYKfDZGA6DJgQru3wNUqv+S0hMZfP/jbp8ou/8UKu\nrNeX7cfCgt3yxoGCJYKmF6t5mvo76JY0MWw' + 'A53BxeP/oyXmJ93uHG5mFRAsVAUKs\ncVVb0Xi6ujxZGVdDWFV696L0BNOoHTfXmac6IBoZQzNNK4n1AATqwo+z7a0pf' + 'RrJ\nAgMBAAGjUDBOMB0GA1UdDgQWBBSKmi/ZKMuLN0ES7/jPa7q7jAjPiDAfBgNVHSME\nGDAWgBSKmi/ZKMuLN0ES7' + '/jPa7q7jAjPiDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3\nDQEBDQUAA4GBAAg2a2kSn05NiUOuWOHwPUjW3wQRsGxPXtb' + 'hWMhmNdCfKKteM2+/\nLd/jz5F3qkOgGQ3UDgr3SHEoWhnLaJMF4a2tm6vL2rEIfPEK81KhTTRxSsAgMVbU\nJXBz1md' + '6Ur0HlgQC7d1CHC8/xi2DDwHopLyxhogaZUxy9IaRxUEa2vJW\n-----END CERTIFICATE-----\n'), +] + + def _sign_in(custom_token, api_key): body = {'token' : custom_token.decode(), 'returnSecureToken' : True} params = {'key' : api_key} @@ -52,6 +77,10 @@ def _sign_in_with_password(email, password, api_key): resp.raise_for_status() return resp.json().get('idToken') +def _random_string(length=10): + letters = string.ascii_lowercase + return ''.join(random.choice(letters) for i in range(length)) + def _random_id(): random_id = str(uuid.uuid4()).lower().replace('-', '') email = 'test{0}@example.{1}.com'.format(random_id[:12], random_id[12:]) @@ -477,6 +506,112 @@ def test_email_sign_in_with_settings(new_user_email_unverified, api_key): assert id_token is not None and len(id_token) > 0 assert auth.get_user(new_user_email_unverified.uid).email_verified +def test_oidc_provider_config(): + provider_id = 'oidc.{0}'.format(_random_string()) + # Create OIDC provider config + provider_config = auth.create_oidc_provider_config( + provider_id=provider_id, client_id='OIDC_CLIENT_ID', issuer='https://oidc.com/issuer', + display_name='OIDC_DISPLAY_NAME', enabled=True) + + try: + _check_oidc_provider_config(provider_config, provider_id) + + # Get OIDC provider config + provider_config = auth.get_oidc_provider_config(provider_id) + _check_oidc_provider_config(provider_config, provider_id) + + # List OIDC provider configs + page = auth.list_oidc_provider_configs() + result = None + for provider_config in page.iterate_all(): + if provider_config.provider_id == provider_id: + result = provider_config + break + _check_oidc_provider_config(result, provider_id) + + # Update OIDC provider config + provider_config = auth.update_oidc_provider_config( + provider_id, client_id='UPDATED_OIDC_CLIENT_ID', + display_name='UPDATED_OIDC_DISPLAY_NAME') + assert provider_config.client_id == 'UPDATED_OIDC_CLIENT_ID' + assert provider_config.display_name == 'UPDATED_OIDC_DISPLAY_NAME' + + # Delete OIDC provider config + auth.delete_oidc_provider_config(provider_id) + with pytest.raises(auth.ConfigurationNotFoundError): + auth.get_oidc_provider_config(provider_id) + provider_id = None + finally: + if provider_id: + auth.delete_oidc_provider_config(provider_id) + +def test_saml_provider_config(): + provider_id = 'saml.{0}'.format(_random_string()) + # Create SAML provider config + provider_config = auth.create_saml_provider_config( + provider_id=provider_id, idp_entity_id='IDP_ENTITY_ID', + sso_url='https://example.com/login', + x509_certificates=[X509_CERTIFICATES[0]], + rp_entity_id='RP_ENTITY_ID', + callback_url='https://projectId.firebaseapp.com/__/auth/handler', + display_name='SAML_DISPLAY_NAME', enabled=True) + + try: + _check_saml_provider_config(provider_config, provider_id) + + # Get SAML provider config + provider_config = auth.get_saml_provider_config(provider_id) + _check_saml_provider_config(provider_config, provider_id) + + # List SAML provider configs + page = auth.list_saml_provider_configs() + result = None + for provider_config in page.iterate_all(): + if provider_config.provider_id == provider_id: + result = provider_config + break + _check_saml_provider_config(result, provider_id) + + # Update SAML provider config + provider_config = auth.update_saml_provider_config( + provider_id, idp_entity_id='UPDATED_IDP_ENTITY_ID', + x509_certificates=[X509_CERTIFICATES[1]], + display_name='UPDATED_SAML_DISPLAY_NAME') + assert provider_config.idp_entity_id == 'UPDATED_IDP_ENTITY_ID' + assert provider_config.x509_certificates == [X509_CERTIFICATES[1]] + assert provider_config.display_name == 'UPDATED_SAML_DISPLAY_NAME' + + # Delete SAML provider config + auth.delete_saml_provider_config(provider_id) + with pytest.raises(auth.ConfigurationNotFoundError): + auth.get_saml_provider_config(provider_id) + provider_id = None + finally: + if provider_id: + auth.delete_saml_provider_config(provider_id) + + +def _check_oidc_provider_config(provider_config, provider_id): + assert isinstance(provider_config, auth.OIDCProviderConfig) + assert provider_config.provider_id == provider_id + assert provider_config.client_id == 'OIDC_CLIENT_ID' + assert provider_config.issuer == 'https://oidc.com/issuer' + assert provider_config.display_name == 'OIDC_DISPLAY_NAME' + assert provider_config.enabled + + +def _check_saml_provider_config(provider_config, provider_id): + assert isinstance(provider_config, auth.SAMLProviderConfig) + assert provider_config.provider_id == provider_id + assert provider_config.idp_entity_id == 'IDP_ENTITY_ID' + assert provider_config.sso_url == 'https://example.com/login' + assert provider_config.x509_certificates == [X509_CERTIFICATES[0]] + assert provider_config.rp_entity_id == 'RP_ENTITY_ID' + assert provider_config.callback_url == 'https://projectId.firebaseapp.com/__/auth/handler' + assert provider_config.display_name == 'SAML_DISPLAY_NAME' + assert provider_config.enabled + + class CredentialWrapper(credentials.Base): """A custom Firebase credential that wraps an OAuth2 token.""" From 3bca6266f0df5c1ae4446c9ffff6c29b4d119dba Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Tue, 14 Apr 2020 15:48:36 -0700 Subject: [PATCH 2/6] More integration tests for tenant_mgt module; Made display_name required for tenants --- firebase_admin/_auth_providers.py | 2 +- firebase_admin/_user_mgt.py | 4 + firebase_admin/tenant_mgt.py | 32 +++-- integration/test_auth.py | 219 ++++++++++++++++++------------ integration/test_tenant_mgt.py | 211 ++++++++++++++++++++++++++++ tests/test_tenant_mgt.py | 51 ++++--- 6 files changed, 405 insertions(+), 114 deletions(-) create mode 100644 integration/test_tenant_mgt.py diff --git a/firebase_admin/_auth_providers.py b/firebase_admin/_auth_providers.py index 28c25ae49..d1f64c382 100644 --- a/firebase_admin/_auth_providers.py +++ b/firebase_admin/_auth_providers.py @@ -42,7 +42,7 @@ def display_name(self): @property def enabled(self): - return self._data['enabled'] + return self._data.get('enabled', False) class OIDCProviderConfig(ProviderConfig): diff --git a/firebase_admin/_user_mgt.py b/firebase_admin/_user_mgt.py index 8b0a81adf..d7a017b99 100644 --- a/firebase_admin/_user_mgt.py +++ b/firebase_admin/_user_mgt.py @@ -244,6 +244,10 @@ def custom_claims(self): return parsed return None + @property + def tenant_id(self): + return self._data.get('tenantId') + class ExportedUserRecord(UserRecord): """Contains metadata associated with a user including password hash and salt.""" diff --git a/firebase_admin/tenant_mgt.py b/firebase_admin/tenant_mgt.py index 1fb64d01c..0bacc65fe 100644 --- a/firebase_admin/tenant_mgt.py +++ b/firebase_admin/tenant_mgt.py @@ -18,6 +18,7 @@ Google Cloud Identity Platform (GCIP) instance. """ +import re import threading import requests @@ -31,6 +32,7 @@ _TENANT_MGT_ATTRIBUTE = '_tenant_mgt' _MAX_LIST_TENANTS_RESULTS = 100 +_DISPLAY_NAME_PATTERN = re.compile('^[a-zA-Z][a-zA-Z0-9-]{3,19}$') __all__ = [ @@ -89,15 +91,16 @@ def get_tenant(tenant_id, app=None): def create_tenant( - display_name=None, 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, app=None): """Creates a new tenant from the given options. Args: - display_name: Display name string for the new tenant (optional). + display_name: Display name string for the new tenant. Must begin with a letter and contain + only letters, digits and hyphens. Length must be between 4 and 20. allow_password_sign_up: A boolean indicating whether to enable or disable the email sign-in - provider. + provider (optional). 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. + sign-in (optional). Disabling this makes the password required for email sign-in. app: An App instance (optional). Returns: @@ -120,7 +123,7 @@ def update_tenant( Args: tenant_id: ID of the tenant to update. - display_name: Display name string for the new tenant (optional). + display_name: Updated display name string for the tenant (optional). allow_password_sign_up: A boolean indicating whether to enable or disable the email sign-in provider. enable_email_link_sign_in: A boolean indicating whether to enable or disable email link @@ -269,11 +272,10 @@ def get_tenant(self, tenant_id): return Tenant(body) def create_tenant( - self, display_name=None, 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): """Creates a new tenant from the given parameters.""" - payload = {} - if display_name is not None: - payload['displayName'] = _auth_utils.validate_string(display_name, 'displayName') + + payload = {'displayName': _validate_display_name(display_name)} if allow_password_sign_up is not None: payload['allowPasswordSignup'] = _auth_utils.validate_boolean( allow_password_sign_up, 'allowPasswordSignup') @@ -297,7 +299,7 @@ def update_tenant( payload = {} if display_name is not None: - payload['displayName'] = _auth_utils.validate_string(display_name, 'displayName') + payload['displayName'] = _validate_display_name(display_name) if allow_password_sign_up is not None: payload['allowPasswordSignup'] = _auth_utils.validate_boolean( allow_password_sign_up, 'allowPasswordSignup') @@ -431,3 +433,13 @@ def __next__(self): def __iter__(self): return self + + +def _validate_display_name(display_name): + if not isinstance(display_name, str): + raise ValueError('Invalid type for displayName') + if not _DISPLAY_NAME_PATTERN.search(display_name): + raise ValueError( + 'displayName must start with a letter and only consist of letters, digits and ' + 'hyphens with 4-20 characters.') + return display_name diff --git a/integration/test_auth.py b/integration/test_auth.py index bc384b25d..cfd775016 100644 --- a/integration/test_auth.py +++ b/integration/test_auth.py @@ -506,110 +506,161 @@ def test_email_sign_in_with_settings(new_user_email_unverified, api_key): assert id_token is not None and len(id_token) > 0 assert auth.get_user(new_user_email_unverified.uid).email_verified -def test_oidc_provider_config(): - provider_id = 'oidc.{0}'.format(_random_string()) - # Create OIDC provider config - provider_config = auth.create_oidc_provider_config( - provider_id=provider_id, client_id='OIDC_CLIENT_ID', issuer='https://oidc.com/issuer', - display_name='OIDC_DISPLAY_NAME', enabled=True) +@pytest.fixture(scope='module') +def oidc_provider(): + provider_config = _create_oidc_provider_config() + yield provider_config + auth.delete_oidc_provider_config(provider_config.provider_id) + + +def test_create_oidc_provider_config(oidc_provider): + assert isinstance(oidc_provider, auth.OIDCProviderConfig) + assert oidc_provider.client_id == 'OIDC_CLIENT_ID' + assert oidc_provider.issuer == 'https://oidc.com/issuer' + assert oidc_provider.display_name == 'OIDC_DISPLAY_NAME' + assert oidc_provider.enabled is True + + +def test_get_oidc_provider_config(oidc_provider): + provider_config = auth.get_oidc_provider_config(oidc_provider.provider_id) + assert isinstance(provider_config, auth.OIDCProviderConfig) + assert provider_config.provider_id == oidc_provider.provider_id + assert provider_config.client_id == 'OIDC_CLIENT_ID' + assert provider_config.issuer == 'https://oidc.com/issuer' + assert provider_config.display_name == 'OIDC_DISPLAY_NAME' + assert provider_config.enabled is True + + +def test_list_oidc_provider_configs(oidc_provider): + page = auth.list_oidc_provider_configs() + result = None + for provider_config in page.iterate_all(): + if provider_config.provider_id == oidc_provider.provider_id: + result = provider_config + break + + assert result is not None + + +def test_update_oidc_provider_config(): + provider_config = _create_oidc_provider_config() try: - _check_oidc_provider_config(provider_config, provider_id) - - # Get OIDC provider config - provider_config = auth.get_oidc_provider_config(provider_id) - _check_oidc_provider_config(provider_config, provider_id) - - # List OIDC provider configs - page = auth.list_oidc_provider_configs() - result = None - for provider_config in page.iterate_all(): - if provider_config.provider_id == provider_id: - result = provider_config - break - _check_oidc_provider_config(result, provider_id) - - # Update OIDC provider config provider_config = auth.update_oidc_provider_config( - provider_id, client_id='UPDATED_OIDC_CLIENT_ID', - display_name='UPDATED_OIDC_DISPLAY_NAME') + provider_config.provider_id, + client_id='UPDATED_OIDC_CLIENT_ID', + issuer='https://oidc.com/updated_issuer', + display_name='UPDATED_OIDC_DISPLAY_NAME', + enabled=False) assert provider_config.client_id == 'UPDATED_OIDC_CLIENT_ID' + assert provider_config.issuer == 'https://oidc.com/updated_issuer' assert provider_config.display_name == 'UPDATED_OIDC_DISPLAY_NAME' - - # Delete OIDC provider config - auth.delete_oidc_provider_config(provider_id) - with pytest.raises(auth.ConfigurationNotFoundError): - auth.get_oidc_provider_config(provider_id) - provider_id = None + assert provider_config.enabled is False finally: - if provider_id: - auth.delete_oidc_provider_config(provider_id) + auth.delete_oidc_provider_config(provider_config.provider_id) -def test_saml_provider_config(): - provider_id = 'saml.{0}'.format(_random_string()) - # Create SAML provider config - provider_config = auth.create_saml_provider_config( - provider_id=provider_id, idp_entity_id='IDP_ENTITY_ID', - sso_url='https://example.com/login', - x509_certificates=[X509_CERTIFICATES[0]], - rp_entity_id='RP_ENTITY_ID', - callback_url='https://projectId.firebaseapp.com/__/auth/handler', - display_name='SAML_DISPLAY_NAME', enabled=True) - try: - _check_saml_provider_config(provider_config, provider_id) - - # Get SAML provider config - provider_config = auth.get_saml_provider_config(provider_id) - _check_saml_provider_config(provider_config, provider_id) - - # List SAML provider configs - page = auth.list_saml_provider_configs() - result = None - for provider_config in page.iterate_all(): - if provider_config.provider_id == provider_id: - result = provider_config - break - _check_saml_provider_config(result, provider_id) - - # Update SAML provider config - provider_config = auth.update_saml_provider_config( - provider_id, idp_entity_id='UPDATED_IDP_ENTITY_ID', - x509_certificates=[X509_CERTIFICATES[1]], - display_name='UPDATED_SAML_DISPLAY_NAME') - assert provider_config.idp_entity_id == 'UPDATED_IDP_ENTITY_ID' - assert provider_config.x509_certificates == [X509_CERTIFICATES[1]] - assert provider_config.display_name == 'UPDATED_SAML_DISPLAY_NAME' +def test_delete_oidc_provider_config(): + provider_config = _create_oidc_provider_config() + auth.delete_oidc_provider_config(provider_config.provider_id) + with pytest.raises(auth.ConfigurationNotFoundError): + auth.get_oidc_provider_config(provider_config.provider_id) - # Delete SAML provider config - auth.delete_saml_provider_config(provider_id) - with pytest.raises(auth.ConfigurationNotFoundError): - auth.get_saml_provider_config(provider_id) - provider_id = None - finally: - if provider_id: - auth.delete_saml_provider_config(provider_id) +@pytest.fixture(scope='module') +def saml_provider(): + provider_config = _create_saml_provider_config() + yield provider_config + auth.delete_saml_provider_config(provider_config.provider_id) -def _check_oidc_provider_config(provider_config, provider_id): - assert isinstance(provider_config, auth.OIDCProviderConfig) - assert provider_config.provider_id == provider_id - assert provider_config.client_id == 'OIDC_CLIENT_ID' - assert provider_config.issuer == 'https://oidc.com/issuer' - assert provider_config.display_name == 'OIDC_DISPLAY_NAME' - assert provider_config.enabled + +def test_create_saml_provider_config(saml_provider): + assert isinstance(saml_provider, auth.SAMLProviderConfig) + assert saml_provider.idp_entity_id == 'IDP_ENTITY_ID' + assert saml_provider.sso_url == 'https://example.com/login' + assert saml_provider.x509_certificates == [X509_CERTIFICATES[0]] + assert saml_provider.rp_entity_id == 'RP_ENTITY_ID' + assert saml_provider.callback_url == 'https://projectId.firebaseapp.com/__/auth/handler' + assert saml_provider.display_name == 'SAML_DISPLAY_NAME' + assert saml_provider.enabled is True -def _check_saml_provider_config(provider_config, provider_id): +def test_get_saml_provider_config(saml_provider): + provider_config = auth.get_saml_provider_config(saml_provider.provider_id) assert isinstance(provider_config, auth.SAMLProviderConfig) - assert provider_config.provider_id == provider_id + assert provider_config.provider_id == saml_provider.provider_id assert provider_config.idp_entity_id == 'IDP_ENTITY_ID' assert provider_config.sso_url == 'https://example.com/login' assert provider_config.x509_certificates == [X509_CERTIFICATES[0]] assert provider_config.rp_entity_id == 'RP_ENTITY_ID' assert provider_config.callback_url == 'https://projectId.firebaseapp.com/__/auth/handler' assert provider_config.display_name == 'SAML_DISPLAY_NAME' - assert provider_config.enabled + assert provider_config.enabled is True + + +def test_list_saml_provider_configs(saml_provider): + page = auth.list_saml_provider_configs() + result = None + for provider_config in page.iterate_all(): + if provider_config.provider_id == saml_provider.provider_id: + result = provider_config + break + + assert result is not None + + +def test_update_saml_provider_config(): + provider_config = _create_saml_provider_config() + try: + provider_config = auth.update_saml_provider_config( + provider_config.provider_id, + idp_entity_id='UPDATED_IDP_ENTITY_ID', + sso_url='https://example.com/updated_login', + x509_certificates=[X509_CERTIFICATES[1]], + rp_entity_id='UPDATED_RP_ENTITY_ID', + callback_url='https://updatedProjectId.firebaseapp.com/__/auth/handler', + display_name='UPDATED_SAML_DISPLAY_NAME', + enabled=False) + assert provider_config.idp_entity_id == 'UPDATED_IDP_ENTITY_ID' + assert provider_config.sso_url == 'https://example.com/updated_login' + assert provider_config.x509_certificates == [X509_CERTIFICATES[1]] + assert provider_config.rp_entity_id == 'UPDATED_RP_ENTITY_ID' + assert provider_config.callback_url == ('https://updatedProjectId.firebaseapp.com/' + '__/auth/handler') + assert provider_config.display_name == 'UPDATED_SAML_DISPLAY_NAME' + assert provider_config.enabled is False + finally: + auth.delete_saml_provider_config(provider_config.provider_id) + + +def test_delete_saml_provider_config(): + provider_config = _create_saml_provider_config() + auth.delete_saml_provider_config(provider_config.provider_id) + with pytest.raises(auth.ConfigurationNotFoundError): + auth.get_saml_provider_config(provider_config.provider_id) + + +def _create_oidc_provider_config(): + provider_id = 'oidc.{0}'.format(_random_string()) + return auth.create_oidc_provider_config( + provider_id=provider_id, + client_id='OIDC_CLIENT_ID', + issuer='https://oidc.com/issuer', + display_name='OIDC_DISPLAY_NAME', + enabled=True) + + +def _create_saml_provider_config(): + provider_id = 'saml.{0}'.format(_random_string()) + return auth.create_saml_provider_config( + provider_id=provider_id, + idp_entity_id='IDP_ENTITY_ID', + sso_url='https://example.com/login', + x509_certificates=[X509_CERTIFICATES[0]], + rp_entity_id='RP_ENTITY_ID', + callback_url='https://projectId.firebaseapp.com/__/auth/handler', + display_name='SAML_DISPLAY_NAME', + enabled=True) class CredentialWrapper(credentials.Base): diff --git a/integration/test_tenant_mgt.py b/integration/test_tenant_mgt.py new file mode 100644 index 000000000..272bbd143 --- /dev/null +++ b/integration/test_tenant_mgt.py @@ -0,0 +1,211 @@ +# Copyright 2020 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.tenant_mgt module.""" + +import random +import time +from urllib import parse +import uuid + +import pytest + +from firebase_admin import auth +from firebase_admin import tenant_mgt + + +ACTION_LINK_CONTINUE_URL = 'http://localhost?a=1&b=5#f=1' +ACTION_CODE_SETTINGS = auth.ActionCodeSettings(ACTION_LINK_CONTINUE_URL) + + +@pytest.fixture(scope='module') +def sample_tenant(): + tenant = tenant_mgt.create_tenant( + display_name='admin-python-tenant', + allow_password_sign_up=True, + enable_email_link_sign_in=True) + yield tenant + tenant_mgt.delete_tenant(tenant.tenant_id) + + +@pytest.fixture(scope='module') +def tenant_user(sample_tenant): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + email = _random_email() + user = client.create_user(email=email) + yield user + client.delete_user(user.uid) + + +def test_get_tenant(sample_tenant): + tenant = tenant_mgt.get_tenant(sample_tenant.tenant_id) + assert isinstance(tenant, tenant_mgt.Tenant) + assert tenant.tenant_id == sample_tenant.tenant_id + assert tenant.display_name == 'admin-python-tenant' + assert tenant.allow_password_sign_up is True + assert tenant.enable_email_link_sign_in is True + + +def test_list_tenants(sample_tenant): + page = tenant_mgt.list_tenants() + result = None + for tenant in page.iterate_all(): + if tenant.tenant_id == sample_tenant.tenant_id: + result = tenant + break + assert isinstance(result, tenant_mgt.Tenant) + assert result.tenant_id == sample_tenant.tenant_id + assert result.display_name == 'admin-python-tenant' + assert result.allow_password_sign_up is True + assert result.enable_email_link_sign_in is True + + +def test_update_tenant(): + tenant = tenant_mgt.create_tenant( + display_name='py-update-test', allow_password_sign_up=True, enable_email_link_sign_in=True) + try: + tenant = tenant_mgt.update_tenant( + tenant.tenant_id, display_name='updated-py-tenant', allow_password_sign_up=False, + enable_email_link_sign_in=False) + assert isinstance(tenant, tenant_mgt.Tenant) + assert tenant.tenant_id == tenant.tenant_id + assert tenant.display_name == 'updated-py-tenant' + assert tenant.allow_password_sign_up is False + assert tenant.enable_email_link_sign_in is False + finally: + tenant_mgt.delete_tenant(tenant.tenant_id) + + +def test_delete_tenant(): + tenant = tenant_mgt.create_tenant(display_name='py-delete-test') + tenant_mgt.delete_tenant(tenant.tenant_id) + with pytest.raises(tenant_mgt.TenantNotFoundError): + tenant_mgt.get_tenant(tenant.tenant_id) + + +def test_auth_for_client(sample_tenant): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + assert isinstance(client, auth.Client) + assert client.tenant_id == sample_tenant.tenant_id + + +def test_create_user(sample_tenant, tenant_user): + assert tenant_user.tenant_id == sample_tenant.tenant_id + + +def test_update_user(sample_tenant): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + user = client.create_user() + try: + email = _random_email() + phone = _random_phone() + user = client.update_user(user.uid, email=email, phone_number=phone) + assert user.tenant_id == sample_tenant.tenant_id + assert user.email == email + assert user.phone_number == phone + finally: + client.delete_user(user.uid) + + +def test_get_user(sample_tenant, tenant_user): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + user = client.get_user(tenant_user.uid) + assert user.uid == tenant_user.uid + assert user.tenant_id == sample_tenant.tenant_id + + +def test_list_users(sample_tenant, tenant_user): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + page = client.list_users() + result = None + for user in page.iterate_all(): + if user.uid == tenant_user.uid: + result = user + break + assert result.tenant_id == sample_tenant.tenant_id + + +def test_set_custom_user_claims(sample_tenant, tenant_user): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + client.set_custom_user_claims(tenant_user.uid, {'premium': True}) + user = client.get_user(tenant_user.uid) + assert user.custom_claims == {'premium': True} + + +def test_delete_user(sample_tenant): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + user = client.create_user() + client.delete_user(user.uid) + with pytest.raises(auth.UserNotFoundError): + client.get_user(user.uid) + + +def test_revoke_refresh_tokens(sample_tenant, tenant_user): + valid_since = int(time.time()) + time.sleep(1) + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + client.revoke_refresh_tokens(tenant_user.uid) + user = client.get_user(tenant_user.uid) + assert user.tokens_valid_after_timestamp > valid_since + + +def test_password_reset_link(sample_tenant, tenant_user): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + link = client.generate_password_reset_link(tenant_user.email, ACTION_CODE_SETTINGS) + assert _tenant_id_from_link(link) == sample_tenant.tenant_id + + +def test_email_verification_link(sample_tenant, tenant_user): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + link = client.generate_email_verification_link(tenant_user.email, ACTION_CODE_SETTINGS) + assert _tenant_id_from_link(link) == sample_tenant.tenant_id + + +def test_sign_in_with_email_link(sample_tenant, tenant_user): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + link = client.generate_sign_in_with_email_link(tenant_user.email, ACTION_CODE_SETTINGS) + assert _tenant_id_from_link(link) == sample_tenant.tenant_id + + +def test_import_users(sample_tenant): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + user = auth.ImportUserRecord( + uid=_random_uid(), email=_random_email()) + result = client.import_users([user]) + try: + assert result.success_count == 1 + assert result.failure_count == 0 + saved_user = client.get_user(user.uid) + assert saved_user.email == user.email + finally: + client.delete_user(user.uid) + + +def _random_uid(): + return str(uuid.uuid4()).lower().replace('-', '') + + +def _random_email(): + random_id = str(uuid.uuid4()).lower().replace('-', '') + return 'test{0}@example.{1}.com'.format(random_id[:12], random_id[12:]) + + +def _random_phone(): + return '+1' + ''.join([str(random.randint(0, 9)) for _ in range(0, 10)]) + + +def _tenant_id_from_link(link): + query = parse.urlparse(link).query + parsed_query = parse.parse_qs(query) + return parsed_query['tenantId'][0] diff --git a/tests/test_tenant_mgt.py b/tests/test_tenant_mgt.py index cb2ca7724..0e615429e 100644 --- a/tests/test_tenant_mgt.py +++ b/tests/test_tenant_mgt.py @@ -211,32 +211,40 @@ def test_tenant_not_found(self, tenant_mgt_app): class TestCreateTenant: @pytest.mark.parametrize('display_name', [True, False, 1, 0, list(), tuple(), dict()]) - def test_invalid_display_name(self, display_name, tenant_mgt_app): + def test_invalid_display_name_type(self, display_name, tenant_mgt_app): with pytest.raises(ValueError) as excinfo: tenant_mgt.create_tenant(display_name=display_name, app=tenant_mgt_app) assert str(excinfo.value).startswith('Invalid type for displayName') + @pytest.mark.parametrize('display_name', ['', 'foo', '1test', 'foo bar']) + def test_invalid_display_name_value(self, display_name, tenant_mgt_app): + with pytest.raises(ValueError) as excinfo: + tenant_mgt.create_tenant(display_name=display_name, app=tenant_mgt_app) + assert str(excinfo.value).startswith('displayName must start') + @pytest.mark.parametrize('allow', INVALID_BOOLEANS) def test_invalid_allow_password_sign_up(self, allow, tenant_mgt_app): with pytest.raises(ValueError) as excinfo: - tenant_mgt.create_tenant(allow_password_sign_up=allow, app=tenant_mgt_app) + tenant_mgt.create_tenant( + display_name='test', allow_password_sign_up=allow, app=tenant_mgt_app) assert str(excinfo.value).startswith('Invalid type for allowPasswordSignup') @pytest.mark.parametrize('enable', INVALID_BOOLEANS) def test_invalid_enable_email_link_sign_in(self, enable, tenant_mgt_app): with pytest.raises(ValueError) as excinfo: - tenant_mgt.create_tenant(enable_email_link_sign_in=enable, app=tenant_mgt_app) + tenant_mgt.create_tenant( + display_name='test', enable_email_link_sign_in=enable, app=tenant_mgt_app) assert str(excinfo.value).startswith('Invalid type for enableEmailLinkSignin') def test_create_tenant(self, tenant_mgt_app): _, recorder = _instrument_tenant_mgt(tenant_mgt_app, 200, GET_TENANT_RESPONSE) tenant = tenant_mgt.create_tenant( - display_name='My Tenant', allow_password_sign_up=True, enable_email_link_sign_in=True, + display_name='My-Tenant', allow_password_sign_up=True, enable_email_link_sign_in=True, app=tenant_mgt_app) _assert_tenant(tenant) self._assert_request(recorder, { - 'displayName': 'My Tenant', + 'displayName': 'My-Tenant', 'allowPasswordSignup': True, 'enableEmailLinkSignin': True, }) @@ -244,27 +252,27 @@ def test_create_tenant(self, tenant_mgt_app): def test_create_tenant_false_values(self, tenant_mgt_app): _, recorder = _instrument_tenant_mgt(tenant_mgt_app, 200, GET_TENANT_RESPONSE) tenant = tenant_mgt.create_tenant( - display_name='', allow_password_sign_up=False, enable_email_link_sign_in=False, + display_name='test', allow_password_sign_up=False, enable_email_link_sign_in=False, app=tenant_mgt_app) _assert_tenant(tenant) self._assert_request(recorder, { - 'displayName': '', + 'displayName': 'test', 'allowPasswordSignup': False, 'enableEmailLinkSignin': False, }) def test_create_tenant_minimal(self, tenant_mgt_app): _, recorder = _instrument_tenant_mgt(tenant_mgt_app, 200, GET_TENANT_RESPONSE) - tenant = tenant_mgt.create_tenant(app=tenant_mgt_app) + tenant = tenant_mgt.create_tenant(display_name='test', app=tenant_mgt_app) _assert_tenant(tenant) - self._assert_request(recorder, {}) + self._assert_request(recorder, {'displayName': 'test'}) def test_error(self, tenant_mgt_app): _instrument_tenant_mgt(tenant_mgt_app, 500, '{}') with pytest.raises(exceptions.InternalError) as excinfo: - tenant_mgt.create_tenant(app=tenant_mgt_app) + tenant_mgt.create_tenant(display_name='test', app=tenant_mgt_app) error_msg = 'Unexpected error response: {}' assert excinfo.value.code == exceptions.INTERNAL @@ -290,11 +298,17 @@ def test_invalid_tenant_id(self, tenant_id, tenant_mgt_app): assert str(excinfo.value).startswith('Tenant ID must be a non-empty string') @pytest.mark.parametrize('display_name', [True, False, 1, 0, list(), tuple(), dict()]) - def test_invalid_display_name(self, display_name, tenant_mgt_app): + def test_invalid_display_name_type(self, display_name, tenant_mgt_app): with pytest.raises(ValueError) as excinfo: tenant_mgt.update_tenant('tenant-id', display_name=display_name, app=tenant_mgt_app) assert str(excinfo.value).startswith('Invalid type for displayName') + @pytest.mark.parametrize('display_name', ['', 'foo', '1test', 'foo bar']) + def test_invalid_display_name_value(self, display_name, tenant_mgt_app): + with pytest.raises(ValueError) as excinfo: + tenant_mgt.update_tenant('tenant-id', display_name=display_name, app=tenant_mgt_app) + assert str(excinfo.value).startswith('displayName must start') + @pytest.mark.parametrize('allow', INVALID_BOOLEANS) def test_invalid_allow_password_sign_up(self, allow, tenant_mgt_app): with pytest.raises(ValueError) as excinfo: @@ -316,12 +330,12 @@ 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) tenant = tenant_mgt.update_tenant( - 'tenant-id', display_name='My Tenant', allow_password_sign_up=True, + 'tenant-id', display_name='My-Tenant', allow_password_sign_up=True, enable_email_link_sign_in=True, app=tenant_mgt_app) _assert_tenant(tenant) body = { - 'displayName': 'My Tenant', + 'displayName': 'My-Tenant', 'allowPasswordSignup': True, 'enableEmailLinkSignin': True, } @@ -331,32 +345,31 @@ def test_update_tenant(self, tenant_mgt_app): def test_update_tenant_false_values(self, tenant_mgt_app): _, recorder = _instrument_tenant_mgt(tenant_mgt_app, 200, GET_TENANT_RESPONSE) tenant = tenant_mgt.update_tenant( - 'tenant-id', display_name='', allow_password_sign_up=False, + 'tenant-id', allow_password_sign_up=False, enable_email_link_sign_in=False, app=tenant_mgt_app) _assert_tenant(tenant) body = { - 'displayName': '', 'allowPasswordSignup': False, 'enableEmailLinkSignin': False, } - mask = ['allowPasswordSignup', 'displayName', 'enableEmailLinkSignin'] + mask = ['allowPasswordSignup', 'enableEmailLinkSignin'] self._assert_request(recorder, body, mask) def test_update_tenant_minimal(self, tenant_mgt_app): _, recorder = _instrument_tenant_mgt(tenant_mgt_app, 200, GET_TENANT_RESPONSE) tenant = tenant_mgt.update_tenant( - 'tenant-id', display_name='My Tenant', app=tenant_mgt_app) + 'tenant-id', display_name='My-Tenant', app=tenant_mgt_app) _assert_tenant(tenant) - body = {'displayName': 'My Tenant'} + body = {'displayName': 'My-Tenant'} mask = ['displayName'] self._assert_request(recorder, body, mask) def test_tenant_not_found_error(self, tenant_mgt_app): _instrument_tenant_mgt(tenant_mgt_app, 500, TENANT_NOT_FOUND_RESPONSE) with pytest.raises(tenant_mgt.TenantNotFoundError) as excinfo: - tenant_mgt.update_tenant('tenant', display_name='My Tenant', app=tenant_mgt_app) + tenant_mgt.update_tenant('tenant', display_name='My-Tenant', app=tenant_mgt_app) error_msg = 'No tenant found for the given identifier (TENANT_NOT_FOUND).' assert excinfo.value.code == exceptions.NOT_FOUND From fb3aefd45a6962930d65a13276f572ab28adefa0 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Tue, 14 Apr 2020 16:29:29 -0700 Subject: [PATCH 3/6] Integration tests for tenant-aware IdP management --- integration/test_tenant_mgt.py | 206 +++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) diff --git a/integration/test_tenant_mgt.py b/integration/test_tenant_mgt.py index 272bbd143..a1de03a80 100644 --- a/integration/test_tenant_mgt.py +++ b/integration/test_tenant_mgt.py @@ -15,18 +15,22 @@ """Integration tests for firebase_admin.tenant_mgt module.""" import random +import string import time from urllib import parse import uuid +import requests import pytest from firebase_admin import auth from firebase_admin import tenant_mgt +from integration import test_auth ACTION_LINK_CONTINUE_URL = 'http://localhost?a=1&b=5#f=1' ACTION_CODE_SETTINGS = auth.ActionCodeSettings(ACTION_LINK_CONTINUE_URL) +VERIFY_TOKEN_URL = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken' @pytest.fixture(scope='module') @@ -100,6 +104,25 @@ def test_auth_for_client(sample_tenant): assert client.tenant_id == sample_tenant.tenant_id +def test_custom_token(sample_tenant, api_key): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + custom_token = client.create_custom_token('user1') + id_token = _sign_in(custom_token, sample_tenant.tenant_id, api_key) + claims = client.verify_id_token(id_token) + assert claims['uid'] == 'user1' + assert claims['firebase']['tenant'] == sample_tenant.tenant_id + + +def test_custom_token_with_claims(sample_tenant, api_key): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + custom_token = client.create_custom_token('user1', {'premium': True}) + id_token = _sign_in(custom_token, sample_tenant.tenant_id, api_key) + claims = client.verify_id_token(id_token) + assert claims['uid'] == 'user1' + assert claims['premium'] is True + assert claims['firebase']['tenant'] == sample_tenant.tenant_id + + def test_create_user(sample_tenant, tenant_user): assert tenant_user.tenant_id == sample_tenant.tenant_id @@ -192,6 +215,172 @@ def test_import_users(sample_tenant): client.delete_user(user.uid) +@pytest.fixture(scope='module') +def oidc_provider(sample_tenant): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + provider_config = _create_oidc_provider_config(client) + yield provider_config + client.delete_oidc_provider_config(provider_config.provider_id) + + +def test_create_oidc_provider_config(oidc_provider): + assert isinstance(oidc_provider, auth.OIDCProviderConfig) + assert oidc_provider.client_id == 'OIDC_CLIENT_ID' + assert oidc_provider.issuer == 'https://oidc.com/issuer' + assert oidc_provider.display_name == 'OIDC_DISPLAY_NAME' + assert oidc_provider.enabled is True + + +def test_get_oidc_provider_config(sample_tenant, oidc_provider): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + provider_config = client.get_oidc_provider_config(oidc_provider.provider_id) + assert isinstance(provider_config, auth.OIDCProviderConfig) + assert provider_config.provider_id == oidc_provider.provider_id + assert provider_config.client_id == 'OIDC_CLIENT_ID' + assert provider_config.issuer == 'https://oidc.com/issuer' + assert provider_config.display_name == 'OIDC_DISPLAY_NAME' + assert provider_config.enabled is True + + +def test_list_oidc_provider_configs(sample_tenant, oidc_provider): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + page = client.list_oidc_provider_configs() + result = None + for provider_config in page.iterate_all(): + if provider_config.provider_id == oidc_provider.provider_id: + result = provider_config + break + + assert result is not None + + +def test_update_oidc_provider_config(sample_tenant): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + provider_config = _create_oidc_provider_config(client) + try: + provider_config = client.update_oidc_provider_config( + provider_config.provider_id, + client_id='UPDATED_OIDC_CLIENT_ID', + issuer='https://oidc.com/updated_issuer', + display_name='UPDATED_OIDC_DISPLAY_NAME', + enabled=False) + assert provider_config.client_id == 'UPDATED_OIDC_CLIENT_ID' + assert provider_config.issuer == 'https://oidc.com/updated_issuer' + assert provider_config.display_name == 'UPDATED_OIDC_DISPLAY_NAME' + assert provider_config.enabled is False + finally: + client.delete_oidc_provider_config(provider_config.provider_id) + + +def test_delete_oidc_provider_config(sample_tenant): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + provider_config = _create_oidc_provider_config(client) + client.delete_oidc_provider_config(provider_config.provider_id) + with pytest.raises(auth.ConfigurationNotFoundError): + client.get_oidc_provider_config(provider_config.provider_id) + + +@pytest.fixture(scope='module') +def saml_provider(sample_tenant): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + provider_config = _create_saml_provider_config(client) + yield provider_config + client.delete_saml_provider_config(provider_config.provider_id) + + +def test_create_saml_provider_config(sample_tenant, saml_provider): + assert isinstance(saml_provider, auth.SAMLProviderConfig) + assert saml_provider.idp_entity_id == 'IDP_ENTITY_ID' + assert saml_provider.sso_url == 'https://example.com/login' + assert saml_provider.x509_certificates == [test_auth.X509_CERTIFICATES[0]] + assert saml_provider.rp_entity_id == 'RP_ENTITY_ID' + assert saml_provider.callback_url == 'https://projectId.firebaseapp.com/__/auth/handler' + assert saml_provider.display_name == 'SAML_DISPLAY_NAME' + assert saml_provider.enabled is True + + +def test_get_saml_provider_config(sample_tenant, saml_provider): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + provider_config = client.get_saml_provider_config(saml_provider.provider_id) + assert isinstance(provider_config, auth.SAMLProviderConfig) + assert provider_config.provider_id == saml_provider.provider_id + assert provider_config.idp_entity_id == 'IDP_ENTITY_ID' + assert provider_config.sso_url == 'https://example.com/login' + assert provider_config.x509_certificates == [test_auth.X509_CERTIFICATES[0]] + assert provider_config.rp_entity_id == 'RP_ENTITY_ID' + assert provider_config.callback_url == 'https://projectId.firebaseapp.com/__/auth/handler' + assert provider_config.display_name == 'SAML_DISPLAY_NAME' + assert provider_config.enabled is True + + +def test_list_saml_provider_configs(sample_tenant, saml_provider): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + page = client.list_saml_provider_configs() + result = None + for provider_config in page.iterate_all(): + if provider_config.provider_id == saml_provider.provider_id: + result = provider_config + break + + assert result is not None + + +def test_update_saml_provider_config(sample_tenant): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + provider_config = _create_saml_provider_config(client) + try: + provider_config = client.update_saml_provider_config( + provider_config.provider_id, + idp_entity_id='UPDATED_IDP_ENTITY_ID', + sso_url='https://example.com/updated_login', + x509_certificates=[test_auth.X509_CERTIFICATES[1]], + rp_entity_id='UPDATED_RP_ENTITY_ID', + callback_url='https://updatedProjectId.firebaseapp.com/__/auth/handler', + display_name='UPDATED_SAML_DISPLAY_NAME', + enabled=False) + assert provider_config.idp_entity_id == 'UPDATED_IDP_ENTITY_ID' + assert provider_config.sso_url == 'https://example.com/updated_login' + assert provider_config.x509_certificates == [test_auth.X509_CERTIFICATES[1]] + assert provider_config.rp_entity_id == 'UPDATED_RP_ENTITY_ID' + assert provider_config.callback_url == ('https://updatedProjectId.firebaseapp.com/' + '__/auth/handler') + assert provider_config.display_name == 'UPDATED_SAML_DISPLAY_NAME' + assert provider_config.enabled is False + finally: + client.delete_saml_provider_config(provider_config.provider_id) + + +def test_delete_saml_provider_config(sample_tenant): + client = tenant_mgt.auth_for_tenant(sample_tenant.tenant_id) + provider_config = _create_saml_provider_config(client) + client.delete_saml_provider_config(provider_config.provider_id) + with pytest.raises(auth.ConfigurationNotFoundError): + client.get_saml_provider_config(provider_config.provider_id) + + +def _create_oidc_provider_config(client): + provider_id = 'oidc.{0}'.format(_random_string()) + return client.create_oidc_provider_config( + provider_id=provider_id, + client_id='OIDC_CLIENT_ID', + issuer='https://oidc.com/issuer', + display_name='OIDC_DISPLAY_NAME', + enabled=True) + + +def _create_saml_provider_config(client): + provider_id = 'saml.{0}'.format(_random_string()) + return client.create_saml_provider_config( + provider_id=provider_id, + idp_entity_id='IDP_ENTITY_ID', + sso_url='https://example.com/login', + x509_certificates=[test_auth.X509_CERTIFICATES[0]], + rp_entity_id='RP_ENTITY_ID', + callback_url='https://projectId.firebaseapp.com/__/auth/handler', + display_name='SAML_DISPLAY_NAME', + enabled=True) + + def _random_uid(): return str(uuid.uuid4()).lower().replace('-', '') @@ -205,7 +394,24 @@ def _random_phone(): return '+1' + ''.join([str(random.randint(0, 9)) for _ in range(0, 10)]) +def _random_string(length=10): + letters = string.ascii_lowercase + return ''.join(random.choice(letters) for i in range(length)) + + def _tenant_id_from_link(link): query = parse.urlparse(link).query parsed_query = parse.parse_qs(query) return parsed_query['tenantId'][0] + + +def _sign_in(custom_token, tenant_id, api_key): + body = { + 'token' : custom_token.decode(), + 'returnSecureToken' : True, + 'tenantId': tenant_id, + } + params = {'key' : api_key} + resp = requests.request('post', VERIFY_TOKEN_URL, params=params, json=body) + resp.raise_for_status() + return resp.json().get('idToken') From 10d49bf15b6d14633d45b01c3f96ef9ac4333ee8 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Tue, 14 Apr 2020 16:55:29 -0700 Subject: [PATCH 4/6] Fixing lint error; Added unit test for UserRecord.tenant_id --- firebase_admin/_user_mgt.py | 5 +++++ integration/test_tenant_mgt.py | 2 +- tests/test_user_mgt.py | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/firebase_admin/_user_mgt.py b/firebase_admin/_user_mgt.py index d7a017b99..0b0c5ddb6 100644 --- a/firebase_admin/_user_mgt.py +++ b/firebase_admin/_user_mgt.py @@ -246,6 +246,11 @@ def custom_claims(self): @property def tenant_id(self): + """Returns the tenant ID of this user. + + Returns: + string: A tenant ID string or None. + """ return self._data.get('tenantId') diff --git a/integration/test_tenant_mgt.py b/integration/test_tenant_mgt.py index a1de03a80..c9eefd96e 100644 --- a/integration/test_tenant_mgt.py +++ b/integration/test_tenant_mgt.py @@ -288,7 +288,7 @@ def saml_provider(sample_tenant): client.delete_saml_provider_config(provider_config.provider_id) -def test_create_saml_provider_config(sample_tenant, saml_provider): +def test_create_saml_provider_config(saml_provider): assert isinstance(saml_provider, auth.SAMLProviderConfig) assert saml_provider.idp_entity_id == 'IDP_ENTITY_ID' assert saml_provider.sso_url == 'https://example.com/login' diff --git a/tests/test_user_mgt.py b/tests/test_user_mgt.py index b64a4d1f3..c7b2de496 100644 --- a/tests/test_user_mgt.py +++ b/tests/test_user_mgt.py @@ -81,6 +81,7 @@ def _check_user_record(user, expected_uid='testuser'): assert user.user_metadata.creation_timestamp == 1234567890000 assert user.user_metadata.last_sign_in_timestamp is None assert user.provider_id == 'firebase' + assert user.tenant_id is None claims = user.custom_claims assert claims['admin'] is True @@ -207,6 +208,10 @@ def test_no_tokens_valid_after_time(self): user = auth.UserRecord({'localId' : 'user'}) assert user.tokens_valid_after_timestamp == 0 + def test_tenant_id(self): + user = auth.UserRecord({'localId' : 'user', 'tenantId': 'test-tenant'}) + assert user.tenant_id == 'test-tenant' + class TestGetUser: From 555f561c3444746bd680d2f85a5b41498d05860d Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Tue, 14 Apr 2020 16:57:23 -0700 Subject: [PATCH 5/6] Trigger staging From 9c42fe73ffc2c9f817d8044a3f590bfed9e4d4f4 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 15 Apr 2020 16:30:08 -0700 Subject: [PATCH 6/6] Added unit tests for tenant names longer than 20 chars --- tests/test_tenant_mgt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_tenant_mgt.py b/tests/test_tenant_mgt.py index 0e615429e..f92dd2a83 100644 --- a/tests/test_tenant_mgt.py +++ b/tests/test_tenant_mgt.py @@ -216,7 +216,7 @@ def test_invalid_display_name_type(self, display_name, tenant_mgt_app): tenant_mgt.create_tenant(display_name=display_name, app=tenant_mgt_app) assert str(excinfo.value).startswith('Invalid type for displayName') - @pytest.mark.parametrize('display_name', ['', 'foo', '1test', 'foo bar']) + @pytest.mark.parametrize('display_name', ['', 'foo', '1test', 'foo bar', 'a'*21]) def test_invalid_display_name_value(self, display_name, tenant_mgt_app): with pytest.raises(ValueError) as excinfo: tenant_mgt.create_tenant(display_name=display_name, app=tenant_mgt_app) @@ -303,7 +303,7 @@ def test_invalid_display_name_type(self, display_name, tenant_mgt_app): tenant_mgt.update_tenant('tenant-id', display_name=display_name, app=tenant_mgt_app) assert str(excinfo.value).startswith('Invalid type for displayName') - @pytest.mark.parametrize('display_name', ['', 'foo', '1test', 'foo bar']) + @pytest.mark.parametrize('display_name', ['', 'foo', '1test', 'foo bar', 'a'*21]) def test_invalid_display_name_value(self, display_name, tenant_mgt_app): with pytest.raises(ValueError) as excinfo: tenant_mgt.update_tenant('tenant-id', display_name=display_name, app=tenant_mgt_app)