8000 feat(auth): Added list_oidc_provider_configs() API (#444) · firebase/firebase-admin-python@99d152f · GitHub
[go: up one dir, main page]

Skip to content

Commit 99d152f

Browse files
authored
feat(auth): Added list_oidc_provider_configs() API (#444)
1 parent 3b82059 commit 99d152f

File tree

6 files changed

+247
-11
lines changed

6 files changed

+247
-11
lines changed

firebase_admin/_auth_client.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,31 @@ def delete_oidc_provider_config(self, provider_id):
470470
"""
471471
self._provider_manager.delete_oidc_provider_config(provider_id)
472472

473+
def list_oidc_provider_configs(
474+
self, page_token=None, max_results=_auth_providers.MAX_LIST_CONFIGS_RESULTS):
475+
"""Retrieves a page of OIDC provider configs from a Firebase project.
476+
477+
The ``page_token`` argument governs the starting point of the page. The ``max_results``
478+
argument governs the maximum number of configs that may be included in the returned
479+
page. This function never returns None. If there are no OIDC configs in the Firebase
480+
project, this returns an empty page.
481+
482+
Args:
483+
page_token: A non-empty page token string, which indicates the starting point of the
484+
page (optional). Defaults to ``None``, which will retrieve the first page of users.
485+
max_results: A positive integer indicating the maximum number of users to include in
486+
the returned page (optional). Defaults to 100, which is also the maximum number
487+
allowed.
488+
489+
Returns:
490+
ListProviderConfigsPage: A ListProviderConfigsPage instance.
491+
492+
Raises:
493+
ValueError: If max_results or page_token are invalid.
494+
FirebaseError: If an error occurs while retrieving the OIDC provider configs.
495+
"""
496+
return self._provider_manager.list_oidc_provider_configs(page_token, max_results)
497+
473498
def get_saml_provider_config(self, provider_id):
474499
"""Returns the SAMLProviderConfig with the given ID.
475500

firebase_admin/_auth_providers.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,13 @@ def iterate_all(self):
140140
return _ProviderConfigIterator(self)
141141

142142

143+
class _ListOIDCProviderConfigsPage(ListProviderConfigsPage):
144+
145+
@property
146+
def provider_configs(self):
147+
return [OIDCProviderConfig(data) for data in self._current.get('oauthIdpConfigs', [])]
148+
149+
143150
class _ListSAMLProviderConfigsPage(ListProviderConfigsPage):
144151

145152
@property
@@ -217,6 +224,13 @@ def delete_oidc_provider_config(self, provider_id):
217224
_validate_oidc_provider_id(provider_id)
218225
self._make_request('delete', '/oauthIdpConfigs/{0}'.format(provider_id))
219226

227+
def list_oidc_provider_configs(self, page_token=None, max_results=MAX_LIST_CONFIGS_RESULTS):
228+
return _ListOIDCProviderConfigsPage(
229+
self._fetch_oidc_provider_configs, page_token, max_results)
230+
231+
def _fetch_oidc_provider_configs(self, page_token=None, max_results=MAX_LIST_CONFIGS_RESULTS):
232+
return self._fetch_provider_configs('/oauthIdpConfigs', page_token, max_results)
233+
220234
def get_saml_provider_config(self, provider_id):
221235
_validate_saml_provider_id(provider_id)
222236
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
297311
self._fetch_saml_provider_configs, page_token, max_results)
298312

299313
def _fetch_saml_provider_configs(self, page_token=None, max_results=MAX_LIST_CONFIGS_RESULTS):
300-
"""Fetches a page of SAML provider configs"""
314+
return self._fetch_provider_configs('/inboundSamlConfigs', page_token, max_results)
315+
316+
def _fetch_provider_configs(self, path, page_token=None, max_results=MAX_LIST_CONFIGS_RESULTS):
317+
"""Fetches a page of auth provider configs"""
301318
if page_token is not None:
302319
if not isinstance(page_token, str) or not page_token:
303320
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
311328
params = 'pageSize={0}'.format(max_results)
312329
if page_token:
313330
params += '&pageToken={0}'.format(page_token)
314-
return self._make_request('get', '/inboundSamlConfigs', params=params)
331+
return self._make_request('get', path, params=params)
315332

316333
def _make_request(self, method, path, **kwargs):
317334
url = '{0}{1}'.format(self.base_url, path)

firebase_admin/auth.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,34 @@ def delete_oidc_provider_config(provider_id, app=None):
637637
client.delete_oidc_provider_config(provider_id)
638638

639639

640+
def list_oidc_provider_configs(
641+
page_token=None, max_results=_auth_providers.MAX_LIST_CONFIGS_RESULTS, app=None):
642+
"""Retrieves a page of OIDC provider configs from a Firebase project.
643+
644+
The ``page_token`` argument governs the starting point of the page. The ``max_results``
645+
argument governs the maximum number of configs that may be included in the returned
646+
page. This function never returns None. If there are no OIDC configs in the Firebase
647+
project, this returns an empty page.
648+
649+
Args:
650+
page_token: A non-empty page token string, which indicates the starting point of the
651+
page (optional). Defaults to ``None``, which will retrieve the first page of users.
652+
max_results: A positive integer indicating the maximum number of users to include in
653+
the returned page (optional). Defaults to 100, which is also the maximum number
654+
allowed.
655+
app: An App instance (optional).
656+
657+
Returns:
658+
ListProviderConfigsPage: A ListProviderConfigsPage instance.
659+
660+
Raises:
661+
ValueError: If max_results or page_token are invalid.
662+
FirebaseError: If an error occurs while retrieving the OIDC provider configs.
663+
"""
664+
client = _get_client(app)
665+
return client.list_oidc_provider_configs(page_token, max_results)
666+
667+
640668
def get_saml_provider_config(provider_id, app=None):
641669
"""Returns the SAMLProviderConfig with the given ID.
642670
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"oauthIdpConfigs": [
3+
{
4+
"name":"projects/mock-project-id/oauthIdpConfigs/oidc.provider0",
5+
"clientId": "CLIENT_ID",
6+
"issuer": "https://oidc.com/issuer",
7+
"displayName": "oidcProviderName",
8+
"enabled": true
9+
},
10+
{
11+
"name":"projects/mock-project-id/oauthIdpConfigs/oidc.provider1",
12+
"clientId": "CLIENT_ID",
13+
"issuer": "https://oidc.com/issuer",
14+
"displayName": "oidcProviderName",
15+
"enabled": true
16+
}
17+
]
18+
}

tests/test_auth_providers.py

Lines changed: 130 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
USER_MGT_URL_PREFIX = 'https://identitytoolkit.googleapis.com/v2beta1/projects/mock-project-id'
2828
OIDC_PROVIDER_CONFIG_RESPONSE = testutils.resource('oidc_provider_config.json')
2929
SAML_PROVIDER_CONFIG_RESPONSE = testutils.resource('saml_provider_config.json')
30+
LIST_OIDC_PROVIDER_CONFIGS_RESPONSE = testutils.resource('list_oidc_provider_configs.json')
3031
LIST_SAML_PROVIDER_CONFIGS_RESPONSE = testutils.resource('list_saml_provider_configs.json')
3132

3233
CONFIG_NOT_FOUND_RESPONSE = """{
@@ -237,6 +238,109 @@ def test_delete(self, user_mgt_app):
237238
assert req.method == 'DELETE'
238239
assert req.url == '{0}{1}'.format(USER_MGT_URL_PREFIX, '/oauthIdpConfigs/oidc.provider')
239240

241+
@pytest.mark.parametrize('arg', [None, 'foo', list(), dict(), 0, -1, 101, False])
242+
def test_invalid_max_results(self, user_mgt_app, arg):
243+
with pytest.raises(ValueError):
244+
auth.list_oidc_provider_configs(max_results=arg, app=user_mgt_app)
245+
246+
@pytest.mark.parametrize('arg', ['', list(), dict(), 0, -1, 101, False])
247+
def test_invalid_page_token(self, user_mgt_app, arg):
248+
with pytest.raises(ValueError):
249+
auth.list_oidc_provider_configs(page_token=arg, app=user_mgt_app)
250+
251+
def test_list_single_page(self, user_mgt_app):
252+
recorder = _instrument_provider_mgt(user_mgt_app, 200, LIST_OIDC_PROVIDER_CONFIGS_RESPONSE)
253+
page = auth.list_oidc_provider_configs(app=user_mgt_app)
254+
255+
self._assert_page(page)
256+
provider_configs = list(config for config in page.iterate_all())
257+
assert len(provider_configs) == 2
258+
259+
assert len(recorder) == 1
260+
req = recorder[0]
261+
assert req.method == 'GET'
262+
assert req.url == '{0}{1}'.format(USER_MGT_URL_PREFIX, '/oauthIdpConfigs?pageSize=100')
263+
264+
def test_list_multiple_pages(self, user_mgt_app):
265+
sample_response = json.loads(OIDC_PROVIDER_CONFIG_RESPONSE)
266+
configs = _create_list_response(sample_response)
267+
268+
# Page 1
269+
response = {
270+
'oauthIdpConfigs': configs[:2],
271+
'nextPageToken': 'token'
272+
}
273+
recorder = _instrument_provider_mgt(user_mgt_app, 200, json.dumps(response))
274+
page = auth.list_oidc_provider_configs(max_results=10, app=user_mgt_app)
275+
276+
self._assert_page(page, next_page_token='token')
277+
assert len(recorder) == 1
278+
req = recorder[0]
279+
assert req.method == 'GET'
280+
assert req.url == '{0}/oauthIdpConfigs?pageSize=10'.format(USER_MGT_URL_PREFIX)
281+
282+
# Page 2 (also the last page)
283+
response = {'oauthIdpConfigs': configs[2:]}
284+
recorder = _instrument_provider_mgt(user_mgt_app, 200, json.dumps(response))
285+
page = page.get_next_page()
286+
287+
self._assert_page(page, count=1, start=2)
288+
assert len(recorder) == 1
289+
req = recorder[0]
290+
assert req.method == 'GET'
291+
assert req.url == '{0}/oauthIdpConfigs?pageSize=10&pageToken=token'.format(
292+
USER_MGT_URL_PREFIX)
293+
294+
def test_paged_iteration(self, user_mgt_app):
295+
sample_response = json.loads(OIDC_PROVIDER_CONFIG_RESPONSE)
296+
configs = _create_list_response(sample_response)
297+
298+
# Page 1
299+
response = {
300+
'oauthIdpConfigs': configs[:2],
301+
'nextPageToken': 'token'
302+
}
303+
recorder = _instrument_provider_mgt(user_mgt_app, 200, json.dumps(response))
304+
page = auth.list_oidc_provider_configs(app=user_mgt_app)
305+
iterator = page.iterate_all()
306+
307+
for index in range(2):
308+
provider_config = next(iterator)
309+
assert provider_config.provider_id == 'oidc.provider{0}'.format(index)
310+
assert len(recorder) == 1
311+
req = recorder[0]
312+
assert req.method == 'GET'
313+
assert req.url == '{0}/oauthIdpConfigs?pageSize=100'.format(USER_MGT_URL_PREFIX)
314+
315+
# Page 2 (also the last page)
316+
response = {'oauthIdpConfigs': configs[2:]}
317+
recorder = _instrument_provider_mgt(user_mgt_app, 200, json.dumps(response))
318+
319+
provider_config = next(iterator)
320+
assert provider_config.provider_id == 'oidc.provider2'
321+
assert len(recorder) == 1
322+
req = recorder[0]
323+
assert req.method == 'GET'
324+
assert req.url == '{0}/oauthIdpConfigs?pageSize=100&pageToken=token'.format(
325+
USER_MGT_URL_PREFIX)
326+
327+
with pytest.raises(StopIteration):
328+
next(iterator)
329+
330+
def test_list_empty_response(self, user_mgt_app):
331+
response = {'oauthIdpConfigs': []}
332+
_instrument_provider_mgt(user_mgt_app, 200, json.dumps(response))
333+
page = auth.list_oidc_provider_configs(app=user_mgt_app)
334+
assert len(page.provider_configs) == 0
335+
provider_configs = list(config for config in page.iterate_all())
336+
assert len(provider_configs) == 0
337+
338+
def test_list_error(self, user_mgt_app):
339+
_instrument_provider_mgt(user_mgt_app, 500, '{"error":"test"}')
340+
with pytest.raises(exceptions.InternalError) as excinfo:
341+
auth.list_oidc_provider_configs(app=user_mgt_app)
342+
assert str(excinfo.value) == 'Unexpected error response: {"error":"test"}'
343+
240344
def test_config_not_found(self, user_mgt_app):
241345
_instrument_provider_mgt(user_mgt_app, 500, CONFIG_NOT_FOUND_RESPONSE)
242346

@@ -257,6 +361,22 @@ def _assert_provider_config(self, provider_config, want_id='oidc.provider'):
257361
assert provider_config.issuer == 'https://oidc.com/issuer'
258362
assert provider_config.client_id == 'CLIENT_ID'
259363

364+
def _assert_page(self, page, count=2, start=0, next_page_token=''):
365+
assert isinstance(page, auth.ListProviderConfigsPage)
366+
index = start
367+
assert len(page.provider_configs) == count
368+
for provider_config in page.provider_configs:
369+
self._assert_provider_config(provider_config, want_id='oidc.provider{0}'.format(index))
370+
index += 1
371+
372+
if next_page_token:
373+
assert page.next_page_token == next_page_token
374+
assert page.has_next_page is True
375+
else:
376+
assert page.next_page_token == ''
377+
assert page.has_next_page is False
378+
assert page.get_next_page() is None
379+
260380

261381
class TestSAMLProviderConfig:
262382

@@ -497,7 +617,7 @@ def test_list_single_page(self, user_mgt_app):
497617

498618
def test_list_multiple_pages(self, user_mgt_app):
499619
sample_response = json.loads(SAML_PROVIDER_CONFIG_RESPONSE)
500-
configs = self._create_list_response(sample_response)
620+
configs = _create_list_response(sample_response)
501621

502622
# Page 1
503623
response = {
@@ -527,7 +647,7 @@ def test_list_multiple_pages(self, user_mgt_app):
527647

528648
def test_paged_iteration(self, user_mgt_app):
529649
sample_response = json.loads(SAML_PROVIDER_CONFIG_RESPONSE)
530-
configs = self._create_list_response(sample_response)
650+
configs = _create_list_response(sample_response)
531651

532652
# Page 1
533653
response = {
@@ -602,10 +722,11 @@ def _assert_page(self, page, count=2, start=0, next_page_token=''):
602722
assert page.has_next_page is False
603723
assert page.get_next_page() is None
604724

605-
def _create_list_response(self, sample_response, count=3):
606-
configs = []
607-
for idx in range(count):
608-
config = dict(sample_response)
609-
config['name'] += str(idx)
610-
configs.append(config)
611-
return configs
725+
726+
def _create_list_response(sample_response, count=3):
727+
configs = []
728+
for idx in range(count):
729+
config = dict(sample_response)
730+
config['name'] += str(idx)
731+
configs.append(config)
732+
return configs

tests/test_tenant_mgt.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
}
102102
}
103103

104+
LIST_OIDC_PROVIDER_CONFIGS_RESPONSE = testutils.resource('list_oidc_provider_configs.json')
104105
LIST_SAML_PROVIDER_CONFIGS_RESPONSE = testutils.resource('list_saml_provider_configs.json')
105106

106107
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):
776777
assert req.url == '{0}/tenants/tenant-id/oauthIdpConfigs/oidc.provider'.format(
777778
PROVIDER_MGT_URL_PREFIX)
778779

780+
def test_list_oidc_provider_configs(self, tenant_mgt_app):
781+
client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app)
782+
recorder = _instrument_provider_mgt(client, 200, LIST_OIDC_PROVIDER_CONFIGS_RESPONSE)
783+
784+
page = client.list_oidc_provider_configs()
785+
786+
assert isinstance(page, auth.ListProviderConfigsPage)
787+
index = 0
788+
assert len(page.provider_configs) == 2
789+
for provider_config in page.provider_configs:
790+
self._assert_oidc_provider_config(
791+
provider_config, want_id='oidc.provider{0}'.format(index))
792+
index += 1
793+
794+
assert page.next_page_token == ''
795+
assert page.has_next_page is False
796+
assert page.get_next_page() is None
797+
provider_configs = list(config for config in page.iterate_all())
798+
assert len(provider_configs) == 2
799+
800+
assert len(recorder) == 1
801+
req = recorder[0]
802+
assert req.method == 'GET'
803+
assert req.url == '{0}{1}'.format(
804+
PROVIDER_MGT_URL_PREFIX, '/tenants/tenant-id/oauthIdpConfigs?pageSize=100')
805+
779806
def test_get_saml_provider_config(self, tenant_mgt_app):
780807
client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app)
781808
recorder = _instrument_provider_mgt(client, 200, SAML_PROVIDER_CONFIG_RESPONSE)

0 commit comments

Comments
 (0)
0