diff --git a/firebase_admin/_auth_utils.py b/firebase_admin/_auth_utils.py index 8f3c419a..f3aeb166 100644 --- a/firebase_admin/_auth_utils.py +++ b/firebase_admin/_auth_utils.py @@ -24,6 +24,7 @@ EMULATOR_HOST_ENV_VAR = 'FIREBASE_AUTH_EMULATOR_HOST' +TENANT_EMULATOR_HOST_ENV_VAR = 'FIREBASE_TENANT_EMULATOR_HOST' MAX_CLAIMS_PAYLOAD_SIZE = 1000 RESERVED_CLAIMS = set([ 'acr', 'amr', 'at_hash', 'aud', 'auth_time', 'azp', 'cnf', 'c_hash', 'exp', 'iat', @@ -78,6 +79,13 @@ def get_emulator_host(): 'It must follow format "host:port".') return emulator_host +def get_tenant_emulator_host(): + emulator_host = os.getenv(TENANT_EMULATOR_HOST_ENV_VAR, '') + if emulator_host and '//' in emulator_host: + raise ValueError( + f'Invalid {TENANT_EMULATOR_HOST_ENV_VAR}: "{emulator_host}". ' + 'It must follow format "host:port".') + return emulator_host def is_emulated(): return get_emulator_host() != '' diff --git a/firebase_admin/tenant_mgt.py b/firebase_admin/tenant_mgt.py index 9e713d98..9a53fb1e 100644 --- a/firebase_admin/tenant_mgt.py +++ b/firebase_admin/tenant_mgt.py @@ -235,9 +235,19 @@ class _TenantManagementService: TENANT_MGT_URL = 'https://identitytoolkit.googleapis.com/v2' def __init__(self, app): - credential = app.credential.get_credential() + tenant_emulator_host = _auth_utils.get_tenant_emulator_host() + + if tenant_emulator_host: + credential = _utils.EmulatorAdminCredentials() + tenant_mgt_url = ( + f'http://{tenant_emulator_host}/identitytoolkit.googleapis.com/v2' + ) + else: + credential = app.credential.get_credential() + tenant_mgt_url = self.TENANT_MGT_URL + version_header = f'Python/Admin/{firebase_admin.__version__}' - base_url = f'{self.TENANT_MGT_URL}/projects/{app.project_id}' + base_url = f'{tenant_mgt_url}/projects/{app.project_id}' self.app = app self.client = _http_client.JsonHttpClient( credential=credential, base_url=base_url, headers={'X-Client-Version': version_header}) diff --git a/tests/test_tenant_mgt.py b/tests/test_tenant_mgt.py index 900faa37..89769a4e 100644 --- a/tests/test_tenant_mgt.py +++ b/tests/test_tenant_mgt.py @@ -112,22 +112,40 @@ USER_MGT_URL_PREFIX = 'https://identitytoolkit.googleapis.com/v1/projects/mock-project-id' PROVIDER_MGT_URL_PREFIX = 'https://identitytoolkit.googleapis.com/v2/projects/mock-project-id' -TENANT_MGT_URL_PREFIX = 'https://identitytoolkit.googleapis.com/v2/projects/mock-project-id' +TENANT_MGT_URL = 'https://identitytoolkit.googleapis.com/v2' +TENANT_MGT_URL_PREFIX = f'{TENANT_MGT_URL}/projects/mock-project-id' +TENANT_MGT_URLS = { + 'URL': TENANT_MGT_URL, + 'PREFIX': TENANT_MGT_URL_PREFIX, +} -@pytest.fixture(scope='module') -def tenant_mgt_app(): +TENANT_EMULATOR_HOST_ENV_VAR = 'FIREBASE_TENANT_EMULATOR_HOST' +TENANT_EMULATOR_HOST = 'localhost:9099' + +EMULATED_TENANT_MGT_URL = f'http://{TENANT_EMULATOR_HOST}/identitytoolkit.googleapis.com/v2' +EMULATED_TENANT_MGT_URL_PREFIX = f'{EMULATED_TENANT_MGT_URL}/projects/mock-project-id' + +@pytest.fixture(scope='module', params=[{'emulated': False}, {'emulated': True}]) +def tenant_mgt_app(request): + monkeypatch = testutils.new_monkeypatch() + if request.param['emulated']: + monkeypatch.setenv(TENANT_EMULATOR_HOST_ENV_VAR, TENANT_EMULATOR_HOST) + monkeypatch.setitem(TENANT_MGT_URLS, 'URL', EMULATED_TENANT_MGT_URL) + monkeypatch.setitem(TENANT_MGT_URLS, 'PREFIX', EMULATED_TENANT_MGT_URL_PREFIX) app = firebase_admin.initialize_app( testutils.MockCredential(), name='tenantMgt', options={'projectId': 'mock-project-id'}) yield app firebase_admin.delete_app(app) + monkeypatch.undo() def _instrument_tenant_mgt(app, status, payload): service = tenant_mgt._get_tenant_mgt_service(app) recorder = [] + mount_url = service.client._base_url.rsplit('/projects/', 1)[0] service.client.session.mount( - tenant_mgt._TenantManagementService.TENANT_MGT_URL, + mount_url, testutils.MockAdapter(payload, status, recorder)) return service, recorder @@ -197,10 +215,13 @@ def test_get_tenant(self, tenant_mgt_app): assert len(recorder) == 1 req = recorder[0] assert req.method == 'GET' - assert req.url == f'{TENANT_MGT_URL_PREFIX}/tenants/tenant-id' + assert req.url == f'{TENANT_MGT_URLS["PREFIX"]}/tenants/tenant-id' assert req.headers['X-Client-Version'] == f'Python/Admin/{firebase_admin.__version__}' - expected_metrics_header = _utils.get_metrics_header() + ' mock-cred-metric-tag' - assert req.headers['x-goog-api-client'] == expected_metrics_header + expected_metrics_headers = [ + _utils.get_metrics_header(), + _utils.get_metrics_header() + ' mock-cred-metric-tag', + ] + assert req.headers['x-goog-api-client'] in expected_metrics_headers def test_tenant_not_found(self, tenant_mgt_app): _instrument_tenant_mgt(tenant_mgt_app, 500, TENANT_NOT_FOUND_RESPONSE) @@ -290,10 +311,13 @@ def _assert_request(self, recorder, body): assert len(recorder) == 1 req = recorder[0] assert req.method == 'POST' - assert req.url == f'{TENANT_MGT_URL_PREFIX}/tenants' + assert req.url == f'{TENANT_MGT_URLS["PREFIX"]}/tenants' assert req.headers['X-Client-Version'] == f'Python/Admin/{firebase_admin.__version__}' - expected_metrics_header = _utils.get_metrics_header() + ' mock-cred-metric-tag' - assert req.headers['x-goog-api-client'] == expected_metrics_header + expected_metrics_headers = [ + _utils.get_metrics_header(), + _utils.get_metrics_header() + ' mock-cred-metric-tag', + ] + assert req.headers['x-goog-api-client'] in expected_metrics_headers got = json.loads(req.body.decode()) assert got == body @@ -390,10 +414,16 @@ def _assert_request(self, recorder, body, mask): assert len(recorder) == 1 req = recorder[0] assert req.method == 'PATCH' - assert req.url == f'{TENANT_MGT_URL_PREFIX}/tenants/tenant-id?updateMask={",".join(mask)}' + assert req.url == ( + f'{TENANT_MGT_URLS["PREFIX"]}/tenants/tenant-id' + f'?updateMask={",".join(mask)}' + ) assert req.headers['X-Client-Version'] == f'Python/Admin/{firebase_admin.__version__}' - expected_metrics_header = _utils.get_metrics_header() + ' mock-cred-metric-tag' - assert req.headers['x-goog-api-client'] == expected_metrics_header + expected_metrics_headers = [ + _utils.get_metrics_header(), + _utils.get_metrics_header() + ' mock-cred-metric-tag', + ] + assert req.headers['x-goog-api-client'] in expected_metrics_headers got = json.loads(req.body.decode()) assert got == body @@ -413,10 +443,13 @@ def test_delete_tenant(self, tenant_mgt_app): assert len(recorder) == 1 req = recorder[0] assert req.method == 'DELETE' - assert req.url == f'{TENANT_MGT_URL_PREFIX}/tenants/tenant-id' + assert req.url == f'{TENANT_MGT_URLS["PREFIX"]}/tenants/tenant-id' assert req.headers['X-Client-Version'] == f'Python/Admin/{firebase_admin.__version__}' - expected_metrics_header = _utils.get_metrics_header() + ' mock-cred-metric-tag' - assert req.headers['x-goog-api-client'] == expected_metrics_header + expected_metrics_headers = [ + _utils.get_metrics_header(), + _utils.get_metrics_header() + ' mock-cred-metric-tag', + ] + assert req.headers['x-goog-api-client'] in expected_metrics_headers def test_tenant_not_found(self, tenant_mgt_app): _instrument_tenant_mgt(tenant_mgt_app, 500, TENANT_NOT_FOUND_RESPONSE) @@ -560,8 +593,11 @@ def _assert_request(self, recorder, expected=None): req = recorder[0] assert req.method == 'GET' assert req.headers['X-Client-Version'] == f'Python/Admin/{firebase_admin.__version__}' - expected_metrics_header = _utils.get_metrics_header() + ' mock-cred-metric-tag' - assert req.headers['x-goog-api-client'] == expected_metrics_header + expected_metrics_headers = [ + _utils.get_metrics_header(), + _utils.get_metrics_header() + ' mock-cred-metric-tag', + ] + assert req.headers['x-goog-api-client'] in expected_metrics_headers request = dict(parse.parse_qsl(parse.urlsplit(req.url).query)) assert request == expected @@ -944,8 +980,11 @@ def _assert_request( assert req.method == method assert req.url == f'{prefix}/tenants/tenant-id{want_url}' assert req.headers['X-Client-Version'] == f'Python/Admin/{firebase_admin.__version__}' - expected_metrics_header = _utils.get_metrics_header() + ' mock-cred-metric-tag' - assert req.headers['x-goog-api-client'] == expected_metrics_header + expected_metrics_headers = [ + _utils.get_metrics_header(), + _utils.get_metrics_header() + ' mock-cred-metric-tag', + ] + assert req.headers['x-goog-api-client'] in expected_metrics_headers body = json.loads(req.body.decode()) assert body == want_body