diff --git a/docs/appendix.rst b/docs/appendix.rst index 20365862..72535a62 100644 --- a/docs/appendix.rst +++ b/docs/appendix.rst @@ -401,6 +401,10 @@ Resources scoped to the HMC The information in an HMC about an MFA server that may be used for HMC user authorization purposes. + SSO Server Definition + The information in an HMC about an SSO server that may be used for + HMC user authorization purposes. + Password Rule A rule which HMC users need to follow when creating a HMC logon password. diff --git a/tests/end2end/test_sso_server_definition.py b/tests/end2end/test_sso_server_definition.py new file mode 100644 index 00000000..1e59db25 --- /dev/null +++ b/tests/end2end/test_sso_server_definition.py @@ -0,0 +1,217 @@ +# Copyright 2021 IBM Corp. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +End2end tests for SSO server definitions (on CPCs in DPM mode). + +These tests do not change any existing SSO server definitions, but create, +modify and delete test SSO server definitions. +""" + + +import warnings +import pytest +from requests.packages import urllib3 + +import zhmcclient + +from .utils import skip_warn, pick_test_resources, TEST_PREFIX, \ + runtest_find_list, runtest_get_properties + +urllib3.disable_warnings() + +# Properties in minimalistic SSOServerDefinition objects (e.g. find_by_name()) +SSOSRVDEF_MINIMAL_PROPS = ['element-uri', 'name'] + +# Properties in SSOServerDefinition objects returned by list() without full +# props +SSOSRVDEF_LIST_PROPS = ['element-uri', 'name','type'] + +# Properties whose values can change between retrievals of SSOServerDefinition +# objects +SSOSRVDEF_VOLATILE_PROPS = [] + + +def test_ssosrvdef_find_list(hmc_session): + """ + Test list(), find(), findall(). + """ + client = zhmcclient.Client(hmc_session) + console = client.consoles.console + hd = hmc_session.hmc_definition + + api_version = client.query_api_version() + hmc_version = api_version['hmc-version'] + hmc_version_info = tuple(map(int, hmc_version.split('.'))) + if hmc_version_info < (2, 17, 0): + skip_warn(f"HMC {hd.host} of version {hmc_version} does not yet " + "support SSO server definitions") + + # Pick the SSO server definitions to test with + ssosrvdef_list = console.sso_server_definitions.list() + if not ssosrvdef_list: + skip_warn(f"No SSO server definitions defined on HMC {hd.host}") + ssosrvdef_list = pick_test_resources(ssosrvdef_list) + + for ssosrvdef in ssosrvdef_list: + print(f"Testing with SSO server definition {ssosrvdef.name!r}") + runtest_find_list( + hmc_session, console.sso_server_definitions, ssosrvdef.name, + 'name', 'element-uri', SSOSRVDEF_VOLATILE_PROPS, + SSOSRVDEF_MINIMAL_PROPS, SSOSRVDEF_LIST_PROPS) + + +def test_ssosrvdef_property(hmc_session): + """ + Test property related methods + """ + client = zhmcclient.Client(hmc_session) + console = client.consoles.console + hd = hmc_session.hmc_definition + + api_version = client.query_api_version() + hmc_version = api_version['hmc-version'] + hmc_version_info = tuple(map(int, hmc_version.split('.'))) + if hmc_version_info < (2, 17, 0): + skip_warn(f"HMC {hd.host} of version {hmc_version} does not yet " + "support SSO server definitions") + + # Pick the SSO server definitions to test with + ssosrvdef_list = console.sso_server_definitions.list() + if not ssosrvdef_list: + skip_warn(f"No SSO server definitions defined on HMC {hd.host}") + ssosrvdef_list = pick_test_resources(ssosrvdef_list) + + for ssosrvdef in ssosrvdef_list: + print(f"Testing with SSO server definition {ssosrvdef.name!r}") + + # Select a property that is not returned by list() + non_list_prop = 'description' + + runtest_get_properties(ssosrvdef.manager, non_list_prop) + + +def test_ssosrvdef_crud(hmc_session): + """ + Test create, read, update and delete a SSO server definition. + """ + client = zhmcclient.Client(hmc_session) + console = client.consoles.console + hd = hmc_session.hmc_definition + + api_version = client.query_api_version() + hmc_version = api_version['hmc-version'] + hmc_version_info = tuple(map(int, hmc_version.split('.'))) + if hmc_version_info < (2, 17, 0): + skip_warn(f"HMC {hd.host} of version {hmc_version} does not yet " + "support SSO server definitions") + + ssosrvdef_name = TEST_PREFIX + ' test_ssosrvdef_crud ssosrvdef1' + ssosrvdef_name_new = ssosrvdef_name + ' new' + + # Ensure a clean starting point for this test + try: + ssosrvdef = console.sso_server_definitions.find( + name=ssosrvdef_name) + except zhmcclient.NotFound: + pass + else: + warnings.warn( + "Deleting test SSO server definition from previous run: " + f"{ssosrvdef_name!r}", UserWarning) + ssosrvdef.delete() + + # Test creating the SSO server definition + + ssosrvdef_input_props = { + "authentication-page-servers":[ + { + "hostname-ipaddr":"images1.example.com", + "port":443 + }, + { + "hostname-ipaddr":"images2.example.com", + "port":80 + } + ], + "authentication-url":"https://sso1.example.com/auth", + "client-id":"sso1-123456", + "client-secret":"sso1-client-secret", + "description":"Primary SSO server", + "issuer-url":"https://sso1.example.com/issuer", + "jwks-url":"https://sso1.example.com/jwks", + "logout-sso-session-on-reauthentication-failure":true, + "logout-url":"https://sso1.example.com/logout", + "name":"SSO Server 1", + "token-url":"https://sso1.example.com/token", + "type":"oidc" + } + ssosrvdef_auto_props = { + 'logout-url': None, + 'logout-sso-session-on-reauthentication-failure': False, + } + + # The code to be tested + try: + ssosrvdef = console.sso_server_definitions.create( + ssosrvdef_input_props) + except zhmcclient.HTTPError as exc: + if exc.http_status == 403 and exc.reason == 1: + skip_warn(f"HMC userid {hd.userid!r} is not authorized for task " + f"'Manage Single Sign-On Servers' on HMC {hd.host}") + else: + raise + + for pn, exp_value in ssosrvdef_input_props.items(): + assert ssosrvdef.properties[pn] == exp_value, \ + f"Unexpected value for property {pn!r}" + ssosrvdef.pull_full_properties() + for pn, exp_value in ssosrvdef_input_props.items(): + assert ssosrvdef.properties[pn] == exp_value, \ + f"Unexpected value for property {pn!r}" + for pn, exp_value in ssosrvdef_auto_props.items(): + assert ssosrvdef.properties[pn] == exp_value, \ + f"Unexpected value for property {pn!r}" + + # Test updating a property of the SSO server definition + + new_desc = "Updated SSO server definition description." + + # The code to be tested + ssosrvdef.update_properties(dict(description=new_desc)) + + assert ssosrvdef.properties['description'] == new_desc + ssosrvdef.pull_full_properties() + assert ssosrvdef.properties['description'] == new_desc + + # Test that SSO server definitions cannot be renamed + + with pytest.raises(zhmcclient.HTTPError) as exc_info: + + # The code to be tested + ssosrvdef.update_properties(dict(name=ssosrvdef_name_new)) + + exc = exc_info.value + assert exc.http_status == 400 + assert exc.reason == 6 + with pytest.raises(zhmcclient.NotFound): + console.sso_server_definitions.find(name=ssosrvdef_name_new) + + # Test deleting the SSO server definition + + # The code to be tested + ssosrvdef.delete() + + with pytest.raises(zhmcclient.NotFound): + console.sso_server_definitions.find(name=ssosrvdef_name) diff --git a/tests/unit/zhmcclient/test_sso_server_definition.py b/tests/unit/zhmcclient/test_sso_server_definition.py new file mode 100644 index 00000000..3e0690f3 --- /dev/null +++ b/tests/unit/zhmcclient/test_sso_server_definition.py @@ -0,0 +1,389 @@ +# Copyright 2017,2021 IBM Corp. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Unit tests for _sso_srv_def module. +""" + + +import re +import copy +import logging +import pytest + +from zhmcclient import Client, HTTPError, NotFound, SSOServerDefinition +from zhmcclient.mock import FakedSession +from tests.common.utils import assert_resources, assert_blanked_in_message + + +class TestSSOServerDefinition: + """All tests for the SSOServerDefinition and SSOServerDefinitionManager + classes.""" + + def setup_method(self): + """ + Setup that is called by pytest before each test method. + Set up a faked session, and add a faked Console without any + child resources. + """ + # pylint: disable=attribute-defined-outside-init + + self.session = FakedSession('fake-host', 'fake-hmc', '2.17.1', '1.8') + self.client = Client(self.session) + + self.faked_console = self.session.hmc.consoles.add({ + 'object-id': None, + # object-uri will be automatically set + 'parent': None, + 'class': 'console', + 'name': 'fake-console1', + 'description': 'Console #1', + }) + self.console = self.client.consoles.find(name=self.faked_console.name) + + def add_sso_srv_def(self, name): + """ + Add a faked SSOServerDefinition object to the faked Console + and return it. + """ + + faked_sso_srv_def = self.faked_console.sso_server_definitions.add({ + 'element-id': f'oid-{name}', + # element-uri will be automatically set + 'parent': '/api/console', + 'class': 'sso-server-definition', + 'name': name, + 'description': f'SSO Server Definition {name}', + "authentication-page-servers":[ + { + "hostname-ipaddr":"images1.example.com", + "port":443 + }, + { + "hostname-ipaddr":"images2.example.com", + "port":80 + } + ], + "authentication-url":"https://sso1.example.com/auth", + "client-id":"sso1-123456", + "element-uri":"/api/console/sso-server-definitions/c6a464c2-a211-11ef-bbc4-fa163e7cf285", + "issuer-url":"https://sso1.example.com/issuer", + "jwks-url":"https://sso1.example.com/jwks", + "logout-sso-session-on-reauthentication-failure":true, + "logout-url":"https://sso1.example.com/logoff", + "token-url":"https://sso1.example.com/token", + "type":"oidc" + }) + return faked_sso_srv_def + + def test_sso_srv_def_manager_repr(self): + """Test SSOServerDefinitionManager.__repr__().""" + + sso_srv_def_mgr = self.console.sso_server_definitions + + # Execute the code to be tested + repr_str = repr(sso_srv_def_mgr) + + repr_str = repr_str.replace('\n', '\\n') + # We check just the begin of the string: + assert re.match( + rf'^{sso_srv_def_mgr.__class__.__name__}\s+at\s+' + rf'0x{id(sso_srv_def_mgr):08x}\s+\(\\n.*', + repr_str) + + def test_sso_srv_def_manager_initial_attrs(self): + """Test initial attributes of SSOServerDefinitionManager.""" + + sso_srv_def_mgr = self.console.sso_server_definitions + + # Verify all public properties of the manager object + assert sso_srv_def_mgr.resource_class == SSOServerDefinition + assert sso_srv_def_mgr.class_name == 'sso-server-definition' + assert sso_srv_def_mgr.session is self.session + assert sso_srv_def_mgr.parent is self.console + assert sso_srv_def_mgr.console is self.console + + @pytest.mark.parametrize( + "full_properties_kwargs, prop_names", [ + (dict(full_properties=False), + ['element-uri', 'name','type']), + (dict(full_properties=True), + ['element-uri', 'name', 'type']), + ({}, # test default for full_properties (False) + ['element-uri', 'name','type']), + ] + ) + @pytest.mark.parametrize( + "filter_args, exp_names", [ + (None, + ['a', 'b']), + ({}, + ['a', 'b']), + ({'name': 'a'}, + ['a']), + ({'name': 'A'}, # SSO user definitions have case-insensitive names + ['a']), + ] + ) + def test_sso_srv_def_manager_list( + self, filter_args, exp_names, full_properties_kwargs, prop_names): + """Test SSOServerDefinitionManager.list().""" + + faked_sso_srv_def1 = self.add_sso_srv_def(name='a') + faked_sso_srv_def2 = self.add_sso_srv_def(name='b') + faked_sso_srv_defs = [faked_sso_srv_def1, faked_sso_srv_def2] + exp_faked_sso_srv_defs = [u for u in faked_sso_srv_defs + if u.name in exp_names] + sso_srv_def_mgr = self.console.sso_server_definitions + + # Execute the code to be tested + sso_srv_defs = sso_srv_def_mgr.list(filter_args=filter_args, + **full_properties_kwargs) + + assert_resources(sso_srv_defs, exp_faked_sso_srv_defs, prop_names) + + @pytest.mark.parametrize( + "input_props, exp_prop_names, exp_exc", [ + ({}, # props missing + None, + HTTPError({'http-status': 400, 'reason': 5})), + ({'description': 'fake description X'}, # props missing + None, + HTTPError({'http-status': 400, 'reason': 5})), + ({'description': 'fake description X', + 'name': 'a', + 'type': 'oidc', + 'client-secret': 'sso1-client-secret', + "issuer-url":"https://sso1.example.com/issuer", + 'authentication-url' :'https://sso1.example.com/auth', + 'token-url':'https://sso1.example.com/token', + "jwks-url":"https://sso1.example.com/jwks", + 'logout-url ':'https://sso1.example.com/logout'}, + ['element-uri', 'name', 'description'], + None), + ({'description': 'fake description X', + 'name': 'a', + 'type': 'oidc', + 'client-secret': 'sso1-client-secret', + "issuer-url":"https://sso1.example.com/issuer", + 'authentication-url' :'https://sso1.example.com/auth', + 'token-url':'https://sso1.example.com/token', + "jwks-url":"https://sso1.example.com/jwks", + 'logout-url ':'https://sso1.example.com/logout'}, + ['element-uri', 'name', 'client-secret'], + None), + ] + ) + def test_sso_srv_def_manager_create( + self, caplog, input_props, exp_prop_names, exp_exc): + """Test SSOServerDefinitionManager.create().""" + + logger_name = "zhmcclient.api" + caplog.set_level(logging.DEBUG, logger=logger_name) + + sso_srv_def_mgr = self.console.sso_server_definitions + + if exp_exc is not None: + + with pytest.raises(exp_exc.__class__) as exc_info: + + # Execute the code to be tested + sso_srv_def_mgr.create(properties=input_props) + + exc = exc_info.value + if isinstance(exp_exc, HTTPError): + assert exc.http_status == exp_exc.http_status + assert exc.reason == exp_exc.reason + + else: + + + # Execute the code to be tested. + sso_srv_def = sso_srv_def_mgr.create(properties=input_props) + + # Get its API call log record + call_record = caplog.records[-2] + + # Check the resource for consistency within itself + assert isinstance(sso_srv_def, SSOServerDefinition) + sso_srv_def_name = sso_srv_def.name + exp_sso_srv_def_name = sso_srv_def.properties['name'] + assert sso_srv_def_name == exp_sso_srv_def_name + sso_srv_def_uri = sso_srv_def.uri + exp_sso_srv_def_uri = sso_srv_def.properties['element-uri'] + assert sso_srv_def_uri == exp_sso_srv_def_uri + + # Check the properties against the expected names and values + for prop_name in exp_prop_names: + assert prop_name in sso_srv_def.properties + if prop_name in input_props: + value = sso_srv_def.properties[prop_name] + exp_value = input_props[prop_name] + assert value == exp_value + + # Verify the API call log record for blanked-out properties. + assert_blanked_in_message( + call_record.message, input_props, + ['client-secret']) + + def test_sso_srv_def_repr(self): + """Test SSOServerDefinition.__repr__().""" + + faked_sso_srv_def1 = self.add_sso_srv_def(name='a') + sso_srv_def1 = self.console.sso_server_definitions.find( + name=faked_sso_srv_def1.name) + + # Execute the code to be tested + repr_str = repr(sso_srv_def1) + + repr_str = repr_str.replace('\n', '\\n') + # We check just the begin of the string: + assert re.match( + rf'^{sso_srv_def1.__class__.__name__}\s+at\s+' + rf'0x{id(sso_srv_def1):08x}\s+\(\\n.*', + repr_str) + + @pytest.mark.parametrize( + "input_props, exp_exc", [ + ({'name': 'a'}, + None), + ({'name': 'b'}, + None), + ] + ) + def test_sso_srv_def_delete(self, input_props, exp_exc): + """Test SSOServerDefinition.delete().""" + + faked_sso_srv_def = self.add_sso_srv_def(name=input_props['name']) + + sso_srv_def_mgr = self.console.sso_server_definitions + sso_srv_def = sso_srv_def_mgr.find(name=faked_sso_srv_def.name) + + if exp_exc is not None: + + with pytest.raises(exp_exc.__class__) as exc_info: + + # Execute the code to be tested + sso_srv_def.delete() + + exc = exc_info.value + if isinstance(exp_exc, HTTPError): + assert exc.http_status == exp_exc.http_status + assert exc.reason == exp_exc.reason + + # Check that the SSO Server Definition still exists + sso_srv_def_mgr.find(name=faked_sso_srv_def.name) + + else: + + # Execute the code to be tested. + sso_srv_def.delete() + + # Check that the SSO Server Definition no longer exists + with pytest.raises(NotFound) as exc_info: + sso_srv_def_mgr.find(name=faked_sso_srv_def.name) + + def test_sso_delete_create_same(self): + """Test SSOServerDefinition.delete() followed by create() with same + name.""" + + sso_srv_def_name = 'faked_a' + + # Add the SSO Server Definition to be tested + self.add_sso_srv_def(name=sso_srv_def_name) + + # Input properties for a SSO Server Definition with the same name + sn_sso_srv_def_props = { + 'name': sso_srv_def_name, + 'description': 'SSO Server Definition with same name', + 'primary-hostname-ipaddr': '10.11.12.13', + 'search-distinguished-name': 'test{0}', + } + + sso_srv_def_mgr = self.console.sso_server_definitions + sso_srv_def = sso_srv_def_mgr.find(name=sso_srv_def_name) + + # Execute the deletion code to be tested + sso_srv_def.delete() + + # Check that the SSO Server Definition no longer exists + with pytest.raises(NotFound): + sso_srv_def_mgr.find(name=sso_srv_def_name) + + # Execute the creation code to be tested. + sso_srv_def_mgr.create(sn_sso_srv_def_props) + + # Check that the SSO Server Definition exists again under that name + sn_sso_srv_def = sso_srv_def_mgr.find(name=sso_srv_def_name) + description = sn_sso_srv_def.get_property('description') + assert description == sn_sso_srv_def_props['description'] + + @pytest.mark.parametrize( + "input_props", [ + {}, + {'description': 'New SSO Server Definition description'}, + {'client-secret': 'bla'}, + ] + ) + def test_sso_srv_def_update_properties(self, caplog, input_props): + """Test SSOServerDefinition.update_properties().""" + + logger_name = "zhmcclient.api" + caplog.set_level(logging.DEBUG, logger=logger_name) + + sso_srv_def_name = 'faked_a' + + # Add the SSO Server Definition to be tested + self.add_sso_srv_def(name=sso_srv_def_name) + + sso_srv_def_mgr = self.console.sso_server_definitions + sso_srv_def = sso_srv_def_mgr.find(name=sso_srv_def_name) + + sso_srv_def.pull_full_properties() + saved_properties = copy.deepcopy(sso_srv_def.properties) + + # Execute the code to be tested + sso_srv_def.update_properties(properties=input_props) + + # Get its API call log record + call_record = caplog.records[-2] + + # Verify that the resource object already reflects the property + # updates. + for prop_name in saved_properties: + if prop_name in input_props: + exp_prop_value = input_props[prop_name] + else: + exp_prop_value = saved_properties[prop_name] + assert prop_name in sso_srv_def.properties + prop_value = sso_srv_def.properties[prop_name] + assert prop_value == exp_prop_value, \ + f"Unexpected value for property {prop_name!r}" + + # Refresh the resource object and verify that the resource object + # still reflects the property updates. + sso_srv_def.pull_full_properties() + for prop_name in saved_properties: + if prop_name in input_props: + exp_prop_value = input_props[prop_name] + else: + exp_prop_value = saved_properties[prop_name] + assert prop_name in sso_srv_def.properties + prop_value = sso_srv_def.properties[prop_name] + assert prop_value == exp_prop_value + + # Verify the API call log record for blanked-out properties. + assert_blanked_in_message( + call_record.message, input_props, + ['client-secret']) diff --git a/zhmcclient/__init__.py b/zhmcclient/__init__.py index 4c97f65b..5a96489d 100644 --- a/zhmcclient/__init__.py +++ b/zhmcclient/__init__.py @@ -52,6 +52,7 @@ from ._task import * # noqa: F401 from ._ldap_server_definition import * # noqa: F401 from ._mfa_server_definition import * # noqa: F401 +from ._sso_server_definition import * # noqa: F401 from ._unmanaged_cpc import * # noqa: F401 from ._storage_group import * # noqa: F401 from ._storage_volume import * # noqa: F401 diff --git a/zhmcclient/_console.py b/zhmcclient/_console.py index eaf0cfac..d5951fdf 100644 --- a/zhmcclient/_console.py +++ b/zhmcclient/_console.py @@ -37,6 +37,7 @@ from ._task import TaskManager from ._ldap_server_definition import LdapServerDefinitionManager from ._mfa_server_definition import MfaServerDefinitionManager +from ._sso_server_definition import SSOServerDefinitionManager from ._unmanaged_cpc import UnmanagedCpcManager from ._group import GroupManager from ._utils import get_api_features @@ -218,6 +219,7 @@ def __init__(self, manager, uri, name=None, properties=None): self._tasks = None self._ldap_server_definitions = None self._mfa_server_definitions = None + self._sso_server_definitions = None self._unmanaged_cpcs = None self._groups = None self._certificates = None @@ -336,6 +338,18 @@ def mfa_server_definitions(self): if not self._mfa_server_definitions: self._mfa_server_definitions = MfaServerDefinitionManager(self) return self._mfa_server_definitions + + @property + def sso_server_definitions(self): + """ + :class:`~zhmcclient.SSOServerDefinitionManager`: Access to the + :term:`sso Server Definitions ` in this + Console. + """ + # We do here some lazy loading. + if not self._sso_server_definitions: + self._sso_server_definitions = SSOServerDefinitionManager(self) + return self._sso_server_definitions @property def unmanaged_cpcs(self): @@ -1612,6 +1626,7 @@ def dump(self): "tasks": [...], "ldap_server_definitions": [...], "mfa_server_definitions": [...], + "sso_server_definitions": [...], "unmanaged_cpcs": [...], "storage_groups": [...], } @@ -1646,6 +1661,9 @@ def dump(self): mfa_server_definitions = self.mfa_server_definitions.dump() if mfa_server_definitions: resource_dict['mfa_server_definitions'] = mfa_server_definitions + sso_server_definitions = self.sso_server_definitions.dump() + if sso_server_definitions: + resource_dict['sso_server_definitions'] = sso_server_definitions storage_groups = self.storage_groups.dump() if storage_groups: resource_dict['storage_groups'] = storage_groups diff --git a/zhmcclient/_sso_server_definition.py b/zhmcclient/_sso_server_definition.py new file mode 100644 index 00000000..7062d241 --- /dev/null +++ b/zhmcclient/_sso_server_definition.py @@ -0,0 +1,301 @@ +# Copyright 2017,2021 IBM Corp. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +A :term:`SSO Server Definition` resource represents a definition that contains +information about an SSO server that may be used for HMC user authentication +purposes.The SSO server support is added by the single sign-on feature +""" + + +import copy + +from ._manager import BaseManager +from ._resource import BaseResource +from ._logging import logged_api_call +from ._utils import RC_SSO_SERVER_DEFINITION + +__all__ = ['SSOServerDefinitionManager', 'SSOServerDefinition'] + + +class SSOServerDefinitionManager(BaseManager): + """ + Manager providing access to the :term:`SSO Server Definition` resources of + a HMC. + + Derived from :class:`~zhmcclient.BaseManager`; see there for common methods + and attributes. + + Objects of this class are not directly created by the user; they are + accessible via the following instance variable of a + :class:`~zhmcclient.Console` object: + + * :attr:`zhmcclient.Console.sso_server_definitions` + + HMC/SE version requirements: + + * HMC version == 2.17.0 + """ + + def __init__(self, console): + # This function should not go into the docs. + # Parameters: + # console (:class:`~zhmcclient.Console`): + # Console object representing the HMC. + + # Resource properties that are supported as filter query parameters. + # If the support for a resource property changes within the set of HMC + # versions that support this type of resource, this list must be set up + # for the version of the HMC this session is connected to. + # Because this resource has case-insensitive names, this list must + # contain the name property. + query_props = [ + 'name', + 'type', + 'additional-properties', + ] + + super().__init__( + resource_class=SSOServerDefinition, + class_name=RC_SSO_SERVER_DEFINITION, + session=console.manager.session, + parent=console, + base_uri='/api/console/sso-server-definitions', + oid_prop='element-id', + uri_prop='element-uri', + name_prop='name', + query_props=query_props, + case_insensitive_names=True) + + @property + def console(self): + """ + :class:`~zhmcclient.Console`: :term:`Console` defining the scope for + this manager. + """ + return self._parent + + @logged_api_call + def list(self, full_properties=False, filter_args=None): + """ + List the :term:`sso Server Definition` resources representing the + definitions of sso servers in this HMC. + + Any resource property may be specified in a filter argument. For + details about filter arguments, see :ref:`Filtering`. + + The listing of resources is handled in an optimized way: + + * If this manager is enabled for :ref:`auto-updating`, a locally + maintained resource list is used (which is automatically updated via + inventory notifications from the HMC) and the provided filter + arguments are applied. + + * Otherwise, if the filter arguments specify the resource name as a + single filter argument with a straight match string (i.e. without + regular expressions), an optimized lookup is performed based on a + locally maintained name-URI cache. + + * Otherwise, the HMC List operation is performed with the subset of the + provided filter arguments that can be handled on the HMC side and the + remaining filter arguments are applied on the client side on the list + result. + + HMC/SE version requirements: + + * HMC version == 2.17.0 + + Authorization requirements: + + * User-related-access permission to the SSO Server Definition objects + included in the result, or task permission to the "Manage Single Sign-On Servers" task. + + Parameters: + + full_properties (bool): + Controls whether the full set of resource properties should be + retrieved, vs. only the short set as returned by the list + operation. + + filter_args (dict): + Filter arguments that narrow the list of returned resources to + those that match the specified filter arguments. For details, see + :ref:`Filtering`. + + `None` causes no filtering to happen, i.e. all resources are + returned. + + Returns: + + : A list of :class:`~zhmcclient.SSOServerDefinition` objects. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + :exc:`~zhmcclient.FilterConversionError` + """ + result_prop = 'sso-server-definitions' + list_uri = f'{self.console.uri}/sso-server-definitions' + return self._list_with_operation( + list_uri, result_prop, full_properties, filter_args, None) + + @logged_api_call(blanked_properties=['client-secret'], properties_pos=1) + def create(self, properties): + """ + Create a new SSO Server Definition in this HMC. + + HMC/SE version requirements: + + * HMC version == 2.17.0 + + Authorization requirements: + + * Task permission to the "Manage Single Sign-On Servers" task. + + Parameters: + + properties (dict): Initial property values. + Allowable properties are defined in section 'Request body contents' + in section 'Create SSO Server Definition' in the :term:`HMC API` + book. + + Returns: + + SSOServerDefinition: + The resource object for the new SSO Server Definition. + The object will have its 'object-uri' property set as returned by + the HMC, and will also have the input properties set. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + result = self.session.post( + self.console.uri + '/sso-server-definitions', body=properties) + # There should not be overlaps, but just in case there are, the + # returned props should overwrite the input props: + props = copy.deepcopy(properties) + props.update(result) + name = props.get(self._name_prop, None) + uri = props[self._uri_prop] + sso_server_definition = SSOServerDefinition(self, uri, name, props) + self._name_uri_cache.update(name, uri) + return sso_server_definition + + +class SSOServerDefinition(BaseResource): + """ + Representation of a :term:`SSO Server Definition`. + + Derived from :class:`~zhmcclient.BaseResource`; see there for common + methods and attributes. + + Objects of this class are not directly created by the user; they are + returned from creation or list functions on their manager object + (in this case, :class:`~zhmcclient.SSOServerDefinitionManager`). + + HMC/SE version requirements: + + * HMC version == 2.17.0 + """ + + def __init__(self, manager, uri, name=None, properties=None): + # This function should not go into the docs. + # manager (:class:`~zhmcclient.SSOServerDefinitionManager`): + # Manager object for this resource object. + # uri (string): + # Canonical URI path of the resource. + # name (string): + # Name of the resource. + # properties (dict): + # Properties to be set for this resource object. May be `None` or + # empty. + assert isinstance(manager, SSOServerDefinitionManager), ( + "Console init: Expected manager type " + f"{SSOServerDefinitionManager}, got {type(manager)}") + super().__init__( + manager, uri, name, properties) + + @logged_api_call + def delete(self): + """ + Delete this SSO Server Definition. + + HMC/SE version requirements: + + * HMC version == 2.17.0 + + Authorization requirements: + + * Task permission to the "Manage Single Sign-On Servers" task. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + # pylint: disable=protected-access + self.manager.session.delete(self.uri, resource=self) + self.manager._name_uri_cache.delete( + self.get_properties_local(self.manager._name_prop, None)) + self.cease_existence_local() + + @logged_api_call(blanked_properties=['client-secret'], properties_pos=1) + def update_properties(self, properties): + """ + Update writeable properties of this SSO Server Definitions. + + This method serializes with other methods that access or change + properties on the same Python object. + + HMC/SE version requirements: + + * HMC version == 2.17.0 + + Authorization requirements: + + * Task permission to the "Manage Single Sign-On Servers" task. + + Parameters: + + properties (dict): New values for the properties to be updated. + Properties not to be updated are omitted. + Allowable properties are the properties with qualifier (w) in + section 'Data model' in section 'SSO Server Definition object' in + the :term:`HMC API` book. + + Raises: + + :exc:`~zhmcclient.HTTPError` + :exc:`~zhmcclient.ParseError` + :exc:`~zhmcclient.AuthError` + :exc:`~zhmcclient.ConnectionError` + """ + # pylint: disable=protected-access + self.manager.session.post(self.uri, resource=self, body=properties) + + # The name of SSO Server Definitions cannot be updated. An attempt to + # do so should cause HTTPError to be raised in the POST above, so we + # assert that here, because we omit the extra code for handling name + # updates: + assert self.manager._name_prop not in properties + self.update_properties_local(copy.deepcopy(properties)) diff --git a/zhmcclient/_utils.py b/zhmcclient/_utils.py index 0ae9f5be..2f6bdc0c 100644 --- a/zhmcclient/_utils.py +++ b/zhmcclient/_utils.py @@ -77,6 +77,7 @@ RC_USER = 'user' RC_GROUP = 'group' RC_LDAP_SERVER_DEFINITION = 'ldap-server-definition' +RC_SSO_SERVER_DEFINITION = 'sso-server-definition' RC_MFA_SERVER_DEFINITION = 'mfa-server-definition' RC_HW_MESSAGE = 'hardware-message' @@ -102,6 +103,7 @@ RC_USER, RC_LDAP_SERVER_DEFINITION, RC_MFA_SERVER_DEFINITION, + RC_SSO_SERVER_DEFINITION, RC_CPC, # For unmanaged CPCs RC_PARTITION_LINK, ) @@ -144,6 +146,7 @@ RC_LOAD_ACTIVATION_PROFILE, RC_LDAP_SERVER_DEFINITION, RC_MFA_SERVER_DEFINITION, + RC_SSO_SERVER_DEFINITION, RC_LOGICAL_PARTITION, RC_CONSOLE, RC_CPC, diff --git a/zhmcclient/mock/_hmc.py b/zhmcclient/mock/_hmc.py index 3880e626..7207e672 100644 --- a/zhmcclient/mock/_hmc.py +++ b/zhmcclient/mock/_hmc.py @@ -36,6 +36,7 @@ 'FakedPasswordRuleManager', 'FakedPasswordRule', 'FakedTaskManager', 'FakedTask', 'FakedLdapServerDefinitionManager', 'FakedLdapServerDefinition', + 'FakedSSOServerDefinitionManager', 'FakedSSOServerDefinition', 'FakedMfaServerDefinitionManager', 'FakedMfaServerDefinition', 'FakedActivationProfileManager', 'FakedActivationProfile', 'FakedAdapterManager', 'FakedAdapter', @@ -1149,6 +1150,8 @@ def __init__(self, manager, properties): hmc=manager.hmc, console=self) self._mfa_server_definitions = FakedMfaServerDefinitionManager( hmc=manager.hmc, console=self) + self._sso_server_definitions = FakedSSOServerDefinitionManager( + hmc=manager.hmc, console=self) self._unmanaged_cpcs = FakedUnmanagedCpcManager( hmc=manager.hmc, console=self) self._groups = FakedGroupManager(hmc=manager.hmc, console=self) @@ -1180,6 +1183,8 @@ def __repr__(self): f"{repr_manager(self.ldap_server_definitions, indent=2)}\n" " _mfa_server_definitions = " f"{repr_manager(self.mfa_server_definitions, indent=2)}\n" + " _sso_server_definitions = " + f"{repr_manager(self.sso_server_definitions, indent=2)}\n" " _unmanaged_cpcs = " f"{repr_manager(self.unmanaged_cpcs, indent=2)}\n" f" _groups = {repr_manager(self.groups, indent=2)}\n" @@ -1257,6 +1262,14 @@ def mfa_server_definitions(self): the faked MFA Server Definition resources of this Console. """ return self._mfa_server_definitions + + @property + def sso_server_definitions(self): + """ + :class:`~zhmcclient.mock.FakedSSOServerDefinitionManager`: Access to + the faked SSO Server Definition resources of this Console. + """ + return self._sso_server_definitions @property def unmanaged_cpcs(self): @@ -1780,6 +1793,84 @@ def __init__(self, manager, properties): super().__init__( manager=manager, properties=properties) + +class FakedSSOServerDefinitionManager(FakedBaseManager): + """ + A manager for faked SSO Server Definition resources within a faked HMC + (see :class:`zhmcclient.mock.FakedHmc`). + + Derived from :class:`zhmcclient.mock.FakedBaseManager`, see there for + common methods and attributes. + """ + + def __init__(self, hmc, console): + super().__init__( + hmc=hmc, + parent=console, + resource_class=FakedSSOServerDefinition, + base_uri=console.uri + '/sso-server-definitions', + oid_prop='element-id', + uri_prop='element-uri', + class_value='sso-server-definition', + name_prop='name', + case_insensitive_names=True) + + def add(self, properties): + # pylint: disable=useless-super-delegation + """ + Add a faked SSO Server Definition resource. + + Parameters: + + properties (dict): + Resource properties. + + Special handling and requirements for certain properties: + + * 'element-id' will be auto-generated with a unique value across + all instances of this resource type, if not specified. + * 'element-uri' will be auto-generated based upon the element ID, + if not specified. + * 'class' will be auto-generated to 'sso-server-definition', + if not specified. + * All of the other class-soecific resource properties will be set + to a default value consistent with the HMC data model. + + Returns: + + :class:`~zhmcclient.mock.FakedSSOServerDefinition`: The faked + SSOServerDefinition resource. + """ + new_lsd = super().add(properties) + + # Resource type specific default values + new_lsd.properties.setdefault('description', '') + new_lsd.properties.setdefault('authentication-url', 'https://sso1.example.com/auth') + new_lsd.properties.setdefault('type', 'oidc') + new_lsd.properties.setdefault('logout-sso-session-on-reauthentication-failure', + True) + new_lsd.properties.setdefault('logout-url','https://sso1.example.com/logout') + new_lsd.properties.setdefault('issuer-url', 'https://sso1.example.com/issuer') + new_lsd.properties.setdefault('jwks-url', 'https://sso1.example.com/jwks') + new_lsd.properties.setdefault('replication-overwrite-possible', False) + + + return new_lsd + + +class FakedSSOServerDefinition(FakedBaseResource): + """ + A faked SSO Server Definition resource within a faked HMC (see + :class:`zhmcclient.mock.FakedHmc`). + + Derived from :class:`zhmcclient.mock.FakedBaseResource`, see there for + common methods and attributes. + """ + + def __init__(self, manager, properties): + super().__init__( + manager=manager, + properties=properties) class FakedActivationProfileManager(FakedBaseManager): diff --git a/zhmcclient/mock/_session.py b/zhmcclient/mock/_session.py index 52b7e811..b7608731 100644 --- a/zhmcclient/mock/_session.py +++ b/zhmcclient/mock/_session.py @@ -293,7 +293,7 @@ }, }, "LdapServerDefinition": { - "description": "An LPAP server definition on an HMC", + "description": "An LDAP server definition on an HMC", "type": "object", "additionalProperties": False, "required": [