From 64691ac29d73153f4abb25170d803d9bb9926ae1 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 9 Apr 2020 15:06:46 -0700 Subject: [PATCH] feat(auth): Added list_oidc_provider_configs() API --- firebase_admin/_auth_client.py | 25 ++++ firebase_admin/_auth_providers.py | 21 +++- firebase_admin/auth.py | 28 +++++ tests/data/list_oidc_provider_configs.json | 18 +++ tests/test_auth_providers.py | 139 +++++++++++++++++++-- tests/test_tenant_mgt.py | 27 ++++ 6 files changed, 247 insertions(+), 11 deletions(-) create mode 100644 tests/data/list_oidc_provider_configs.json diff --git a/firebase_admin/_auth_client.py b/firebase_admin/_auth_client.py index e147d85ae..1f692d07f 100644 --- a/firebase_admin/_auth_client.py +++ b/firebase_admin/_auth_client.py @@ -470,6 +470,31 @@ def delete_oidc_provider_config(self, provider_id): """ self._provider_manager.delete_oidc_provider_config(provider_id) + def list_oidc_provider_configs( + self, page_token=None, max_results=_auth_providers.MAX_LIST_CONFIGS_RESULTS): + """Retrieves a page of OIDC provider configs from a Firebase project. + + The ``page_token`` argument governs the starting point of the page. The ``max_results`` + argument governs the maximum number of configs that may be included in the returned + page. This function never returns None. If there are no OIDC configs in the Firebase + project, this returns an empty page. + + Args: + page_token: A non-empty page token string, which indicates the starting point of the + page (optional). Defaults to ``None``, which will retrieve the first page of users. + max_results: A positive integer indicating the maximum number of users to include in + the returned page (optional). Defaults to 100, which is also the maximum number + allowed. + + Returns: + ListProviderConfigsPage: A ListProviderConfigsPage instance. + + Raises: + ValueError: If max_results or page_token are invalid. + FirebaseError: If an error occurs while retrieving the OIDC provider configs. + """ + return self._provider_manager.list_oidc_provider_configs(page_token, max_results) + def get_saml_provider_config(self, provider_id): """Returns the SAMLProviderConfig with the given ID. diff --git a/firebase_admin/_auth_providers.py b/firebase_admin/_auth_providers.py index 121105bc3..28c25ae49 100644 --- a/firebase_admin/_auth_providers.py +++ b/firebase_admin/_auth_providers.py @@ -140,6 +140,13 @@ def iterate_all(self): return _ProviderConfigIterator(self) +class _ListOIDCProviderConfigsPage(ListProviderConfigsPage): + + @property + def provider_configs(self): + return [OIDCProviderConfig(data) for data in self._current.get('oauthIdpConfigs', [])] + + class _ListSAMLProviderConfigsPage(ListProviderConfigsPage): @property @@ -217,6 +224,13 @@ def delete_oidc_provider_config(self, provider_id): _validate_oidc_provider_id(provider_id) self._make_request('delete', '/oauthIdpConfigs/{0}'.format(provider_id)) + def list_oidc_provider_configs(self, page_token=None, max_results=MAX_LIST_CONFIGS_RESULTS): + return _ListOIDCProviderConfigsPage( + self._fetch_oidc_provider_configs, page_token, max_results) + + def _fetch_oidc_provider_configs(self, page_token=None, max_results=MAX_LIST_CONFIGS_RESULTS): + return self._fetch_provider_configs('/oauthIdpConfigs', page_token, max_results) + def get_saml_provider_config(self, provider_id): _validate_saml_provider_id(provider_id) body = self._make_request('get', '/inboundSamlConfigs/{0}'.format(provider_id)) @@ -297,7 +311,10 @@ def list_saml_provider_configs(self, page_token=None, max_results=MAX_LIST_CONFI self._fetch_saml_provider_configs, page_token, max_results) def _fetch_saml_provider_configs(self, page_token=None, max_results=MAX_LIST_CONFIGS_RESULTS): - """Fetches a page of SAML provider configs""" + return self._fetch_provider_configs('/inboundSamlConfigs', page_token, max_results) + + def _fetch_provider_configs(self, path, page_token=None, max_results=MAX_LIST_CONFIGS_RESULTS): + """Fetches a page of auth provider configs""" if page_token is not None: if not isinstance(page_token, str) or not page_token: raise ValueError('Page token must be a non-empty string.') @@ -311,7 +328,7 @@ def _fetch_saml_provider_configs(self, page_token=None, max_results=MAX_LIST_CON params = 'pageSize={0}'.format(max_results) if page_token: params += '&pageToken={0}'.format(page_token) - return self._make_request('get', '/inboundSamlConfigs', params=params) + return self._make_request('get', path, params=params) def _make_request(self, method, path, **kwargs): url = '{0}{1}'.format(self.base_url, path) diff --git a/firebase_admin/auth.py b/firebase_admin/auth.py index c84529883..a2add003c 100644 --- a/firebase_admin/auth.py +++ b/firebase_admin/auth.py @@ -637,6 +637,34 @@ def delete_oidc_provider_config(provider_id, app=None): client.delete_oidc_provider_config(provider_id) +def list_oidc_provider_configs( + page_token=None, max_results=_auth_providers.MAX_LIST_CONFIGS_RESULTS, app=None): + """Retrieves a page of OIDC provider configs from a Firebase project. + + The ``page_token`` argument governs the starting point of the page. The ``max_results`` + argument governs the maximum number of configs that may be included in the returned + page. This function never returns None. If there are no OIDC configs in the Firebase + project, this returns an empty page. + + Args: + page_token: A non-empty page token string, which indicates the starting point of the + page (optional). Defaults to ``None``, which will retrieve the first page of users. + max_results: A positive integer indicating the maximum number of users to include in + the returned page (optional). Defaults to 100, which is also the maximum number + allowed. + app: An App instance (optional). + + Returns: + ListProviderConfigsPage: A ListProviderConfigsPage instance. + + Raises: + ValueError: If max_results or page_token are invalid. + FirebaseError: If an error occurs while retrieving the OIDC provider configs. + """ + client = _get_client(app) + return client.list_oidc_provider_configs(page_token, max_results) + + def get_saml_provider_config(provider_id, app=None): """Returns the SAMLProviderConfig with the given ID. diff --git a/tests/data/list_oidc_provider_configs.json b/tests/data/list_oidc_provider_configs.json new file mode 100644 index 000000000..b2b381304 --- /dev/null +++ b/tests/data/list_oidc_provider_configs.json @@ -0,0 +1,18 @@ +{ + "oauthIdpConfigs": [ + { + "name":"projects/mock-project-id/oauthIdpConfigs/oidc.provider0", + "clientId": "CLIENT_ID", + "issuer": "https://oidc.com/issuer", + "displayName": "oidcProviderName", + "enabled": true + }, + { + "name":"projects/mock-project-id/oauthIdpConfigs/oidc.provider1", + "clientId": "CLIENT_ID", + "issuer": "https://oidc.com/issuer", + "displayName": "oidcProviderName", + "enabled": true + } + ] +} diff --git a/tests/test_auth_providers.py b/tests/test_auth_providers.py index a7dfd4bbe..124aea3cc 100644 --- a/tests/test_auth_providers.py +++ b/tests/test_auth_providers.py @@ -27,6 +27,7 @@ USER_MGT_URL_PREFIX = 'https://identitytoolkit.googleapis.com/v2beta1/projects/mock-project-id' OIDC_PROVIDER_CONFIG_RESPONSE = testutils.resource('oidc_provider_config.json') SAML_PROVIDER_CONFIG_RESPONSE = testutils.resource('saml_provider_config.json') +LIST_OIDC_PROVIDER_CONFIGS_RESPONSE = testutils.resource('list_oidc_provider_configs.json') LIST_SAML_PROVIDER_CONFIGS_RESPONSE = testutils.resource('list_saml_provider_configs.json') CONFIG_NOT_FOUND_RESPONSE = """{ @@ -237,6 +238,109 @@ def test_delete(self, user_mgt_app): assert req.method == 'DELETE' assert req.url == '{0}{1}'.format(USER_MGT_URL_PREFIX, '/oauthIdpConfigs/oidc.provider') + @pytest.mark.parametrize('arg', [None, 'foo', list(), dict(), 0, -1, 101, False]) + def test_invalid_max_results(self, user_mgt_app, arg): + with pytest.raises(ValueError): + auth.list_oidc_provider_configs(max_results=arg, app=user_mgt_app) + + @pytest.mark.parametrize('arg', ['', list(), dict(), 0, -1, 101, False]) + def test_invalid_page_token(self, user_mgt_app, arg): + with pytest.raises(ValueError): + auth.list_oidc_provider_configs(page_token=arg, app=user_mgt_app) + + def test_list_single_page(self, user_mgt_app): + recorder = _instrument_provider_mgt(user_mgt_app, 200, LIST_OIDC_PROVIDER_CONFIGS_RESPONSE) + page = auth.list_oidc_provider_configs(app=user_mgt_app) + + self._assert_page(page) + provider_configs = list(config for config in page.iterate_all()) + assert len(provider_configs) == 2 + + assert len(recorder) == 1 + req = recorder[0] + assert req.method == 'GET' + assert req.url == '{0}{1}'.format(USER_MGT_URL_PREFIX, '/oauthIdpConfigs?pageSize=100') + + def test_list_multiple_pages(self, user_mgt_app): + sample_response = json.loads(OIDC_PROVIDER_CONFIG_RESPONSE) + configs = _create_list_response(sample_response) + + # Page 1 + response = { + 'oauthIdpConfigs': configs[:2], + 'nextPageToken': 'token' + } + recorder = _instrument_provider_mgt(user_mgt_app, 200, json.dumps(response)) + page = auth.list_oidc_provider_configs(max_results=10, app=user_mgt_app) + + self._assert_page(page, next_page_token='token') + assert len(recorder) == 1 + req = recorder[0] + assert req.method == 'GET' + assert req.url == '{0}/oauthIdpConfigs?pageSize=10'.format(USER_MGT_URL_PREFIX) + + # Page 2 (also the last page) + response = {'oauthIdpConfigs': configs[2:]} + recorder = _instrument_provider_mgt(user_mgt_app, 200, json.dumps(response)) + page = page.get_next_page() + + self._assert_page(page, count=1, start=2) + assert len(recorder) == 1 + req = recorder[0] + assert req.method == 'GET' + assert req.url == '{0}/oauthIdpConfigs?pageSize=10&pageToken=token'.format( + USER_MGT_URL_PREFIX) + + def test_paged_iteration(self, user_mgt_app): + sample_response = json.loads(OIDC_PROVIDER_CONFIG_RESPONSE) + configs = _create_list_response(sample_response) + + # Page 1 + response = { + 'oauthIdpConfigs': configs[:2], + 'nextPageToken': 'token' + } + recorder = _instrument_provider_mgt(user_mgt_app, 200, json.dumps(response)) + page = auth.list_oidc_provider_configs(app=user_mgt_app) + iterator = page.iterate_all() + + for index in range(2): + provider_config = next(iterator) + assert provider_config.provider_id == 'oidc.provider{0}'.format(index) + assert len(recorder) == 1 + req = recorder[0] + assert req.method == 'GET' + assert req.url == '{0}/oauthIdpConfigs?pageSize=100'.format(USER_MGT_URL_PREFIX) + + # Page 2 (also the last page) + response = {'oauthIdpConfigs': configs[2:]} + recorder = _instrument_provider_mgt(user_mgt_app, 200, json.dumps(response)) + + provider_config = next(iterator) + assert provider_config.provider_id == 'oidc.provider2' + assert len(recorder) == 1 + req = recorder[0] + assert req.method == 'GET' + assert req.url == '{0}/oauthIdpConfigs?pageSize=100&pageToken=token'.format( + USER_MGT_URL_PREFIX) + + with pytest.raises(StopIteration): + next(iterator) + + def test_list_empty_response(self, user_mgt_app): + response = {'oauthIdpConfigs': []} + _instrument_provider_mgt(user_mgt_app, 200, json.dumps(response)) + page = auth.list_oidc_provider_configs(app=user_mgt_app) + assert len(page.provider_configs) == 0 + provider_configs = list(config for config in page.iterate_all()) + assert len(provider_configs) == 0 + + def test_list_error(self, user_mgt_app): + _instrument_provider_mgt(user_mgt_app, 500, '{"error":"test"}') + with pytest.raises(exceptions.InternalError) as excinfo: + auth.list_oidc_provider_configs(app=user_mgt_app) + assert str(excinfo.value) == 'Unexpected error response: {"error":"test"}' + def test_config_not_found(self, user_mgt_app): _instrument_provider_mgt(user_mgt_app, 500, CONFIG_NOT_FOUND_RESPONSE) @@ -257,6 +361,22 @@ def _assert_provider_config(self, provider_config, want_id='oidc.provider'): assert provider_config.issuer == 'https://oidc.com/issuer' assert provider_config.client_id == 'CLIENT_ID' + def _assert_page(self, page, count=2, start=0, next_page_token=''): + assert isinstance(page, auth.ListProviderConfigsPage) + index = start + assert len(page.provider_configs) == count + for provider_config in page.provider_configs: + self._assert_provider_config(provider_config, want_id='oidc.provider{0}'.format(index)) + index += 1 + + if next_page_token: + assert page.next_page_token == next_page_token + assert page.has_next_page is True + else: + assert page.next_page_token == '' + assert page.has_next_page is False + assert page.get_next_page() is None + class TestSAMLProviderConfig: @@ -497,7 +617,7 @@ def test_list_single_page(self, user_mgt_app): def test_list_multiple_pages(self, user_mgt_app): sample_response = json.loads(SAML_PROVIDER_CONFIG_RESPONSE) - configs = self._create_list_response(sample_response) + configs = _create_list_response(sample_response) # Page 1 response = { @@ -527,7 +647,7 @@ def test_list_multiple_pages(self, user_mgt_app): def test_paged_iteration(self, user_mgt_app): sample_response = json.loads(SAML_PROVIDER_CONFIG_RESPONSE) - configs = self._create_list_response(sample_response) + configs = _create_list_response(sample_response) # Page 1 response = { @@ -602,10 +722,11 @@ def _assert_page(self, page, count=2, start=0, next_page_token=''): assert page.has_next_page is False assert page.get_next_page() is None - def _create_list_response(self, sample_response, count=3): - configs = [] - for idx in range(count): - config = dict(sample_response) - config['name'] += str(idx) - configs.append(config) - return configs + +def _create_list_response(sample_response, count=3): + configs = [] + for idx in range(count): + config = dict(sample_response) + config['name'] += str(idx) + configs.append(config) + return configs diff --git a/tests/test_tenant_mgt.py b/tests/test_tenant_mgt.py index be88cd084..cb2ca7724 100644 --- a/tests/test_tenant_mgt.py +++ b/tests/test_tenant_mgt.py @@ -101,6 +101,7 @@ } } +LIST_OIDC_PROVIDER_CONFIGS_RESPONSE = testutils.resource('list_oidc_provider_configs.json') LIST_SAML_PROVIDER_CONFIGS_RESPONSE = testutils.resource('list_saml_provider_configs.json') INVALID_TENANT_IDS = [None, '', 0, 1, True, False, list(), tuple(), dict()] @@ -776,6 +777,32 @@ def test_delete_oidc_provider_config(self, tenant_mgt_app): assert req.url == '{0}/tenants/tenant-id/oauthIdpConfigs/oidc.provider'.format( PROVIDER_MGT_URL_PREFIX) + def test_list_oidc_provider_configs(self, tenant_mgt_app): + client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app) + recorder = _instrument_provider_mgt(client, 200, LIST_OIDC_PROVIDER_CONFIGS_RESPONSE) + + page = client.list_oidc_provider_configs() + + assert isinstance(page, auth.ListProviderConfigsPage) + index = 0 + assert len(page.provider_configs) == 2 + for provider_config in page.provider_configs: + self._assert_oidc_provider_config( + provider_config, want_id='oidc.provider{0}'.format(index)) + index += 1 + + assert page.next_page_token == '' + assert page.has_next_page is False + assert page.get_next_page() is None + provider_configs = list(config for config in page.iterate_all()) + assert len(provider_configs) == 2 + + assert len(recorder) == 1 + req = recorder[0] + assert req.method == 'GET' + assert req.url == '{0}{1}'.format( + PROVIDER_MGT_URL_PREFIX, '/tenants/tenant-id/oauthIdpConfigs?pageSize=100') + def test_get_saml_provider_config(self, tenant_mgt_app): client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app) recorder = _instrument_provider_mgt(client, 200, SAML_PROVIDER_CONFIG_RESPONSE)