Skip to content

OIDCProviderConfig create/update APIs #443

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions firebase_admin/_auth_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
43 changes: 43 additions & 0 deletions firebase_admin/_auth_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
56 changes: 56 additions & 0 deletions firebase_admin/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
144 changes: 144 additions & 0 deletions tests/test_auth_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
35 changes: 35 additions & 0 deletions tests/test_tenant_mgt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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, '{}')
Expand Down