From e6cee094292cb6f07f684ac1e9cf03d42361e461 Mon Sep 17 00:00:00 2001 From: Blallo Date: Tue, 25 Jan 2022 23:46:24 +0100 Subject: [PATCH] Throw away previous async model implementation --- async_tests/test_async_ldap_model.py | 845 ++++------------------- async_tests/test_async_ldap_new_model.py | 196 ------ integration_tests/test_model.py | 2 +- src/phi/async_ldap/model.py | 519 +++----------- src/phi/async_ldap/new_model.py | 137 ---- test/aux_async_model.py | 2 +- 6 files changed, 243 insertions(+), 1458 deletions(-) delete mode 100644 async_tests/test_async_ldap_new_model.py delete mode 100644 src/phi/async_ldap/new_model.py diff --git a/async_tests/test_async_ldap_model.py b/async_tests/test_async_ldap_model.py index 4b83d92..5851bde 100644 --- a/async_tests/test_async_ldap_model.py +++ b/async_tests/test_async_ldap_model.py @@ -1,775 +1,196 @@ # -*- encoding: utf-8 -*- - -from argparse import Namespace import asyncio from async_generator import asynccontextmanager -from bonsai import NoSuchObjectError +from bonsai import LDAPEntry import mock import pytest -from phi.async_ldap.model import ( - get_class, - recall, - call_if_callable, - inheritance, - iter_children, - Entry, - build_heritage, - Hackers, - Robots, - Congregations, - Service, - create_new_, - User, - PhiEntryExists, - PhiEntryDoesNotExist, - PhiAttributeMissing, - PhiUnauthorized, - Group, -) -from phi.security import hash_pass +from phi.async_ldap.model import get_dn, User +from phi.async_ldap.mixins import Member +from phi.exceptions import PhiCannotExecute -BASE_DN = "dc=test,dc=abbiamoundominio,dc=org" -USER_LIST = [{"uid": ["conte_mascetti"]}, {"uid": ["perozzi"]}, {"uid": ["necchi"]}] -SERVICE_LIST = [{"uid": ["phi"]}, {"uid": ["irc"]}] -GROUP_LIST = [{"cn": ["amici_miei"]}, {"cn": ["antani"]}] -EXISTING_USER = [ - { - "dn": f"uid=existing_user,ou=Hackers,{BASE_DN}>", - "objectClass": ["inetOrgPerson", "organizationalPerson", "person", "top"], - "cn": ["exists"], - "sn": ["Existing User"], - "mail": ["existing@mail.org"], - "uid": ["existing_user"], - "userPassword": ["{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g="], - } -] -MISSING_AUTH_USER = [ - { - "dn": f"uid=existing_user,ou=Hackers,{BASE_DN}>", - "objectClass": ["inetOrgPerson", "organizationalPerson", "person", "top"], - "cn": ["exists"], - "sn": ["Existing User"], - "mail": ["existing@mail.org"], - "uid": ["existing_user"], - } -] -EXISTING_SERVICE = [ - { - "dn": f"uid=existing_service,ou=Services,{BASE_DN}>", - "objectClass": ["account", "simpleSecurityObject", "top"], - "uid": ["existing_service"], - "userPassword": ["{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g="], - } -] +BASE_DN = "dc=test,dc=domain,dc=tld" -class MockClient: - def __init__(self, base_dn, *args): - self._base_dn = base_dn - self.args = args - self.called_with_args = {} - self.username = f"uid=mock_connection,ou=Services,{base_dn}" +class MockClient(object): + def __init__(self, *args, **kwargs): + self.return_value = kwargs.get("return_value") + self.connect_called = False + self.conn = mock.MagicMock() + self.search_event = asyncio.Event() + self.add_event = asyncio.Event() + self.delete_event = asyncio.Event() + + async def connect_called_with_search(self): + return await self.search_event.wait() + + async def connect_called_with_add(self): + return await self.add_event.wait() + + async def connect_called_with_delete(self): + return await self.delete_event.wait() @property def base_dn(self): - return self._base_dn + return BASE_DN @asynccontextmanager async def connect(self, *args, **kwargs): - conn = Namespace() + self.connect_called = True - async def _search(*args, **kwargs): - self.called_with_args["search"] = {"args": args, "kwargs": kwargs} - if "Services" in self.args: - return SERVICE_LIST - elif "Groups" in self.args: - return GROUP_LIST - if "Users" in self.args or f"None=Entry,{BASE_DN}" in args: - return USER_LIST - elif f"uid=existing_user,ou=Hackers,{BASE_DN}" in args: - return EXISTING_USER - elif f"uid=existing_service,ou=Services,{BASE_DN}" in args: - return EXISTING_SERVICE - elif f"uid=not_existing,ou=Hackers,{BASE_DN}" in args: - return [] - elif f"uid=not_existing,ou=Services,{BASE_DN}" in args: - return [] - elif f"uid=missing_auth_user,ou=Hackers,{BASE_DN}" in args: - return MISSING_AUTH_USER + async def _search(*a, **kw): + self.search_event.set() + return self.return_value - conn.search = _search + async def _add(*a, **kw): + self.add_event.set() + return self.return_value - async def _add(*args, **kwargs): - self.called_with_args["add"] = {"args": args, "kwargs": kwargs} - return + async def _modify(*a, **kw): + return self.return_value - conn.add = _add + async def _delete(*a, **kw): + self.delete_event.set() + return self.return_value - async def _modify(*args, **kwargs): - self.called_with_args["modify"] = {"args": args, "kwargs": kwargs} - return + self.conn.search = mock.MagicMock(side_effect=_search) + self.conn.add = mock.MagicMock(side_effect=_add) + self.conn.modify = mock.MagicMock(side_effect=_modify) + self.conn.delete = mock.MagicMock(side_effect=_delete) - conn.modify = _modify - - yield conn + yield self.conn -@pytest.fixture -def client_fixture(): - return MockClient(BASE_DN) +cl = mock.MagicMock() +cl.base_dn = BASE_DN -@pytest.fixture -def client_fixture_multi(): - res = Namespace() - res.users = MockClient(BASE_DN, "Users") - res.services = MockClient(BASE_DN, "Services") - res.groups = MockClient(BASE_DN, "Groups") - return res +@pytest.mark.parametrize( + "input_obj, expected_result", + [ + (User(cl, "test_user"), f"uid=test_user,ou=Hackers,{BASE_DN}"), + ( + LDAPEntry(f"uid=test_user,ou=Hackers,{BASE_DN}"), + f"uid=test_user,ou=Hackers,{BASE_DN}", + ), + (f"uid=test_user,ou=Hackers,{BASE_DN}", f"uid=test_user,ou=Hackers,{BASE_DN}"), + ], +) +def test_get_dn(input_obj, expected_result): + assert get_dn(input_obj) == expected_result -@pytest.fixture -def lineage_fixture(): - class Grand: - @classmethod - def name(cls): - return cls.__name__ +def test_get_dn_raises(): + with pytest.raises(ValueError) as e: + _ = get_dn(object) - class Ma(Grand): - pass - - class Child(Ma): - pass - - grand = Grand() - ma = Ma() - child = Child() - - return Grand, Ma, Child, grand, ma, child + assert "Unacceptable input:" in str(e.value) -def test_get_class(): - c = MockClient(BASE_DN) +def test_repr(): + _cl = MockClient(return_value=None) + u = User(_cl, "test_user") - assert get_class(c) is MockClient + assert repr(u) == f"" -def test_recall(lineage_fixture): - Grand, Ma, Child, grand, ma, child = lineage_fixture +def test_str(): + _cl = MockClient(return_value=None) + u = User(_cl, "test_user") - assert object is recall(grand) - assert recall(grand) is recall(Grand) - assert Grand is recall(ma) - assert recall(ma) is recall(Ma) - assert Ma is recall(child) - assert recall(child) is recall(Child) + assert str(u) == f"" -def test_call_if_callable(): - class Dummy: - classattr = "classattr" +@pytest.mark.parametrize( + "input_obj", + [ + User(cl, "test_user"), + LDAPEntry(f"uid=test_user,ou=Hackers,{BASE_DN}"), + f"uid=test_user,ou=Hackers,{BASE_DN}", + ], +) +def test_eq(input_obj): + u = User(cl, "test_user") - @property - def prop(self): - return "prop" - - def func(self): - return "func" - - @classmethod - def clsmth(cls): - return "clsmth" - - d = Dummy() - - assert "classattr" == call_if_callable(d, "classattr") - assert "prop" == call_if_callable(d, "prop") - assert "func" == call_if_callable(d, "func") - assert "clsmth" == call_if_callable(d, "clsmth") - - -def test_inheritance(lineage_fixture): - Grand, Ma, Child, _, _, _ = lineage_fixture - - assert inheritance(Grand, "name") == "Grand" - assert inheritance(Ma, "name") == "Ma,Grand" - assert inheritance(Child, "name") == "Child,Ma,Grand" - assert inheritance(Child, "name", Grand) == "Child,Ma" - assert inheritance(Child, "name", Ma) == "Child" + assert u == input_obj @pytest.mark.asyncio -async def test_iter_children(): - LIST = [1, 2, 3, 4] +async def test_User_add(): + _cl = MockClient(return_value=None) + u = User(_cl, "test_user") - async def _async_gen(): - for i in LIST: - yield i + assert u.dn == f"uid=test_user,ou=Hackers,{BASE_DN}" - ALIST = _async_gen() + _ = await u.save() - assert LIST == await iter_children(ALIST) - - -def test_Entry(client_fixture): - e = Entry(client_fixture) - - assert e.base_dn == BASE_DN - assert e.client is not None - - -def test_Entry_name(client_fixture): - e = Entry(client_fixture) - - assert e.name() == "Entry" - - -def test_Entry_qualified_name(client_fixture): - e = Entry(client_fixture) - - assert e.qualified_name() == "None=Entry" - - -def test_Entry_dn(client_fixture): - e = Entry(client_fixture) - - assert e.dn == "None=Entry,{}".format(BASE_DN) + assert _cl.connect_called + assert await _cl.connect_called_with_add() @pytest.mark.asyncio -async def test_Entry_describe(client_fixture): - e = Entry(client_fixture) - - assert USER_LIST == await e.describe() - - -@pytest.mark.asyncio -async def test_build_heritage(client_fixture): - class MockAIterable(object): - client = None - - def __init__(self, elements): - self.elements = elements - - async def get_children(self): - for el in self.elements: - yield el - - class Dummy(object): - def __init__(self, *args, **kwargs): - self.id = sum([id(el) for el in args]) + sum( - [id(v) for _, v in kwargs.items()] - ) - - def __repr__(self): - return f"" - - def __eq__(self, other): - return self.id == other.id - - m = MockAIterable(USER_LIST) - - res = [el async for el in build_heritage(m, Dummy, attribute_id="uid")] - exp_res = [Dummy(el["uid"][0], None) for el in USER_LIST] - - assert res == exp_res - - -def test_Hackers(client_fixture_multi): - h = Hackers(client_fixture_multi.users) - - assert h.kind == "ou" - assert h.dn == "ou={},{}".format(h.name(), BASE_DN) - assert repr(h) == f"" - - -def test_Hackers_singleton(client_fixture): - other_client = MockClient(BASE_DN) - h1 = Hackers(client_fixture) - h2 = Hackers(other_client) - h3 = Hackers(client_fixture) - - assert client_fixture is not other_client - assert h1 is h3 - assert h2 is not h1 - - -@pytest.mark.asyncio -async def test_Entry_get_children(client_fixture_multi): - h = Hackers(client_fixture_multi.users) - - assert USER_LIST == [el async for el in h.get_children()] - - -@pytest.mark.asyncio -async def test_Hackers_anext(client_fixture_multi): - h = Hackers(client_fixture_multi.users) - - exp_res = [User(el["uid"][0], client_fixture_multi.users) for el in USER_LIST] - - assert exp_res == [el async for el in h] - - -def test_Hackers_children(client_fixture_multi): - h = Hackers(client_fixture_multi.users) - - assert USER_LIST == h.children - - -@pytest.mark.asyncio -async def test_Hackers_get_by_attr(client_fixture): - h = Hackers(client_fixture) - - res = await h.get_by_attr("uid", "existing_user") - - assert len(res) == 1 - assert res[0] is User("existing_user", client_fixture) - assert client_fixture.called_with_args["search"]["args"] == ( - f"uid=existing_user,ou=Hackers,{BASE_DN}", - 0, +async def test_User_modify(): + """ + This test does not use the MockClient check facilities because + of implementation details of the Entry class. + """ + _cl = MockClient( + return_value=[ + LDAPEntry(f"uid=test_user,ou=Hackers,{BASE_DN}"), + ] ) + u = User(_cl, "test_user") + + # This is the asyncio equivalent of a semaphore + modified = asyncio.Event() + + async def _mock_modify(): + modified.set() + + u._entry = mock.MagicMock() + u._entry.modify = mock.MagicMock(side_effect=_mock_modify) + + u["cn"] = "random_cn" + _ = await u.modify() + + assert _cl.connect_called + assert await _cl.connect_called_with_search() + # The `wait()` call here is needed to wait for `_mock_modify` + # to end its async execution + assert await modified.wait() @pytest.mark.asyncio -async def test_Hackers_get_by_attr_empty(client_fixture): - h = Hackers(client_fixture) +async def test_User_delete(): + _cl = MockClient(return_value=None) + u = User(_cl, "test_user") - res = await h.get_by_attr("uid", "not_existing") + _ = await u.delete() - assert res is None + assert _cl.connect_called + assert await _cl.connect_called_with_delete() @pytest.mark.asyncio -async def test_Hackers_get_by_uid(client_fixture): - h = Hackers(client_fixture) +async def test_User_get_invalid_attr_raises(): + _cl = MockClient(return_value=None) + u = User(_cl, "test_user") - res = await h.get_by_uid("existing_user") + with pytest.raises(PhiCannotExecute) as ex: + _ = u["iDoNotExist"] - assert res is User("existing_user", client_fixture) - - -def test_Robots(client_fixture_multi): - r = Robots(client_fixture_multi.services) - - assert r.kind == "ou" - assert r.dn == "ou={},{}".format(r.name(), BASE_DN) - assert repr(r) == f"" - - -def test_Robots_singleton(client_fixture): - other_client = MockClient(BASE_DN) - r1 = Robots(client_fixture) - r2 = Robots(other_client) - r3 = Robots(client_fixture) - - assert client_fixture is not other_client - assert r1 is r3 - assert r2 is not r1 + assert "iDoNotExist" in str(ex.value) + assert "is not an allowed ldap attribute" in str(ex.value) @pytest.mark.asyncio -async def test_Robots_anext(client_fixture_multi): - r = Robots(client_fixture_multi.services) +async def test_User_set_invalid_attr_raises(): + _cl = MockClient(return_value=None) + u = User(_cl, "test_user") - exp_res = [ - Service(el["uid"][0], client_fixture_multi.services) for el in SERVICE_LIST - ] + with pytest.raises(PhiCannotExecute) as ex: + u["iDoNotExist"] = "hello" - assert exp_res == [el async for el in r] - - -def test_Robots_children(client_fixture_multi): - r = Robots(client_fixture_multi.services) - - assert SERVICE_LIST == r.children - - -def test_Congregations(client_fixture_multi): - g = Congregations(client_fixture_multi.groups) - - assert g.kind == "ou" - assert g.dn == "ou={},{}".format(g.name(), BASE_DN) - assert repr(g) == f"" - - -def test_Congregations_singleton(client_fixture): - other_client = MockClient(BASE_DN) - g1 = Congregations(client_fixture) - g2 = Congregations(other_client) - g3 = Congregations(client_fixture) - - assert client_fixture is not other_client - assert g1 is g3 - assert g2 is not g1 - - -@pytest.mark.asyncio -async def test_Congregations_anext(client_fixture_multi): - c = Congregations(client_fixture_multi.groups) - - exp_res = [Group(el["cn"][0], client_fixture_multi.groups) for el in GROUP_LIST] - - assert exp_res == [el async for el in c] - - -def test_Congregations_children(client_fixture_multi): - c = Congregations(client_fixture_multi.groups) - - assert GROUP_LIST == c.children - - -@pytest.mark.asyncio -async def test_create_new_(client_fixture): - self = Namespace() - self.dn = f"uid=test,ou=Tests,{BASE_DN}" - self.client = client_fixture - self.object_class = ["a", "b", "c"] - ENTRY_DICT = {"attr1": "val1", "attr2": "val2", "attr3": "val3", "attr4": "val4"} - - res = await create_new_(self, **ENTRY_DICT) - - assert res is not None - assert client_fixture.called_with_args["add"]["args"][0] == res - assert res["objectClass"] == self.object_class - for k, v in ENTRY_DICT.items(): - assert v == res[k][0] - - -def test_User(client_fixture): - c = User("conte_mascetti", client_fixture) - - assert c.kind == "uid" - assert c.name == "conte_mascetti" - assert c.dn == "uid=conte_mascetti,ou=Hackers,{}".format(BASE_DN) - assert repr(c) == f"" - - -def test_User_unsettable_name(client_fixture): - c = User("conte_mascetti", client_fixture) - - with pytest.raises(RuntimeError) as e: - c.name = "totò" - - assert "Name property is not modifiable." in str(e.value) - - -def test_User_singleton(client_fixture): - other_client = MockClient(BASE_DN) - c1 = User("conte_mascetti", client_fixture) - c2 = User("perozzi", client_fixture) - c3 = User("conte_mascetti", client_fixture) - c4 = User("conte_mascetti", other_client) - - assert client_fixture is not other_client - assert c1 is c3 - assert c2 is not c1 - assert c4 is not c1 - - -@pytest.mark.asyncio -async def test_User_create_existing(client_fixture): - c = User("existing_user", client_fixture) - - with pytest.raises(PhiEntryExists) as e: - await c.create( - "existing@mail.org", password="password", sn="exists", cn="Existing User" - ) - - assert c.dn in str(e.value) - - -@pytest.mark.asyncio -async def test_User_create_not_existing(client_fixture): - c = User("not_existing", client_fixture) - - await c.create("not@existing.org", "password") - - assert client_fixture.called_with_args["search"]["args"] == (c.dn, 0) - assert c._entry["mail"][0] == "not@existing.org" - - -@pytest.mark.asyncio -async def test_User_sync_existing(client_fixture, caplog): - c = User("existing_user", client_fixture) - - await c.sync() - - assert f"User({c.name}): synced" in caplog.text - assert client_fixture.called_with_args["search"]["args"] == (c.dn, 0) - for k, v in EXISTING_USER[0].items(): - if k != "dn": - assert c._entry[k] == v - - -@pytest.mark.asyncio -async def test_User_sync_not_existing(client_fixture, caplog): - c = User("not_existing", client_fixture) - - with pytest.raises(PhiEntryDoesNotExist) as e: - await c.sync() - - assert c.dn in str(e.value) - assert client_fixture.called_with_args["search"]["args"] == (c.dn, 0) - - -@pytest.mark.asyncio -async def test_User_modify_existing(client_fixture, caplog): - c = User("existing_user", client_fixture) - c._entry = mock.MagicMock() - - async def _modify(): - return - - c._entry.modify = _modify - - await c.sync() - await c.modify("mail", "other@existing.org") - - assert f"User({c.name}): modified (mail)" in caplog.text - c._entry.__setitem__.assert_called_with("mail", "other@existing.org") - c._entry.__delitem__.assert_called_with("mail") - - -@pytest.mark.asyncio -async def test_User_modify_existing_append(client_fixture, caplog): - c = User("existing_user", client_fixture) - c._entry = mock.MagicMock() - - async def _modify(): - return - - c._entry.modify = _modify - - await c.sync() - await c.modify("mail", "other@existing.org", append=True) - - assert f"User({c.name}): modified (mail)" in caplog.text - c._entry.__setitem__.assert_called_with("mail", "other@existing.org") - c._entry.__delitem__.assert_not_called() - - -@pytest.mark.asyncio -async def test_User_modify_not_existing(client_fixture): - c = User("existing_user", client_fixture) - c._entry = mock.MagicMock() - attr = {"__delitem__.side_effect": KeyError} - c._entry.configure_mock(**attr) - - async def _modify(): - return - - c._entry.modify = _modify - - await c.sync() - with pytest.raises(PhiAttributeMissing) as e: - await c.modify("snafu", "modified") - - assert c.dn in str(e.value) - assert "snafu" in str(e.value) - - -@pytest.mark.asyncio -async def test_User_remove_existing(client_fixture, caplog): - c = User("existing_user", client_fixture) - c._entry = mock.MagicMock() - - def _delete(): - coro = mock.Mock(name="coroutine") - fn = mock.MagicMock(side_effect=asyncio.coroutine(coro)) - return fn - - delete = _delete() - c._entry.delete = delete - - await c.sync() - await c.remove() - - assert f"User({c.name}): removed" in caplog.text - delete.assert_called_once() - - -@pytest.mark.asyncio -async def test_User_remove_not_existing(client_fixture): - c = User("not_existing", client_fixture) - c._entry = mock.MagicMock() - - def _delete(): - fn = mock.MagicMock(side_effect=NoSuchObjectError) - return fn - - delete = _delete() - c._entry.delete = delete - - with pytest.raises(PhiEntryDoesNotExist) as e: - await c.remove() - - assert c.dn in str(e.value) - - -@pytest.mark.asyncio -async def test_User_modify_password(client_fixture, caplog): - c = User("existing_user", client_fixture) - - def _modify(): - coro = mock.Mock(name="coroutine") - fn = mock.MagicMock(side_effect=asyncio.coroutine(coro)) - return fn - - modify = _modify() - c.modify = modify - - await c.modify_password("new-password") - - modify.assert_called_with("userPassword", hash_pass("new-password")) - assert "User(existing_user): password modified" in caplog.text - - -@pytest.mark.asyncio -async def test_User_modify_password_raises(client_fixture): - c = User("existing_user", client_fixture) - - def _modify(): - coro = mock.Mock( - name="coroutine", side_effect=PhiAttributeMissing("TESTDN", "TESTATTR") - ) - fn = mock.MagicMock(side_effect=asyncio.coroutine(coro)) - return fn - - modify = _modify() - c.modify = modify - c.connection = mock.MagicMock() - c.connection.username = "existing_user" - - with pytest.raises(PhiUnauthorized) as e: - await c.modify_password("randombytes") - - modify.assert_called_with("userPassword", hash_pass("randombytes")) - assert f"uid=mock_connection,ou=Services,{BASE_DN}" in str(e.value.user) - - -@pytest.mark.asyncio -async def test_User_verify_password(client_fixture): - c = User("existing_user", client_fixture) - - res_true = await c.verify_password("password") - res_false = await c.verify_password("wrong") - - assert res_true - assert not res_false - - -@pytest.mark.asyncio -async def test_User_verify_password_raises_not_existing(client_fixture): - c = User("not_existing", client_fixture) - - with pytest.raises(PhiEntryDoesNotExist) as e: - await c.verify_password("randombytes") - - assert f"uid=not_existing,ou=Hackers,{BASE_DN}" in str(e.value) - - -@pytest.mark.asyncio -async def test_User_verify_password_raises_unauthorized(client_fixture): - c = User("missing_auth_user", client_fixture) - - with pytest.raises(PhiUnauthorized) as e: - await c.verify_password("password") - - assert f"uid=mock_connection,ou=Services,{BASE_DN}" in str(e.value.user) - - -def test_Service(client_fixture): - c = Service("phi", client_fixture) - - assert c.kind == "uid" - assert c.name == "phi" - assert c.dn == "uid=phi,ou=Services,{}".format(BASE_DN) - assert repr(c) == f"" - - -def test_Service_unsettable_name(client_fixture): - c = Service("phi", client_fixture) - - with pytest.raises(RuntimeError) as e: - c.name = "theta" - - assert "Name property is not modifiable." in str(e.value) - - -def test_Service_singleton(client_fixture): - other_client = MockClient(BASE_DN) - c1 = Service("phi", client_fixture) - c2 = Service("irc", client_fixture) - c3 = Service("phi", client_fixture) - c4 = Service("phi", other_client) - - assert client_fixture is not other_client - assert c1 is c3 - assert c2 is not c1 - assert c4 is not c1 - - -@pytest.mark.asyncio -async def test_Service_create_existing(client_fixture): - s = Service("existing_service", client_fixture) - - with pytest.raises(PhiEntryExists) as e: - await s.create("password") - - assert s.dn in str(e.value) - - -@pytest.mark.asyncio -async def test_Service_create_not_existing(client_fixture): - s = Service("not_existing", client_fixture) - - await s.create("password") - - assert client_fixture.called_with_args["search"]["args"] == (s.dn, 0) - - -@pytest.mark.asyncio -async def test_Service_sync_existing(client_fixture): - s = Service("existing_service", client_fixture) - - await s.sync() - - assert client_fixture.called_with_args["search"]["args"] == (s.dn, 0) - - -@pytest.mark.asyncio -async def test_Service_sync_non_existing(client_fixture): - pass - - -def test_Group(client_fixture): - c = Group("amici_miei", client_fixture) - - assert c.kind == "cn" - assert c.name == "amici_miei" - assert c.dn == "cn=amici_miei,ou=Groups,{}".format(BASE_DN) - assert repr(c) == f"" - - -def test_Group_unsettable_name(client_fixture): - c = Group("amici_miei", client_fixture) - - with pytest.raises(RuntimeError) as e: - c.name = "nemici" - - assert "Name property is not modifiable." in str(e.value) - - -def test_Group_singleton(client_fixture): - other_client = MockClient(BASE_DN) - c1 = Group("amici_miei", client_fixture) - c2 = Group("antani", client_fixture) - c3 = Group("amici_miei", client_fixture) - c4 = Group("amici_miei", other_client) - - assert client_fixture is not other_client - assert c1 is c3 - assert c2 is not c1 - assert c4 is not c1 + assert "iDoNotExist" in str(ex.value) + assert "is not an allowed ldap attribute" in str(ex.value) diff --git a/async_tests/test_async_ldap_new_model.py b/async_tests/test_async_ldap_new_model.py deleted file mode 100644 index d4da324..0000000 --- a/async_tests/test_async_ldap_new_model.py +++ /dev/null @@ -1,196 +0,0 @@ -# -*- encoding: utf-8 -*- -import asyncio - -from async_generator import asynccontextmanager -from bonsai import LDAPEntry -import mock -import pytest - -from phi.async_ldap.new_model import get_dn, User -from phi.async_ldap.mixins import Member -from phi.exceptions import PhiCannotExecute - -BASE_DN = "dc=test,dc=domain,dc=tld" - - -class MockClient(object): - def __init__(self, *args, **kwargs): - self.return_value = kwargs.get("return_value") - self.connect_called = False - self.conn = mock.MagicMock() - self.search_event = asyncio.Event() - self.add_event = asyncio.Event() - self.delete_event = asyncio.Event() - - async def connect_called_with_search(self): - return await self.search_event.wait() - - async def connect_called_with_add(self): - return await self.add_event.wait() - - async def connect_called_with_delete(self): - return await self.delete_event.wait() - - @property - def base_dn(self): - return BASE_DN - - @asynccontextmanager - async def connect(self, *args, **kwargs): - self.connect_called = True - - async def _search(*a, **kw): - self.search_event.set() - return self.return_value - - async def _add(*a, **kw): - self.add_event.set() - return self.return_value - - async def _modify(*a, **kw): - return self.return_value - - async def _delete(*a, **kw): - self.delete_event.set() - return self.return_value - - self.conn.search = mock.MagicMock(side_effect=_search) - self.conn.add = mock.MagicMock(side_effect=_add) - self.conn.modify = mock.MagicMock(side_effect=_modify) - self.conn.delete = mock.MagicMock(side_effect=_delete) - - yield self.conn - - -cl = mock.MagicMock() -cl.base_dn = BASE_DN - - -@pytest.mark.parametrize( - "input_obj, expected_result", - [ - (User(cl, "test_user"), f"uid=test_user,ou=Hackers,{BASE_DN}"), - ( - LDAPEntry(f"uid=test_user,ou=Hackers,{BASE_DN}"), - f"uid=test_user,ou=Hackers,{BASE_DN}", - ), - (f"uid=test_user,ou=Hackers,{BASE_DN}", f"uid=test_user,ou=Hackers,{BASE_DN}"), - ], -) -def test_get_dn(input_obj, expected_result): - assert get_dn(input_obj) == expected_result - - -def test_get_dn_raises(): - with pytest.raises(ValueError) as e: - _ = get_dn(object) - - assert "Unacceptable input:" in str(e.value) - - -def test_repr(): - _cl = MockClient(return_value=None) - u = User(_cl, "test_user") - - assert repr(u) == f"" - - -def test_str(): - _cl = MockClient(return_value=None) - u = User(_cl, "test_user") - - assert str(u) == f"" - - -@pytest.mark.parametrize( - "input_obj", - [ - User(cl, "test_user"), - LDAPEntry(f"uid=test_user,ou=Hackers,{BASE_DN}"), - f"uid=test_user,ou=Hackers,{BASE_DN}", - ], -) -def test_eq(input_obj): - u = User(cl, "test_user") - - assert u == input_obj - - -@pytest.mark.asyncio -async def test_User_add(): - _cl = MockClient(return_value=None) - u = User(_cl, "test_user") - - assert u.dn == f"uid=test_user,ou=Hackers,{BASE_DN}" - - _ = await u.save() - - assert _cl.connect_called - assert await _cl.connect_called_with_add() - - -@pytest.mark.asyncio -async def test_User_modify(): - """ - This test does not use the MockClient check facilities because - of implementation details of the Entry class. - """ - _cl = MockClient( - return_value=[ - LDAPEntry(f"uid=test_user,ou=Hackers,{BASE_DN}"), - ] - ) - u = User(_cl, "test_user") - - # This is the asyncio equivalent of a semaphore - modified = asyncio.Event() - - async def _mock_modify(): - modified.set() - - u._entry = mock.MagicMock() - u._entry.modify = mock.MagicMock(side_effect=_mock_modify) - - u["cn"] = "random_cn" - _ = await u.modify() - - assert _cl.connect_called - assert await _cl.connect_called_with_search() - # The `wait()` call here is needed to wait for `_mock_modify` - # to end its async execution - assert await modified.wait() - - -@pytest.mark.asyncio -async def test_User_delete(): - _cl = MockClient(return_value=None) - u = User(_cl, "test_user") - - _ = await u.delete() - - assert _cl.connect_called - assert await _cl.connect_called_with_delete() - - -@pytest.mark.asyncio -async def test_User_get_invalid_attr_raises(): - _cl = MockClient(return_value=None) - u = User(_cl, "test_user") - - with pytest.raises(PhiCannotExecute) as ex: - _ = u["iDoNotExist"] - - assert "iDoNotExist" in str(ex.value) - assert "is not an allowed ldap attribute" in str(ex.value) - - -@pytest.mark.asyncio -async def test_User_set_invalid_attr_raises(): - _cl = MockClient(return_value=None) - u = User(_cl, "test_user") - - with pytest.raises(PhiCannotExecute) as ex: - u["iDoNotExist"] = "hello" - - assert "iDoNotExist" in str(ex.value) - assert "is not an allowed ldap attribute" in str(ex.value) diff --git a/integration_tests/test_model.py b/integration_tests/test_model.py index 20ae496..131751a 100644 --- a/integration_tests/test_model.py +++ b/integration_tests/test_model.py @@ -5,7 +5,7 @@ import asyncio from async_generator import asynccontextmanager import pytest -from phi.async_ldap.new_model import ( +from phi.async_ldap.model import ( Hackers, User, Robots, diff --git a/src/phi/async_ldap/model.py b/src/phi/async_ldap/model.py index d81e514..5e08e68 100644 --- a/src/phi/async_ldap/model.py +++ b/src/phi/async_ldap/model.py @@ -1,440 +1,137 @@ # -*- encoding: utf-8 -*- -import asyncio -import logging -import typing as T +from bonsai import LDAPEntry +from multidict import MultiDict -from bonsai import LDAPEntry, LDAPModOp, NoSuchObjectError # type: ignore - -from phi.exceptions import ( - PhiAttributeMissing, - PhiEntryDoesNotExist, - PhiEntryExists, - PhiUnauthorized, - PhiUnexpectedRuntimeValue, -) -from phi.logging import get_logger -from phi.security import hash_pass - -log = get_logger(__name__) -alog = logging.getLogger("asyncio") -alog.setLevel(logging.DEBUG) +from phi.async_ldap import mixins -def get_class(obj): - """ - Return the input if input is a class, else tryes to get the class from the - `__class__` method. - """ - if type(obj) is type: +def parse_dn(dn): + return MultiDict(e.split("=") for e in dn.split(",")) + + +def get_dn(obj): + if isinstance(obj, mixins.Entry): + return obj.dn + elif isinstance(obj, LDAPEntry): + return obj["dn"] + elif isinstance(obj, str): return obj - return obj.__class__ + else: + raise ValueError(f"Unacceptable input: {obj}") -def recall(cls): - """ - Prints the name of the parent class. - """ - _cls = get_class(cls) - return _cls.__bases__[0] +class User(mixins.Member, mixins.Entry, mixins.Singleton): + object_class = [ + "inetOrgPerson", + "simpleSecurityObject", + "organizationalPerson", + "person", + "top", + ] + _instances = dict() # type: ignore + id_tag = "uid" + ou = "Hackers" + ldap_attributes = ["uid", "ou", "cn", "sn", "mail", "userPassword"] + async def iter_groups(self): # To be monkeypatched later + pass # pragma: no cover -def call_if_callable(cls, attr): - """ - Tell between methods and properties. Call if method. - """ - _attr = getattr(cls, attr) - if "__call__" in dir(_attr): - return _attr() - return _attr + async def groups(self): + return [g async for g in self.iter_groups()] + async def delete(self): + async for group in self.iter_groups(): + await group.remove_member(self) + await super().delete() -def inheritance(obj, attr, root_cls=object): - """ - Concatenates the value obtained from invoking the method attr - on the class and on any parent class until root_cls. - """ - res = call_if_callable(obj, attr) - base = get_class(obj) - while base is not root_cls: - if base is not get_class(obj): - res += ",{}".format(call_if_callable(base, attr)) - base = recall(base) - return res.strip(",") +class Hackers(mixins.OrganizationalUnit, mixins.Entry, mixins.Singleton): + _instances = dict() # type: ignore + ou = "Hackers" + child_class = User -def singletonize(cls, name): - """ - Helper function to be plugged in `__new__` method to make the class a singleton. - """ - if name not in cls._instances: - cls._instances[name] = object.__new__(cls) - return cls._instances[name] +class Service(mixins.Member, mixins.Entry, mixins.Singleton): + object_class = ["simpleSecurityObject", "account", "top"] + _instances = dict() # type: ignore + id_tag = "uid" + ou = "Robots" + ldap_attributes = ["uid", "ou", "userPassword"] -async def iter_children(children): - return [child async for child in children] + async def iter_groups(self): # To be monkeypatched later + pass # pragma: no cover + async def groups(self): + return [g async for g in self.iter_groups()] -class Entry(object): - """ - LDAP Entry. Interface to LDAP. - """ + async def delete(self): + async for group in self.iter_groups(): + await group.remove_member(self) + await super().delete() - kind: T.Union[None, str] = None - _name: T.Union[None, str] = None - @classmethod - def name(cls): - if "_name" in dir(cls) and cls._name is not None: - return cls._name - return cls.__name__ +class Robots(mixins.OrganizationalUnit, mixins.Entry, mixins.Singleton): + _instances = dict() # type: ignore + ou = "Robots" + child_class = Service - @classmethod - def qualified_name(cls): - return "{}={}".format(cls.kind, cls.name()) - def __init__(self, client): - self.client = client - self.base_dn = client.base_dn - - def __repr__(self): - return f"<{call_if_callable(self, 'name')} {self.dn}>" - - def __dict__(self): - return self._dict - - def __aiter__(self): - return self - - async def get_children(self): - async with self.client.connect(is_async=True) as conn: - for el in await conn.search(self.dn, 2): - yield el - - @property - def children(self): - """ - Synchronous property to enumerate the children. - """ - return asyncio.run(iter_children(self.get_children())) - - @property - def dn(self): - return "{},{}".format(inheritance(self, "qualified_name", Entry), self.base_dn) - - @property - def attributes(self): - raise NotImplemented() - - async def describe(self): - async with self.client.connect(is_async=True) as conn: - # This returns a list of dicts. It should always contain only one item: - # the one we are interested. - res = await conn.search(self.dn, 0) - if len(res) == 0: - return - elif len(res) > 1: - raise PhiUnexpectedRuntimeValue( - "return value should be no more than one", res - ) - res = res[0] - res.update({"dn": self.dn, "name": self.name}) - return res - - -async def build_heritage(obj, child_class, attribute_id="uid"): - """ - Given the object and the child class, yields the - instances of the children. - """ - async for child in obj.get_children(): - if attribute_id in child: - _name = child[attribute_id][0] - yield child_class(_name, obj.client) - - -class Hackers(Entry): - """ - This class is where Users belong. - """ - - kind = "ou" - _name = "Hackers" - _instances: T.Dict[str, Entry] = dict() - - def __new__(cls, client, *args, **kwargs): - return singletonize(cls, f"{cls._name}-{id(client)}") - - def __init__(self, client, *args, **kwargs): - super().__init__(client) - self._hackers = build_heritage(self, User) - - async def __anext__(self): - try: - return await self._hackers.__anext__() - except StopAsyncIteration: - self._hackers = build_heritage(self, User) - raise - - async def get_by_attr(self, attr, value): - async with self.client.connect(is_async=True) as conn: - res = await conn.search("{}={},{}".format(attr, value, self.dn), 0) - if len(res) == 0: - return None - return [User(r["uid"][0], self.client) for r in res] - - async def get_by_uid(self, uid): - res = await self.get_by_attr("uid", uid) - return res[0] - - @property - def attributes(self): - return {"kind": self.kind} - - -class Robots(Entry): - """ - This class is where Services belong. - """ - - kind = "ou" - _name = "Services" - _instances: T.Dict[str, Entry] = dict() - - def __new__(cls, client, *args, **kwargs): - return singletonize(cls, f"{cls._name}-{id(client)}") - - def __init__(self, client, *args, **kwargs): - super().__init__(client) - self._robots = build_heritage(self, Service) - - async def __anext__(self): - try: - return await self._robots.__anext__() - except StopAsyncIteration: - self._robots = build_heritage(self, Service) - raise - - -class Congregations(Entry): - """ - This class is where Groups belong. - """ - - kind = "ou" - _name = "Groups" - _instances: T.Dict[str, Entry] = dict() - - def __new__(cls, client, *args, **kwargs): - return singletonize(cls, f"{cls._name}-{id(client)}") - - def __init__(self, client, *args, **kwargs): - super().__init__(client) - self._groups = build_heritage(self, Group, attribute_id="cn") - - async def __anext__(self): - try: - return await self._groups.__anext__() - except StopAsyncIteration: - self._groups = build_heritage(self, Group, attribute_id="cn") - raise - - -async def create_new_(self, **kwargs): - entry = LDAPEntry(self.dn) - entry["objectClass"] = self.object_class - for k, w in kwargs.items(): - entry[k] = w - async with self.client.connect(is_async=True) as conn: - await conn.add(entry) - return entry - - -class User(Hackers): - """ - This class models a user. Users may have attributes - and belong one or more Group(s). - """ - - kind = "uid" - _instances: T.Dict[str, Entry] = dict() - object_class = ["inetOrgPerson", "organizationalPerson", "person", "top"] - - def __new__(cls, client, name, *args, **kwargs): - return singletonize(cls, f"{name}-{id(client)}") - - def __init__(self, client, name, *args, **kwargs): - super().__init__(client, *args, **kwargs) - self._name = name - self._entry = LDAPEntry(self.dn) - - def __repr__(self): - return f"<{get_class(self).__name__}({self.name}) {self.dn}>" - - def qualified_name(self): - return "{}={}".format(self.kind, self.name) - - @property - def name(self): - return self._name - - @name.setter - def name(self, name): - raise RuntimeError("Name property is not modifiable.") - - async def create(self, mail, password=None, sn=None, cn=None): - async with self.client.connect(is_async=True) as conn: - res = await conn.search(self.dn, 0) - if len(res) > 0: - raise PhiEntryExists(self.dn) - _sn = sn if sn is not None else self.name - _cn = cn if cn is not None else self.name - hashed = hash_pass(password) - self._entry = await create_new_( - self, uid=self.name, mail=mail, sn=_sn, cn=_cn, userPassword=hashed - ) - alog.info("User(%s): created", self.name) - - async def sync(self): - async with self.client.connect(is_async=True) as conn: - res = await conn.search(self.dn, 0) - if len(res) == 0: - raise PhiEntryDoesNotExist(self.dn) - for k, v in res[0].items(): - if not k == "dn": - self._entry[k] = v - alog.info("User(%s): synced", self.name) - - async def modify(self, key, value, append=False): - async with self.client.connect(is_async=True) as conn: - self._entry.connection = conn - try: - if not append: - del self._entry[key] - self._entry[key] = value - except KeyError: - raise PhiAttributeMissing(self.dn, key) - await self._entry.modify() - alog.info("User(%s): modified (%s)", self.name, key) - - async def remove(self): - async with self.client.connect(is_async=True) as conn: - self._entry.connection = conn - try: - await self._entry.delete() - alog.info("User(%s): removed", self.name) - except NoSuchObjectError: - raise PhiEntryDoesNotExist(self.dn) - - async def modify_password(self, new_pass, old_pass=None): - try: - await self.modify("userPassword", hash_pass(new_pass)) - except PhiAttributeMissing: - raise PhiUnauthorized(user=self.client.username,) - alog.info("User(%s): password modified", self.name) - - async def verify_password(self, given_pass): - async with self.client.connect(is_async=True) as conn: - res = await conn.search(self.dn, 0) - if len(res) == 0: - raise PhiEntryDoesNotExist(self.dn) - try: - match_pass = res[0]["userPassword"][0] == hash_pass(given_pass) - except KeyError: - raise PhiUnauthorized(user=self.client.username,) - return match_pass - - -class Service(Robots): - """ - This class models a system user (i.e. users that are ancillary to - services on a machine). System user may have attributes - and belong to one or more Group(s). - """ - - kind = "uid" - _instances: T.Dict[str, Entry] = dict() - object_class = ["account", "top", "simpleSecurityObject"] - - def __new__(cls, client, name, *args, **kwargs): - return singletonize(cls, f"{name}-{id(client)}") - - def __init__(self, client, name, *args, **kwargs): - super().__init__(client, *args, **kwargs) - self._name = name - self._entry = LDAPEntry(self.dn) - - def __repr__(self): - return f"<{get_class(self).__name__}({self.name}) {self.dn}>" - - def qualified_name(self): - return "{}={}".format(self.kind, self.name) - - @property - def name(self): - return self._name - - @name.setter - def name(self, name): - raise RuntimeError("Name property is not modifiable.") - - async def create(self, password): - async with self.client.connect(is_async=True) as conn: - res = await conn.search(self.dn, 0) - if len(res) > 0: - raise PhiEntryExists(self.dn) - self._entry = await create_new_(self, uid=self.name, userPassword=password) - alog.info("Service(%s): created", self.name) - - async def sync(self): - async with self.client.connect(is_async=True) as conn: - res = await conn.search(self.dn, 0) - if len(res) == 0: - raise PhiEntryDoesNotExist(self.dn) - for k, v in res[0].items(): - if not k == "dn": - self._entry[k] = v - alog.info("Service(%s): synced", self.name) - - async def remove(self): - pass - - async def modify_password(self, new_pass, old_pass=None): - pass - - async def verify_password(self, given_pass): - pass - - -class Group(Congregations): - """ - This class models a group. Groups may have attributes - and may have Users and Services belonging to them. - """ - - kind = "cn" - _instances: T.Dict[str, Entry] = dict() +class Group(mixins.Member, mixins.Entry, mixins.Singleton): object_class = ["groupOfNames", "top"] + _instances = dict() # type: ignore + id_tag = "cn" + ou = "Congregations" + ldap_attributes = ["cn", "ou", "member"] + memeber_classes = {"Hackers": User, "Robots": Service} + empty = False - def __new__(cls, client, name, *args, **kwargs): - return singletonize(cls, f"{name}-{id(client)}") + async def add_member(self, member): + member_dn = get_dn(member) + self._entry["member"].append(member_dn) + await self.modify() - def __init__(self, client, name, *args, **kwargs): - super().__init__(client, *args, **kwargs) - self._name = name - self._entry = LDAPEntry(self.dn) + async def remove_member(self, member): + new_group_members = [get_dn(m) async for m in self.get_members() if member != m] + if len(new_group_members) == 0: + await self.delete() + self.empty = True + else: + self._entry["member"] = new_group_members + await self.modify() - def __repr__(self): - return f"<{get_class(self).__name__}({self.name}) {self.dn}>" + async def get_members(self): + await self.sync() + for member in self._entry.get("member", []): + dn = parse_dn(member) + yield self.memeber_classes.get(dn["ou"])(self.client, dn["uid"]) - def qualified_name(self): - return "{}={}".format(self.kind, self.name) - @property - def name(self): - return self._name +class Congregations(mixins.OrganizationalUnit, mixins.Entry, mixins.Singleton): + _instances = dict() # type: ignore + ou = "Congregations" + child_class = Group - @name.setter - def name(self, name): - raise RuntimeError("Name property is not modifiable.") + +# We define this async method here **after** `User`, `Service` and `Group` have been +# defined, in order to avoid definition loops that would prevent the code from running. +# Indeed, this function explicitely uses `Group` but is needed as a `User` and `Service` +# method. In turn, `Group` definition relies on both `User` and `Service` being yet +# defined. +async def iter_groups(self): + async with self.client.connect(is_async=True) as conn: + res = await conn.search(f"{self.dn}", 2, attrlist=["memberOf"]) + if not res or len(res) == 0: + return + elif len(res) == 1: + for group in res[0].get("memberOf", []): + yield Group(self.client, parse_dn(get_dn(group))["cn"]) + else: + raise PhiUnexpectedRuntimeValue( + "return value should be no more than one", res + ) + + +# Monkeypatch +User.iter_groups = iter_groups # type: ignore +Service.iter_groups = iter_groups # type: ignore diff --git a/src/phi/async_ldap/new_model.py b/src/phi/async_ldap/new_model.py deleted file mode 100644 index 5e08e68..0000000 --- a/src/phi/async_ldap/new_model.py +++ /dev/null @@ -1,137 +0,0 @@ -# -*- encoding: utf-8 -*- - -from bonsai import LDAPEntry -from multidict import MultiDict - -from phi.async_ldap import mixins - - -def parse_dn(dn): - return MultiDict(e.split("=") for e in dn.split(",")) - - -def get_dn(obj): - if isinstance(obj, mixins.Entry): - return obj.dn - elif isinstance(obj, LDAPEntry): - return obj["dn"] - elif isinstance(obj, str): - return obj - else: - raise ValueError(f"Unacceptable input: {obj}") - - -class User(mixins.Member, mixins.Entry, mixins.Singleton): - object_class = [ - "inetOrgPerson", - "simpleSecurityObject", - "organizationalPerson", - "person", - "top", - ] - _instances = dict() # type: ignore - id_tag = "uid" - ou = "Hackers" - ldap_attributes = ["uid", "ou", "cn", "sn", "mail", "userPassword"] - - async def iter_groups(self): # To be monkeypatched later - pass # pragma: no cover - - async def groups(self): - return [g async for g in self.iter_groups()] - - async def delete(self): - async for group in self.iter_groups(): - await group.remove_member(self) - await super().delete() - - -class Hackers(mixins.OrganizationalUnit, mixins.Entry, mixins.Singleton): - _instances = dict() # type: ignore - ou = "Hackers" - child_class = User - - -class Service(mixins.Member, mixins.Entry, mixins.Singleton): - object_class = ["simpleSecurityObject", "account", "top"] - _instances = dict() # type: ignore - id_tag = "uid" - ou = "Robots" - ldap_attributes = ["uid", "ou", "userPassword"] - - async def iter_groups(self): # To be monkeypatched later - pass # pragma: no cover - - async def groups(self): - return [g async for g in self.iter_groups()] - - async def delete(self): - async for group in self.iter_groups(): - await group.remove_member(self) - await super().delete() - - -class Robots(mixins.OrganizationalUnit, mixins.Entry, mixins.Singleton): - _instances = dict() # type: ignore - ou = "Robots" - child_class = Service - - -class Group(mixins.Member, mixins.Entry, mixins.Singleton): - object_class = ["groupOfNames", "top"] - _instances = dict() # type: ignore - id_tag = "cn" - ou = "Congregations" - ldap_attributes = ["cn", "ou", "member"] - memeber_classes = {"Hackers": User, "Robots": Service} - empty = False - - async def add_member(self, member): - member_dn = get_dn(member) - self._entry["member"].append(member_dn) - await self.modify() - - async def remove_member(self, member): - new_group_members = [get_dn(m) async for m in self.get_members() if member != m] - if len(new_group_members) == 0: - await self.delete() - self.empty = True - else: - self._entry["member"] = new_group_members - await self.modify() - - async def get_members(self): - await self.sync() - for member in self._entry.get("member", []): - dn = parse_dn(member) - yield self.memeber_classes.get(dn["ou"])(self.client, dn["uid"]) - - -class Congregations(mixins.OrganizationalUnit, mixins.Entry, mixins.Singleton): - _instances = dict() # type: ignore - ou = "Congregations" - child_class = Group - - -# We define this async method here **after** `User`, `Service` and `Group` have been -# defined, in order to avoid definition loops that would prevent the code from running. -# Indeed, this function explicitely uses `Group` but is needed as a `User` and `Service` -# method. In turn, `Group` definition relies on both `User` and `Service` being yet -# defined. -async def iter_groups(self): - async with self.client.connect(is_async=True) as conn: - res = await conn.search(f"{self.dn}", 2, attrlist=["memberOf"]) - if not res or len(res) == 0: - return - elif len(res) == 1: - for group in res[0].get("memberOf", []): - yield Group(self.client, parse_dn(get_dn(group))["cn"]) - else: - raise PhiUnexpectedRuntimeValue( - "return value should be no more than one", res - ) - - -# Monkeypatch -User.iter_groups = iter_groups # type: ignore -Service.iter_groups = iter_groups # type: ignore diff --git a/test/aux_async_model.py b/test/aux_async_model.py index eb58fc4..c6e185a 100644 --- a/test/aux_async_model.py +++ b/test/aux_async_model.py @@ -3,7 +3,7 @@ import asyncio from pprint import pprint as pp -from phi.async_ldap.new_model import ( +from phi.async_ldap.model import ( Hackers, User, Robots,