8000 fix(auth): Integration tests for multi-tenancy and IdP management API… · CyberSys/firebase-admin-python@96bcabb · GitHub
[go: up one dir, main page]

Skip to content

Commit 96bcabb

Browse files
authored
fix(auth): Integration tests for multi-tenancy and IdP management APIs (firebase#446)
* fix(auth): Integration tests for IdP management APIs * More integration tests for tenant_mgt module; Made display_name required for tenants * Integration tests for tenant-aware IdP management * Fixing lint error; Added unit test for UserRecord.tenant_id * Trigger staging * Added unit tests for tenant names longer than 20 chars
1 parent 99d152f commit 96bcabb

File tree

7 files changed

+672
-30
lines changed

7 files changed

+672
-30
lines changed

firebase_admin/_auth_providers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def display_name(self):
4242

4343
@property
4444
def enabled(self):
45-
return self._data['enabled']
45+
return self._data.get('enabled', False)
4646

4747

4848
class OIDCProviderConfig(ProviderConfig):

firebase_admin/_user_mgt.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,15 @@ def custom_claims(self):
244244
return parsed
245245
return None
246246

247+
@property
248+
def tenant_id(self):
249+
"""Returns the tenant ID of this user.
250+
251+
Returns:
252+
string: A tenant ID string or None.
253+
"""
254+
return self._data.get('tenantId')
255+
247256

248257
class ExportedUserRecord(UserRecord):
249258
"""Contains metadata associated with a user including password hash and salt."""

firebase_admin/tenant_mgt.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
Google Cloud Identity Platform (GCIP) instance.
1919
"""
2020

21+
import re
2122
import threading
2223

2324
import requests
@@ -31,6 +32,7 @@
3132

3233
_TENANT_MGT_ATTRIBUTE = '_tenant_mgt'
3334
_MAX_LIST_TENANTS_RESULTS = 100
35+
_DISPLAY_NAME_PATTERN = re.compile('^[a-zA-Z][a-zA-Z0-9-]{3,19}$')
3436

3537

3638
__all__ = [
@@ -89,15 +91,16 @@ def get_tenant(tenant_id, app=None):
8991

9092

9193
def create_tenant(
92-
display_name=None, allow_password_sign_up=None, enable_email_link_sign_in=None, app=None):
94+
display_name, allow_password_sign_up=None, enable_email_link_sign_in=None, app=None):
9395
"""Creates a new tenant from the given options.
9496
9597
Args:
96-
display_name: Display name string for the new tenant (optional).
98+
display_name: Display name string for the new tenant. Must begin with a letter and contain
99+
only letters, digits and hyphens. Length must be between 4 and 20.
97100
allow_password_sign_up: A boolean indicating whether to enable or disable the email sign-in
98-
provider.
101+
provider (optional).
99102
enable_email_link_sign_in: A boolean indicating whether to enable or disable email link
100-
sign-in. Disabling this makes the password required for email sign-in.
103+
sign-in (optional). Disabling this makes the password required for email sign-in.
101104
app: An App instance (optional).
102105
103106
Returns:
@@ -120,7 +123,7 @@ def update_tenant(
120123
121124
Args:
122125
tenant_id: ID of the tenant to update.
123-
display_name: Display name string for the new tenant (optional).
126+
display_name: Updated display name string for the tenant (optional).
124127
allow_password_sign_up: A boolean indicating whether to enable or disable the email sign-in
125128
provider.
126129
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):
269272
return Tenant(body)
270273

271274
def create_tenant(
272-
self, display_name=None, allow_password_sign_up=None, enable_email_link_sign_in=None):
275+
self, display_name, allow_password_sign_up=None, enable_email_link_sign_in=None):
273276
"""Creates a new tenant from the given parameters."""
274-
payload = {}
275-
if display_name is not None:
276-
payload['displayName'] = _auth_utils.validate_string(display_name, 'displayName')
277+
278+
payload = {'displayName': _validate_display_name(display_name)}
277279
if allow_password_sign_up is not None:
278280
payload['allowPasswordSignup'] = _auth_utils.validate_boolean(
279281
allow_password_sign_up, 'allowPasswordSignup')
@@ -297,7 +299,7 @@ def update_tenant(
297299

298300
payload = {}
299301
if display_name is not None:
300-
payload['displayName'] = _auth_utils.validate_string(display_name, 'displayName')
302+
payload['displayName'] = _validate_display_name(display_name)
301303
if allow_password_sign_up is not None:
302304
payload['allowPasswordSignup'] = _auth_utils.validate_boolean(
303305
allow_password_sign_up, 'allowPasswordSignup')
@@ -431,3 +433,13 @@ def __next__(self):
431433

432434
def __iter__(self):
433435
return self
436+
437+
438+
def _validate_display_name(display_name):
439+
if not isinstance(display_name, str):
440+
raise ValueError('Invalid type for displayName')
441+
if not _DISPLAY_NAME_PATTERN.search(display_name):
442+
raise ValueError(
443+
'displayName must start with a letter and only consist of letters, digits and '
444+
'hyphens with 4-20 characters.')
445+
return display_name

integration/test_auth.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import base64
1717
import datetime
1818
import random
19+
import string
1920
import time
2021
from urllib import parse
2122
import uuid
@@ -38,6 +39,30 @@
3839

3940
ACTION_LINK_CONTINUE_URL = 'http://localhost?a=1&b=5#f=1'
4041

42+
X509_CERTIFICATES = [
43+
('-----BEGIN CERTIFICATE-----\nMIICZjCCAc+gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBQMQswCQYDVQQGEwJ1czE'
44+
'L\nMAkGA1UECAwCQ0ExDTALBgNVBAoMBEFjbWUxETAPBgNVBAMMCGFjbWUuY29tMRIw\nEAYDVQQHDAlTdW5ueXZhbGU'
45+
'wHhcNMTgxMjA2MDc1MTUxWhcNMjgxMjAzMDc1MTUx\nWjBQMQswCQYDVQQGEwJ1czELMAkGA1UECAwCQ0ExDTALBgNVB'
46+
'AoMBEFjbWUxETAP\nBgNVBAMMCGFjbWUuY29tMRIwEAYDVQQHDAlTdW5ueXZhbGUwgZ8wDQYJKoZIhvcN\nAQEBBQADg'
47+
'Y0AMIGJAoGBAKphmggjiVgqMLXyzvI7cKphscIIQ+wcv7Dld6MD4aKv\n7Jqr8ltujMxBUeY4LFEKw8Terb01snYpDot'
48+
'filaG6NxpF/GfVVmMalzwWp0mT8+H\nyzyPj89mRcozu17RwuooR6n1ofXjGcBE86lqC21UhA3WVgjPOLqB42rlE9gPn'
49+
'ZLB\nAgMBAAGjUDBOMB0GA1UdDgQWBBS0iM7WnbCNOnieOP1HIA+Oz/ML+zAfBgNVHSME\nGDAWgBS0iM7WnbCNOnieO'
50+
'P1HIA+Oz/ML+zAMBgNVHRMEBTADAQH/MA0GCSqGSIb3\nDQEBDQUAA4GBAF3jBgS+wP+K/jTupEQur6iaqS4UvXd//d4'
51+
'vo1MV06oTLQMTz+rP\nOSMDNwxzfaOn6vgYLKP/Dcy9dSTnSzgxLAxfKvDQZA0vE3udsw0Bd245MmX4+GOp\nlbrN99X'
52+
'P1u+lFxCSdMUzvQ/jW4ysw/Nq4JdJ0gPAyPvL6Qi/3mQdIQwx\n-----END CERTIFICATE-----\n'),
53+
('-----BEGIN CERTIFICATE-----\nMIICZjCCAc+gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBQMQswCQYDVQQGEwJ1czE'
54+
'L\nMAkGA1UECAwCQ0ExDTALBgNVBAoMBEFjbWUxETAPBgNVBAMMCGFjbWUuY29tMRIw\nEAYDVQQHDAlTdW5ueXZhbGU'
55+
'wHhcNMTgxMjA2MDc1ODE4WhcNMjgxMjAzMDc1ODE4\nWjBQMQswCQYDVQQGEwJ1czELMAkGA1UECAwCQ0ExDTALBgNVB'
56+
'AoMBEFjbWUxETAP\nBgNVBAMMCGFjbWUuY29tMRIwEAYDVQQHDAlTdW5ueXZhbGUwgZ8wDQYJKoZIhvcN\nAQEBBQADg'
57+
'Y0AMIGJAoGBAKuzYKfDZGA6DJgQru3wNUqv+S0hMZfP/jbp8ou/8UKu\nrNeX7cfCgt3yxoGCJYKmF6t5mvo76JY0MWw'
58+
'A53BxeP/oyXmJ93uHG5mFRAsVAUKs\ncVVb0Xi6ujxZGVdDWFV696L0BNOoHTfXmac6IBoZQzNNK4n1AATqwo+z7a0pf'
59+
'RrJ\nAgMBAAGjUDBOMB0GA1UdDgQWBBSKmi/ZKMuLN0ES7/jPa7q7jAjPiDAfBgNVHSME\nGDAWgBSKmi/ZKMuLN0ES7'
60+
'/jPa7q7jAjPiDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3\nDQEBDQUAA4GBAAg2a2kSn05NiUOuWOHwPUjW3wQRsGxPXtb'
61+
'hWMhmNdCfKKteM2+/\nLd/jz5F3qkOgGQ3UDgr3SHEoWhnLaJMF4a2tm6vL2rEIfPEK81KhTTRxSsAgMVbU\nJXBz1md'
62+
'6Ur0HlgQC7d1CHC8/xi2DDwHopLyxhogaZUxy9IaRxUEa2vJW\n-----END CERTIFICATE-----\n'),
63+
]
64+
65+
4166
def _sign_in(custom_token, api_key):
4267
body = {'token' : custom_token.decode(), 'returnSecureToken' : True}
4368
params = {'key' : api_key}
@@ -52,6 +77,10 @@ def _sign_in_with_password(email, password, api_key):
5277
resp.raise_for_status()
5378
return resp.json().get('idToken')
5479

80+
def _random_string(length=10):
81+
letters = string.ascii_lowercase
82+
return ''.join(random.choice(letters) for i in range(length))
83+
5584
def _random_id():
5685
random_id = str(uuid.uuid4()).lower().replace('-', '')
5786
email = 'test{0}@example.{1}.com'.format(random_id[:12], random_id[12:])
@@ -477,6 +506,163 @@ def test_email_sign_in_with_settings(new_user_email_unverified, api_key):
477506
assert id_token is not None and len(id_token) > 0
478507
assert auth.get_user(new_user_email_unverified.uid).email_verified
479508

509+
510+
@pytest.fixture(scope='module')
511+
def oidc_provider():
512+
provider_config = _create_oidc_provider_config()
513+
yield provider_config
514+
auth.delete_oidc_provider_config(provider_config.provider_id)
515+
516+
517+
def test_create_oidc_provider_config(oidc_provider):
518+
assert isinstance(oidc_provider, auth.OIDCProviderConfig)
519+
assert oidc_provider.client_id == 'OIDC_CLIENT_ID'
520+
assert oidc_provider.issuer == 'https://oidc.com/issuer'
521+
assert oidc_provider.display_name == 'OIDC_DISPLAY_NAME'
522+
assert oidc_provider.enabled is True
523+
524+
525+
def test_get_oidc_provider_config(oidc_provider):
526+
provider_config = auth.get_oidc_provider_config(oidc_provider.provider_id)
527+
assert isinstance(provider_config, auth.OIDCProviderConfig)
528+
assert provider_config.provider_id == oidc_provider.provider_id
529+
assert provider_config.client_id == 'OIDC_CLIENT_ID'
530+
assert provider_config.issuer == 'https://oidc.com/issuer'
531+
assert provider_config.display_name == 'OIDC_DISPLAY_NAME'
532+
assert provider_config.enabled is True
533+
534+
535+
def test_list_oidc_provider_configs(oidc_provider):
536+
page = auth.list_oidc_provider_configs()
537+
result = None
538+
for provider_config in page.iterate_all():
539+
if provider_config.provider_id == oidc_provider.provider_id:
540+
result = provider_config
541+
break
542+
543+
assert result is not None
544+
545+
546+
def test_update_oidc_provider_config():
547+
provider_config = _create_oidc_provider_config()
548+
try:
549+
provider_config = auth.update_oidc_provider_config(
550+
provider_config.provider_id,
551+
client_id='UPDATED_OIDC_CLIENT_ID',
552+
issuer='https://oidc.com/updated_issuer',
553+
display_name='UPDATED_OIDC_DISPLAY_NAME',
554+
enabled=False)
555+
assert provider_config.client_id == 'UPDATED_OIDC_CLIENT_ID'
556+
assert provider_config.issuer == 'https://oidc.com/updated_issuer'
557+
assert provider_config.display_name == 'UPDATED_OIDC_DISPLAY_NAME'
558+
assert provider_config.enabled is False
559+
finally:
560+
auth.delete_oidc_provider_config(provider_config.provider_id)
561+
562+
563+
def test_delete_oidc_provider_config():
564+
provider_config = _create_oidc_provider_config()
565+
auth.delete_oidc_provider_config(provider_config.provider_id)
566+
with pytest.raises(auth.ConfigurationNotFoundError):
567+
auth.get_oidc_provider_config(provider_config.provider_id)
568+
569+
570+
@pytest.fixture(scope='module')
571+
def saml_provider():
572+
provider_config = _create_saml_provider_config()
573+
yield provider_config
574+
auth.delete_saml_provider_config(provider_config.provider_id)
575+
576+
577+
def test_create_saml_provider_config(saml_provider):
578+
assert isinstance(saml_provider, auth.SAMLProviderConfig)
579+
assert saml_provider.idp_entity_id == 'IDP_ENTITY_ID'
580+
assert saml_provider.sso_url == 'https://example.com/login'
581+
assert saml_provider.x509_certificates == [X509_CERTIFICATES[0]]
582+
assert saml_provider.rp_entity_id == 'RP_ENTITY_ID'
583+
assert saml_provider.callback_url == 'https://projectId.firebaseapp.com/__/auth/handler'
584+
assert saml_provider.display_name == 'SAML_DISPLAY_NAME'
585+
assert saml_provider.enabled is True
586+
587+
588+
def test_get_saml_provider_config(saml_provider):
589+
provider_config = auth.get_saml_provider_config(saml_provider.provider_id)
590+
assert isinstance(provider_config, auth.SAMLProviderConfig)
591+
assert provider_config.provider_id == saml_provider.provider_id
592+
assert provider_config.idp_entity_id == 'IDP_ENTITY_ID'
593+
assert provider_config.sso_url == 'https://example.com/login'
594+
assert provider_config.x509_certificates == [X509_CERTIFICATES[0]]
595+
assert provider_config.rp_entity_id == 'RP_ENTITY_ID'
596+
assert provider_config.callback_url == 'https://projectId.firebaseapp.com/__/auth/handler'
597+
assert provider_config.display_name == 'SAML_DISPLAY_NAME'
598+
assert provider_config.enabled is True
599+
600+
601+
def test_list_saml_provider_configs(saml_provider):
602+
page = auth.list_saml_provider_configs()
603+
result = None
604+
for provider_config in page.iterate_all():
605+
if provider_config.provider_id == saml_provider.provider_id:
606+
result = provider_config
607+
break
608+
609+
assert result is not None
610+
611+
612+
def test_update_saml_provider_config():
613+
provider_config = _create_saml_provider_config()
614+
try:
615+
provider_config = auth.update_saml_provider_config(
616+
provider_config.provider_id,
617+
idp_entity_id='UPDATED_IDP_ENTITY_ID',
618+
sso_url='https://example.com/updated_login',
619+
x509_certificates=[X509_CERTIFICATES[1]],
620+
rp_entity_id='UPDATED_RP_ENTITY_ID',
621+
callback_url='https://updatedProjectId.firebaseapp.com/__/auth/handler',
622+
display_name='UPDATED_SAML_DISPLAY_NAME',
623+
enabled=False)
624+
assert provider_config.idp_entity_id == 'UPDATED_IDP_ENTITY_ID'
625+
assert provider_config.sso_url == 'https://example.com/updated_login'
626+
assert provider_config.x509_certificates == [X509_CERTIFICATES[1]]
627+
assert provider_config.rp_entity_id == 'UPDATED_RP_ENTITY_ID'
628+
assert provider_config.callback_url == ('https://updatedProjectId.firebaseapp.com/'
629+
'__/auth/handler')
630+
assert provider_config.display_name == 'UPDATED_SAML_DISPLAY_NAME'
631+
assert provider_config.enabled is False
632+
finally:
633+
auth.delete_saml_provider_config(provider_config.provider_id)
634+
635+
636+
def test_delete_saml_provider_config():
637+
provider_config = _create_saml_provider_config()
638+
auth.delete_saml_provider_config(provider_config.provider_id)
639+
with pytest.raises(auth.ConfigurationNotFoundError):
640+
auth.get_saml_provider_config(provider_config.provider_id)
641+
642+
643+
def _create_oidc_provider_config():
644+
provider_id = 'oidc.{0}'.format(_random_string())
645+
return auth.create_oidc_provider_config(
646+
provider_id=provider_id,
647+
client_id='OIDC_CLIENT_ID',
648+
issuer='https://oidc.com/issuer',
649+
display_name='OIDC_DISPLAY_NAME',
650+
enabled=True)
651+
652+
653+
def _create_saml_provider_config():
654+
provider_id = 'saml.{0}'.format(_random_string())
655+
return auth.create_saml_provider_config(
656+
provider_id=provider_id,
657+
idp_entity_id='IDP_ENTITY_ID',
658+
sso_url='https://example.com/login',
659+
x509_certificates=[X509_CERTIFICATES[0]],
660+
rp_entity_id='RP_ENTITY_ID',
661+
callback_url='https://projectId.firebaseapp.com/__/auth/handler',
662+
display_name='SAML_DISPLAY_NAME',
663+
enabled=True)
664+
665+
480666
class CredentialWrapper(credentials.Base):
481667
"""A custom Firebase credential that wraps an OAuth2 token."""
482668

0 commit comments

Comments
 (0)
0