From 60be29a59649c221fde1789dc491e071d8999619 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 11 Aug 2023 17:48:26 +0100 Subject: [PATCH] identity: Add support for domain config https://docs.openstack.org/api-ref/identity/v3/index.html#create-domain-configuration Change-Id: I25e5e4266bfa1bf470fa2dcd38e9002950ecebef --- doc/source/user/proxies/identity_v3.rst | 8 ++ doc/source/user/resources/identity/index.rst | 17 +-- .../resources/identity/v3/domain_config.rst | 12 ++ openstack/identity/v3/_proxy.py | 117 ++++++++++++++++++ openstack/identity/v3/domain_config.py | 47 +++++++ .../identity/v3/test_domain_config.py | 83 +++++++++++++ .../unit/identity/v3/test_domain_config.py | 47 +++++++ .../tests/unit/identity/v3/test_proxy.py | 70 +++++++++++ ...domain-configuration-2e8bcaa20736b379.yaml | 5 + 9 files changed, 393 insertions(+), 13 deletions(-) create mode 100644 doc/source/user/resources/identity/v3/domain_config.rst create mode 100644 openstack/identity/v3/domain_config.py create mode 100644 openstack/tests/functional/identity/v3/test_domain_config.py create mode 100644 openstack/tests/unit/identity/v3/test_domain_config.py create mode 100644 releasenotes/notes/add-identity-domain-configuration-2e8bcaa20736b379.yaml diff --git a/doc/source/user/proxies/identity_v3.rst b/doc/source/user/proxies/identity_v3.rst index 9498f47dc..1afe30ed0 100644 --- a/doc/source/user/proxies/identity_v3.rst +++ b/doc/source/user/proxies/identity_v3.rst @@ -28,6 +28,14 @@ Domain Operations :members: create_domain, update_domain, delete_domain, get_domain, find_domain, domains +Domain Config Operations +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.identity.v3._proxy.Proxy + :noindex: + :members: create_domain_config, delete_domain_config, get_domain_config, + update_domain_config + Endpoint Operations ^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/identity/index.rst b/doc/source/user/resources/identity/index.rst index b52221494..7c35d6ebb 100644 --- a/doc/source/user/resources/identity/index.rst +++ b/doc/source/user/resources/identity/index.rst @@ -5,24 +5,15 @@ Identity v2 Resources --------------------- .. toctree:: :maxdepth: 1 + :glob: - v2/extension - v2/role - v2/tenant - v2/user + v2/* Identity v3 Resources --------------------- .. toctree:: :maxdepth: 1 + :glob: - v3/credential - v3/domain - v3/endpoint - v3/group - v3/policy - v3/project - v3/service - v3/trust - v3/user + v3/* diff --git a/doc/source/user/resources/identity/v3/domain_config.rst b/doc/source/user/resources/identity/v3/domain_config.rst new file mode 100644 index 000000000..28defa237 --- /dev/null +++ b/doc/source/user/resources/identity/v3/domain_config.rst @@ -0,0 +1,12 @@ +openstack.identity.v3.domain_config +=================================== + +.. automodule:: openstack.identity.v3.domain_config + +The Domain Class +---------------- + +The ``DomainConfig`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.identity.v3.domain_config.DomainConfig + :members: diff --git a/openstack/identity/v3/_proxy.py b/openstack/identity/v3/_proxy.py index d772f75c0..902e1b3e5 100644 --- a/openstack/identity/v3/_proxy.py +++ b/openstack/identity/v3/_proxy.py @@ -16,6 +16,7 @@ from openstack.identity.v3 import ( ) from openstack.identity.v3 import credential as _credential from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import domain_config as _domain_config from openstack.identity.v3 import endpoint as _endpoint from openstack.identity.v3 import federation_protocol as _federation_protocol from openstack.identity.v3 import group as _group @@ -51,6 +52,7 @@ from openstack.identity.v3 import system as _system from openstack.identity.v3 import trust as _trust from openstack.identity.v3 import user as _user from openstack import proxy +from openstack import resource from openstack import utils @@ -83,6 +85,8 @@ class Proxy(proxy.Proxy): "user": _user.User, } + # ========== Credentials ========== + def create_credential(self, **attrs): """Create a new credential from attributes @@ -165,6 +169,8 @@ class Proxy(proxy.Proxy): """ return self._update(_credential.Credential, credential, **attrs) + # ========== Domains ========== + def create_domain(self, **attrs): """Create a new domain from attributes @@ -244,6 +250,85 @@ class Proxy(proxy.Proxy): """ return self._update(_domain.Domain, domain, **attrs) + # ========== Domain configs ========== + + def create_domain_config(self, domain, **attrs): + """Create a new config for a domain from attributes. + + :param domain: The value can be the ID of a domain or + a :class:`~openstack.identity.v3.domain.Domain` instance. + :param dict attrs: Keyword arguments which will be used to create a + :class:`~openstack.identity.v3.domain_config.DomainConfig` + comprised of the properties on the DomainConfig class. + + :returns: The results of domain config creation + :rtype: :class:`~openstack.identity.v3.domain_config.DomainConfig` + """ + domain_id = resource.Resource._get_id(domain) + return self._create( + _domain_config.DomainConfig, + domain_id=domain_id, + **attrs, + ) + + def delete_domain_config(self, domain, ignore_missing=True): + """Delete a config for a domain + + :param domain: The value can be the ID of a domain or a + a :class:`~openstack.identity.v3.domain.Domain` instance. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be + raised when the identity provider does not exist. + When set to ``True``, no exception will be set when + attempting to delete a nonexistent config for a domain. + + :returns: ``None`` + """ + domain_id = resource.Resource._get_id(domain) + self._delete( + _domain_config.DomainConfig, + domain_id=domain_id, + ignore_missing=ignore_missing, + ) + + def get_domain_config(self, domain): + """Get a single config for a domain + + :param domain_id: The value can be the ID of a domain or a + :class:`~openstack.identity.v3.domain.Domain` instance. + + :returns: One + :class:`~openstack.identity.v3.domain_config.DomainConfig` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + domain_id = resource.Resource._get_id(domain) + return self._get( + _domain_config.DomainConfig, + domain_id=domain_id, + requires_id=False, + ) + + def update_domain_config(self, domain, **attrs): + """Update a config for a domain + + :param domain_id: The value can be the ID of a domain or a + :class:`~openstack.identity.v3.domain.Domain` instance. + :attrs kwargs: The attributes to update on the config for a domain + represented by ``domain_id``. + + :returns: The updated config for a domain + :rtype: :class:`~openstack.identity.v3.domain_config.DomainConfig` + """ + domain_id = resource.Resource._get_id(domain) + return self._update( + _domain_config.DomainConfig, + domain_id=domain_id, + **attrs, + ) + + # ========== Endpoints ========== + def create_endpoint(self, **attrs): """Create a new endpoint from attributes @@ -326,6 +411,8 @@ class Proxy(proxy.Proxy): """ return self._update(_endpoint.Endpoint, endpoint, **attrs) + # ========== Groups ========== + def create_group(self, **attrs): """Create a new group from attributes @@ -462,6 +549,8 @@ class Proxy(proxy.Proxy): users = self._list(_user.User, base_path=base_path, **attrs) return users + # ========== Policies ========== + def create_policy(self, **attrs): """Create a new policy from attributes @@ -541,6 +630,8 @@ class Proxy(proxy.Proxy): """ return self._update(_policy.Policy, policy, **attrs) + # ========== Project ========== + def create_project(self, **attrs): """Create a new project from attributes @@ -638,6 +729,8 @@ class Proxy(proxy.Proxy): """ return self._update(_project.Project, project, **attrs) + # ========== Services ========== + def create_service(self, **attrs): """Create a new service from attributes @@ -717,6 +810,8 @@ class Proxy(proxy.Proxy): """ return self._update(_service.Service, service, **attrs) + # ========== Users ========== + def create_user(self, **attrs): """Create a new user from attributes @@ -799,6 +894,8 @@ class Proxy(proxy.Proxy): """ return self._update(_user.User, user, **attrs) + # ========== Trusts ========== + def create_trust(self, **attrs): """Create a new trust from attributes @@ -865,6 +962,8 @@ class Proxy(proxy.Proxy): # TODO(briancurtin): This is paginated but requires base list changes. return self._list(_trust.Trust, **query) + # ========== Regions ========== + def create_region(self, **attrs): """Create a new region from attributes @@ -944,6 +1043,8 @@ class Proxy(proxy.Proxy): """ return self._update(_region.Region, region, **attrs) + # ========== Roles ========== + def create_role(self, **attrs): """Create a new role from attributes @@ -1025,6 +1126,8 @@ class Proxy(proxy.Proxy): """ return self._update(_role.Role, role, **attrs) + # ========== Role assignments ========== + def role_assignments_filter( self, domain=None, project=None, system=None, group=None, user=None ): @@ -1127,6 +1230,8 @@ class Proxy(proxy.Proxy): """ return self._list(_role_assignment.RoleAssignment, **query) + # ========== Registered limits ========== + def registered_limits(self, **query): """Retrieve a generator of registered_limits @@ -1204,6 +1309,8 @@ class Proxy(proxy.Proxy): ignore_missing=ignore_missing, ) + # ========== Limits ========== + def limits(self, **query): """Retrieve a generator of limits @@ -1267,6 +1374,8 @@ class Proxy(proxy.Proxy): """ self._delete(limit.Limit, limit, ignore_missing=ignore_missing) + # ========== Roles ========== + def assign_domain_role_to_user(self, domain, user, role): """Assign role to user on a domain @@ -1555,6 +1664,8 @@ class Proxy(proxy.Proxy): system = self._get_resource(_system.System, system) return system.validate_group_has_role(self, group, role) + # ========== Application credentials ========== + def application_credentials(self, user, **query): """Retrieve a generator of application credentials @@ -1681,6 +1792,8 @@ class Proxy(proxy.Proxy): ignore_missing=ignore_missing, ) + # ========== Federation protocols ========== + def create_federation_protocol(self, idp_id, **attrs): """Create a new federation protocol from attributes @@ -1834,6 +1947,8 @@ class Proxy(proxy.Proxy): idp_id = idp_id.id return self._update(cls, protocol, idp_id=idp_id, **attrs) + # ========== Mappings ========== + def create_mapping(self, **attrs): """Create a new mapping from attributes @@ -1914,6 +2029,8 @@ class Proxy(proxy.Proxy): """ return self._update(_mapping.Mapping, mapping, **attrs) + # ========== Identity providers ========== + def create_identity_provider(self, **attrs): """Create a new identity provider from attributes diff --git a/openstack/identity/v3/domain_config.py b/openstack/identity/v3/domain_config.py new file mode 100644 index 000000000..ba95185d3 --- /dev/null +++ b/openstack/identity/v3/domain_config.py @@ -0,0 +1,47 @@ +# 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. + +from openstack import resource + + +class DomainConfigLDAP(resource.Resource): + #: The base distinguished name (DN) of LDAP. + user_tree_dn = resource.Body('user_tree_dn') + #: The LDAP URL. + url = resource.Body('url') + + +class DomainConfigDriver(resource.Resource): + #: The Identity backend driver. + driver = resource.Body('driver') + + +class DomainConfig(resource.Resource): + resource_key = 'config' + base_path = '/domains/%(domain_id)s/config' + requires_id = False + create_requires_id = False + commit_method = 'PATCH' + create_method = 'PUT' + + # capabilities + allow_create = True + allow_fetch = True + allow_commit = True + allow_delete = True + + #: The domain ID. + domain_id = resource.URI('domain_id') + #: An identity object. + identity = resource.Body('identity', type=DomainConfigDriver) + #: The config object. + ldap = resource.Body('ldap', type=DomainConfigLDAP) diff --git a/openstack/tests/functional/identity/v3/test_domain_config.py b/openstack/tests/functional/identity/v3/test_domain_config.py new file mode 100644 index 000000000..e294498f4 --- /dev/null +++ b/openstack/tests/functional/identity/v3/test_domain_config.py @@ -0,0 +1,83 @@ +# 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. + +import uuid + +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import domain_config as _domain_config +from openstack.tests.functional import base + + +class TestDomainConfig(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + + self.domain_name = self.getUniqueString() + + # create the domain and domain config + + self.domain = self.operator_cloud.create_domain( + name=self.domain_name, + ) + self.assertIsInstance(self.domain, _domain.Domain) + self.addCleanup(self._delete_domain) + + def _delete_domain(self): + self.operator_cloud.identity.update_domain( + self.domain, + enabled=False, + ) + self.operator_cloud.identity.delete_domain(self.domain) + + def test_domain_config(self): + # create the domain config + + domain_config = self.operator_cloud.identity.create_domain_config( + self.domain, + identity={'driver': uuid.uuid4().hex}, + ldap={'url': uuid.uuid4().hex}, + ) + self.assertIsInstance( + domain_config, + _domain_config.DomainConfig, + ) + + # update the domain config + + ldap_url = uuid.uuid4().hex + domain_config = self.operator_cloud.identity.update_domain_config( + self.domain, + ldap={'url': ldap_url}, + ) + self.assertIsInstance( + domain_config, + _domain_config.DomainConfig, + ) + + # retrieve details of the (updated) domain config + + domain_config = self.operator_cloud.identity.get_domain_config( + self.domain, + ) + self.assertIsInstance( + domain_config, + _domain_config.DomainConfig, + ) + self.assertEqual(ldap_url, domain_config.ldap.url) + + # delete the domain config + + result = self.operator_cloud.identity.delete_domain_config( + self.domain, + ignore_missing=False, + ) + self.assertIsNone(result) diff --git a/openstack/tests/unit/identity/v3/test_domain_config.py b/openstack/tests/unit/identity/v3/test_domain_config.py new file mode 100644 index 000000000..26d774d9a --- /dev/null +++ b/openstack/tests/unit/identity/v3/test_domain_config.py @@ -0,0 +1,47 @@ +# 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. + +from openstack.identity.v3 import domain_config +from openstack.tests.unit import base + + +EXAMPLE = { + 'identity': { + 'driver': 'ldap', + }, + 'ldap': { + 'url': 'ldap://myldap.com:389/', + 'user_tree_dn': 'ou=Users,dc=my_new_root,dc=org', + }, +} + + +class TestDomainConfig(base.TestCase): + def test_basic(self): + sot = domain_config.DomainConfig() + self.assertEqual('config', sot.resource_key) + self.assertEqual('/domains/%(domain_id)s/config', sot.base_path) + self.assertTrue(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + + def test_make_it(self): + sot = domain_config.DomainConfig(**EXAMPLE) + self.assertIsInstance(sot.identity, domain_config.DomainConfigDriver) + self.assertEqual(EXAMPLE['identity']['driver'], sot.identity.driver) + self.assertIsInstance(sot.ldap, domain_config.DomainConfigLDAP) + self.assertEqual(EXAMPLE['ldap']['url'], sot.ldap.url) + self.assertEqual( + EXAMPLE['ldap']['user_tree_dn'], + sot.ldap.user_tree_dn, + ) diff --git a/openstack/tests/unit/identity/v3/test_proxy.py b/openstack/tests/unit/identity/v3/test_proxy.py index 4c40dff37..a0b7fcfad 100644 --- a/openstack/tests/unit/identity/v3/test_proxy.py +++ b/openstack/tests/unit/identity/v3/test_proxy.py @@ -15,6 +15,7 @@ import uuid from openstack.identity.v3 import _proxy from openstack.identity.v3 import credential from openstack.identity.v3 import domain +from openstack.identity.v3 import domain_config from openstack.identity.v3 import endpoint from openstack.identity.v3 import group from openstack.identity.v3 import policy @@ -85,6 +86,75 @@ class TestIdentityProxyDomain(TestIdentityProxyBase): self.verify_update(self.proxy.update_domain, domain.Domain) +class TestIdentityProxyDomainConfig(TestIdentityProxyBase): + def test_domain_config_create_attrs(self): + self.verify_create( + self.proxy.create_domain_config, + domain_config.DomainConfig, + method_args=['domain_id'], + method_kwargs={}, + expected_args=[], + expected_kwargs={ + 'domain_id': 'domain_id', + }, + ) + + def test_domain_config_delete(self): + self.verify_delete( + self.proxy.delete_domain_config, + domain_config.DomainConfig, + ignore_missing=False, + method_args=['domain_id'], + method_kwargs={}, + expected_args=[], + expected_kwargs={ + 'domain_id': 'domain_id', + }, + ) + + def test_domain_config_delete_ignore(self): + self.verify_delete( + self.proxy.delete_domain_config, + domain_config.DomainConfig, + ignore_missing=True, + method_args=['domain_id'], + method_kwargs={}, + expected_args=[], + expected_kwargs={ + 'domain_id': 'domain_id', + }, + ) + + # no find_domain_config + + def test_domain_config_get(self): + self.verify_get( + self.proxy.get_domain_config, + domain_config.DomainConfig, + method_args=['domain_id'], + method_kwargs={}, + expected_args=[], + expected_kwargs={ + 'domain_id': 'domain_id', + 'requires_id': False, + }, + ) + + # no domain_configs + + def test_domain_config_update(self): + self.verify_update( + self.proxy.update_domain_config, + domain_config.DomainConfig, + method_args=['domain_id'], + method_kwargs={}, + expected_args=[], + expected_kwargs={ + 'domain_id': 'domain_id', + }, + ) + + class TestIdentityProxyEndpoint(TestIdentityProxyBase): def test_endpoint_create_attrs(self): self.verify_create(self.proxy.create_endpoint, endpoint.Endpoint) diff --git a/releasenotes/notes/add-identity-domain-configuration-2e8bcaa20736b379.yaml b/releasenotes/notes/add-identity-domain-configuration-2e8bcaa20736b379.yaml new file mode 100644 index 000000000..6929c8be6 --- /dev/null +++ b/releasenotes/notes/add-identity-domain-configuration-2e8bcaa20736b379.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support for creating, updating and deleting domain configurations for + the identity service.