diff --git a/firebase_admin/_auth_client.py b/firebase_admin/_auth_client.py index 2d2b21437..e147d85ae 100644 --- a/firebase_admin/_auth_client.py +++ b/firebase_admin/_auth_client.py @@ -406,6 +406,57 @@ def get_oidc_provider_config(self, provider_id): """ return self._provider_manager.get_oidc_provider_config(provider_id) + def create_oidc_provider_config( + self, provider_id, client_id, issuer, display_name=None, enabled=None): + """Creates a new OIDC provider config from the given parameters. + + OIDC provider support requires Google Cloud's Identity Platform (GCIP). To learn more about + GCIP, including pricing and features, see https://cloud.google.com/identity-platform. + + Args: + provider_id: Provider ID string. Must have the prefix ``oidc.``. + client_id: Client ID of the new config. + issuer: Issuer of the new config. Must be a valid URL. + display_name: The user-friendly display name to the current configuration (optional). + This name is also used as the provider label in the Cloud Console. + enabled: A boolean indicating whether the provider configuration is enabled or disabled + (optional). A user cannot sign in using a disabled provider. + + Returns: + OIDCProviderConfig: The newly created OIDCProviderConfig instance. + + Raises: + ValueError: If any of the specified input parameters are invalid. + FirebaseError: If an error occurs while creating the new OIDC provider config. + """ + return self._provider_manager.create_oidc_provider_config( + provider_id, client_id=client_id, issuer=issuer, display_name=display_name, + enabled=enabled) + + def update_oidc_provider_config( + self, provider_id, client_id=None, issuer=None, display_name=None, enabled=None): + """Updates an existing OIDC provider config with the given parameters. + + Args: + provider_id: Provider ID string. Must have the prefix ``oidc.``. + client_id: Client ID of the new config (optional). + issuer: Issuer of the new config (optional). Must be a valid URL. + display_name: The user-friendly display name to the current configuration (optional). + Pass ``auth.DELETE_ATTRIBUTE`` to delete the current display name. + enabled: A boolean indicating whether the provider configuration is enabled or disabled + (optional). + + Returns: + OIDCProviderConfig: The updated OIDCProviderConfig instance. + + Raises: + ValueError: If any of the specified input parameters are invalid. + FirebaseError: If an error occurs while updating the OIDC provider config. + """ + return self._provider_manager.update_oidc_provider_config( + provider_id, client_id=client_id, issuer=issuer, display_name=display_name, + enabled=enabled) + def delete_oidc_provider_config(self, provider_id): """Deletes the OIDCProviderConfig with the given ID. diff --git a/firebase_admin/_auth_providers.py b/firebase_admin/_auth_providers.py index 309b2ed1f..121105bc3 100644 --- a/firebase_admin/_auth_providers.py +++ b/firebase_admin/_auth_providers.py @@ -170,6 +170,49 @@ def get_oidc_provider_config(self, provider_id): body = self._make_request('get', '/oauthIdpConfigs/{0}'.format(provider_id)) return OIDCProviderConfig(body) + def create_oidc_provider_config( + self, provider_id, client_id, issuer, display_name=None, enabled=None): + """Creates a new OIDC provider config from the given parameters.""" + _validate_oidc_provider_id(provider_id) + req = { + 'clientId': _validate_non_empty_string(client_id, 'client_id'), + 'issuer': _validate_url(issuer, 'issuer'), + } + if display_name is not None: + req['displayName'] = _auth_utils.validate_string(display_name, 'display_name') + if enabled is not None: + req['enabled'] = _auth_utils.validate_boolean(enabled, 'enabled') + + params = 'oauthIdpConfigId={0}'.format(provider_id) + body = self._make_request('post', '/oauthIdpConfigs', json=req, params=params) + return OIDCProviderConfig(body) + + def update_oidc_provider_config( + self, provider_id, client_id=None, issuer=None, display_name=None, enabled=None): + """Updates an existing OIDC provider config with the given parameters.""" + _validate_oidc_provider_id(provider_id) + req = {} + if display_name is not None: + if display_name == _user_mgt.DELETE_ATTRIBUTE: + req['displayName'] = None + else: + req['displayName'] = _auth_utils.validate_string(display_name, 'display_name') + if enabled is not None: + req['enabled'] = _auth_utils.validate_boolean(enabled, 'enabled') + if client_id: + req['clientId'] = _validate_non_empty_string(client_id, 'client_id') + if issuer: + req['issuer'] = _validate_url(issuer, 'issuer') + + if not req: + raise ValueError('At least one parameter must be specified for update.') + + update_mask = _auth_utils.build_update_mask(req) + params = 'updateMask={0}'.format(','.join(update_mask)) + url = '/oauthIdpConfigs/{0}'.format(provider_id) + body = self._make_request('patch', url, json=req, params=params) + return OIDCProviderConfig(body) + def delete_oidc_provider_config(self, provider_id): _validate_oidc_provider_id(provider_id) self._make_request('delete', '/oauthIdpConfigs/{0}'.format(provider_id)) diff --git a/firebase_admin/auth.py b/firebase_admin/auth.py index 536be23a2..c84529883 100644 --- a/firebase_admin/auth.py +++ b/firebase_admin/auth.py @@ -564,6 +564,62 @@ def get_oidc_provider_config(provider_id, app=None): client = _get_client(app) return client.get_oidc_provider_config(provider_id) +def create_oidc_provider_config( + provider_id, client_id, issuer, display_name=None, enabled=None, app=None): + """Creates a new OIDC provider config from the given parameters. + + OIDC provider support requires Google Cloud's Identity Platform (GCIP). To learn more about + GCIP, including pricing and features, see https://cloud.google.com/identity-platform. + + Args: + provider_id: Provider ID string. Must have the prefix ``oidc.``. + client_id: Client ID of the new config. + issuer: Issuer of the new config. Must be a valid URL. + display_name: The user-friendly display name to the current configuration (optional). + This name is also used as the provider label in the Cloud Console. + enabled: A boolean indicating whether the provider configuration is enabled or disabled + (optional). A user cannot sign in using a disabled provider. + app: An App instance (optional). + + Returns: + OIDCProviderConfig: The newly created OIDCProviderConfig instance. + + Raises: + ValueError: If any of the specified input parameters are invalid. + FirebaseError: If an error occurs while creating the new OIDC provider config. + """ + client = _get_client(app) + return client.create_oidc_provider_config( + provider_id, client_id=client_id, issuer=issuer, display_name=display_name, + enabled=enabled) + + +def update_oidc_provider_config( + provider_id, client_id=None, issuer=None, display_name=None, enabled=None, app=None): + """Updates an existing OIDC provider config with the given parameters. + + Args: + provider_id: Provider ID string. Must have the prefix ``oidc.``. + client_id: Client ID of the new config (optional). + issuer: Issuer of the new config (optional). Must be a valid URL. + display_name: The user-friendly display name to the current configuration (optional). + Pass ``auth.DELETE_ATTRIBUTE`` to delete the current display name. + enabled: A boolean indicating whether the provider configuration is enabled or disabled + (optional). + app: An App instance (optional). + + Returns: + OIDCProviderConfig: The updated OIDCProviderConfig instance. + + Raises: + ValueError: If any of the specified input parameters are invalid. + FirebaseError: If an error occurs while updating the OIDC provider config. + """ + client = _get_client(app) + return client.update_oidc_provider_config( + provider_id, client_id=client_id, issuer=issuer, display_name=display_name, + enabled=enabled) + def delete_oidc_provider_config(provider_id, app=None): """Deletes the OIDCProviderConfig with the given ID. diff --git a/tests/test_auth_providers.py b/tests/test_auth_providers.py index 531dcebea..a7dfd4bbe 100644 --- a/tests/test_auth_providers.py +++ b/tests/test_auth_providers.py @@ -58,6 +58,21 @@ def _instrument_provider_mgt(app, status, payload): class TestOIDCProviderConfig: + VALID_CREATE_OPTIONS = { + 'provider_id': 'oidc.provider', + 'client_id': 'CLIENT_ID', + 'issuer': 'https://oidc.com/issuer', + 'display_name': 'oidcProviderName', + 'enabled': True, + } + + OIDC_CONFIG_REQUEST = { + 'displayName': 'oidcProviderName', + 'enabled': True, + 'clientId': 'CLIENT_ID', + 'issuer': 'https://oidc.com/issuer', + } + @pytest.mark.parametrize('provider_id', INVALID_PROVIDER_IDS + ['saml.provider']) def test_get_invalid_provider_id(self, user_mgt_app, provider_id): with pytest.raises(ValueError) as excinfo: @@ -76,6 +91,135 @@ def test_get(self, user_mgt_app): assert req.method == 'GET' assert req.url == '{0}{1}'.format(USER_MGT_URL_PREFIX, '/oauthIdpConfigs/oidc.provider') + @pytest.mark.parametrize('invalid_opts', [ + {'provider_id': None}, {'provider_id': ''}, {'provider_id': 'saml.provider'}, + {'client_id': None}, {'client_id': ''}, + {'issuer': None}, {'issuer': ''}, {'issuer': 'not a url'}, + {'display_name': True}, + {'enabled': 'true'}, + ]) + def test_create_invalid_args(self, user_mgt_app, invalid_opts): + options = dict(self.VALID_CREATE_OPTIONS) + options.update(invalid_opts) + with pytest.raises(ValueError): + auth.create_oidc_provider_config(**options, app=user_mgt_app) + + def test_create(self, user_mgt_app): + recorder = _instrument_provider_mgt(user_mgt_app, 200, OIDC_PROVIDER_CONFIG_RESPONSE) + + provider_config = auth.create_oidc_provider_config( + **self.VALID_CREATE_OPTIONS, app=user_mgt_app) + + self._assert_provider_config(provider_config) + assert len(recorder) == 1 + req = recorder[0] + assert req.method == 'POST' + assert req.url == '{0}/oauthIdpConfigs?oauthIdpConfigId=oidc.provider'.format( + USER_MGT_URL_PREFIX) + got = json.loads(req.body.decode()) + assert got == self.OIDC_CONFIG_REQUEST + + def test_create_minimal(self, user_mgt_app): + recorder = _instrument_provider_mgt(user_mgt_app, 200, OIDC_PROVIDER_CONFIG_RESPONSE) + options = dict(self.VALID_CREATE_OPTIONS) + del options['display_name'] + del options['enabled'] + want = dict(self.OIDC_CONFIG_REQUEST) + del want['displayName'] + del want['enabled'] + + provider_config = auth.create_oidc_provider_config(**options, app=user_mgt_app) + + self._assert_provider_config(provider_config) + assert len(recorder) == 1 + req = recorder[0] + assert req.method == 'POST' + assert req.url == '{0}/oauthIdpConfigs?oauthIdpConfigId=oidc.provider'.format( + USER_MGT_URL_PREFIX) + got = json.loads(req.body.decode()) + assert got == want + + def test_create_empty_values(self, user_mgt_app): + recorder = _instrument_provider_mgt(user_mgt_app, 200, OIDC_PROVIDER_CONFIG_RESPONSE) + options = dict(self.VALID_CREATE_OPTIONS) + options['display_name'] = '' + options['enabled'] = False + want = dict(self.OIDC_CONFIG_REQUEST) + want['displayName'] = '' + want['enabled'] = False + + provider_config = auth.create_oidc_provider_config(**options, app=user_mgt_app) + + self._assert_provider_config(provider_config) + assert len(recorder) == 1 + req = recorder[0] + assert req.method == 'POST' + assert req.url == '{0}/oauthIdpConfigs?oauthIdpConfigId=oidc.provider'.format( + USER_MGT_URL_PREFIX) + got = json.loads(req.body.decode()) + assert got == want + + @pytest.mark.parametrize('invalid_opts', [ + {}, + {'provider_id': None}, {'provider_id': ''}, {'provider_id': 'saml.provider'}, + {'client_id': ''}, + {'issuer': ''}, {'issuer': 'not a url'}, + {'display_name': True}, + {'enabled': 'true'}, + ]) + def test_update_invalid_args(self, user_mgt_app, invalid_opts): + options = {'provider_id': 'oidc.provider'} + options.update(invalid_opts) + with pytest.raises(ValueError): + auth.update_oidc_provider_config(**options, app=user_mgt_app) + + def test_update(self, user_mgt_app): + recorder = _instrument_provider_mgt(user_mgt_app, 200, OIDC_PROVIDER_CONFIG_RESPONSE) + + provider_config = auth.update_oidc_provider_config( + **self.VALID_CREATE_OPTIONS, app=user_mgt_app) + + self._assert_provider_config(provider_config) + assert len(recorder) == 1 + req = recorder[0] + assert req.method == 'PATCH' + mask = ['clientId', 'displayName', 'enabled', 'issuer'] + assert req.url == '{0}/oauthIdpConfigs/oidc.provider?updateMask={1}'.format( + USER_MGT_URL_PREFIX, ','.join(mask)) + got = json.loads(req.body.decode()) + assert got == self.OIDC_CONFIG_REQUEST + + def test_update_minimal(self, user_mgt_app): + recorder = _instrument_provider_mgt(user_mgt_app, 200, OIDC_PROVIDER_CONFIG_RESPONSE) + + provider_config = auth.update_oidc_provider_config( + 'oidc.provider', display_name='oidcProviderName', app=user_mgt_app) + + self._assert_provider_config(provider_config) + assert len(recorder) == 1 + req = recorder[0] + assert req.method == 'PATCH' + assert req.url == '{0}/oauthIdpConfigs/oidc.provider?updateMask=displayName'.format( + USER_MGT_URL_PREFIX) + got = json.loads(req.body.decode()) + assert got == {'displayName': 'oidcProviderName'} + + def test_update_empty_values(self, user_mgt_app): + recorder = _instrument_provider_mgt(user_mgt_app, 200, OIDC_PROVIDER_CONFIG_RESPONSE) + + provider_config = auth.update_oidc_provider_config( + 'oidc.provider', display_name=auth.DELETE_ATTRIBUTE, enabled=False, app=user_mgt_app) + + self._assert_provider_config(provider_config) + assert len(recorder) == 1 + req = recorder[0] + assert req.method == 'PATCH' + mask = ['displayName', 'enabled'] + assert req.url == '{0}/oauthIdpConfigs/oidc.provider?updateMask={1}'.format( + USER_MGT_URL_PREFIX, ','.join(mask)) + got = json.loads(req.body.decode()) + assert got == {'displayName': None, 'enabled': False} + @pytest.mark.parametrize('provider_id', INVALID_PROVIDER_IDS + ['saml.provider']) def test_delete_invalid_provider_id(self, user_mgt_app, provider_id): with pytest.raises(ValueError) as excinfo: diff --git a/tests/test_tenant_mgt.py b/tests/test_tenant_mgt.py index 06b1064fe..be88cd084 100644 --- a/tests/test_tenant_mgt.py +++ b/tests/test_tenant_mgt.py @@ -79,6 +79,13 @@ MOCK_LIST_USERS_RESPONSE = testutils.resource('list_users.json') OIDC_PROVIDER_CONFIG_RESPONSE = testutils.resource('oidc_provider_config.json') +OIDC_PROVIDER_CONFIG_REQUEST = { + 'displayName': 'oidcProviderName', + 'enabled': True, + 'clientId': 'CLIENT_ID', + 'issuer': 'https://oidc.com/issuer', +} + SAML_PROVIDER_CONFIG_RESPONSE = testutils.resource('saml_provider_config.json') SAML_PROVIDER_CONFIG_REQUEST = body = { 'displayName': 'samlProviderName', @@ -729,6 +736,34 @@ def test_get_oidc_provider_config(self, tenant_mgt_app): assert req.url == '{0}/tenants/tenant-id/oauthIdpConfigs/oidc.provider'.format( PROVIDER_MGT_URL_PREFIX) + def test_create_oidc_provider_config(self, tenant_mgt_app): + client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app) + recorder = _instrument_provider_mgt(client, 200, OIDC_PROVIDER_CONFIG_RESPONSE) + + provider_config = client.create_oidc_provider_config( + 'oidc.provider', client_id='CLIENT_ID', issuer='https://oidc.com/issuer', + display_name='oidcProviderName', enabled=True) + + self._assert_oidc_provider_config(provider_config) + self._assert_request( + recorder, '/oauthIdpConfigs?oauthIdpConfigId=oidc.provider', + OIDC_PROVIDER_CONFIG_REQUEST, prefix=PROVIDER_MGT_URL_PREFIX) + + def test_update_oidc_provider_config(self, tenant_mgt_app): + client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app) + recorder = _instrument_provider_mgt(client, 200, OIDC_PROVIDER_CONFIG_RESPONSE) + + provider_config = client.update_oidc_provider_config( + 'oidc.provider', client_id='CLIENT_ID', issuer='https://oidc.com/issuer', + display_name='oidcProviderName', enabled=True) + + self._assert_oidc_provider_config(provider_config) + mask = ['clientId', 'displayName', 'enabled', 'issuer'] + url = '/oauthIdpConfigs/oidc.provider?updateMask={0}'.format(','.join(mask)) + self._assert_request( + recorder, url, OIDC_PROVIDER_CONFIG_REQUEST, method='PATCH', + prefix=PROVIDER_MGT_URL_PREFIX) + def test_delete_oidc_provider_config(self, tenant_mgt_app): client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app) recorder = _instrument_provider_mgt(client, 200, '{}')