Throw away previous async model implementation
This commit is contained in:
parent
c5a4b86349
commit
e6cee09429
|
@ -1,775 +1,196 @@
|
||||||
# -*- encoding: utf-8 -*-
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
from argparse import Namespace
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from async_generator import asynccontextmanager
|
from async_generator import asynccontextmanager
|
||||||
from bonsai import NoSuchObjectError
|
from bonsai import LDAPEntry
|
||||||
import mock
|
import mock
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from phi.async_ldap.model import (
|
from phi.async_ldap.model import get_dn, User
|
||||||
get_class,
|
from phi.async_ldap.mixins import Member
|
||||||
recall,
|
from phi.exceptions import PhiCannotExecute
|
||||||
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
|
|
||||||
|
|
||||||
BASE_DN = "dc=test,dc=abbiamoundominio,dc=org"
|
BASE_DN = "dc=test,dc=domain,dc=tld"
|
||||||
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="],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class MockClient:
|
class MockClient(object):
|
||||||
def __init__(self, base_dn, *args):
|
def __init__(self, *args, **kwargs):
|
||||||
self._base_dn = base_dn
|
self.return_value = kwargs.get("return_value")
|
||||||
self.args = args
|
self.connect_called = False
|
||||||
self.called_with_args = {}
|
self.conn = mock.MagicMock()
|
||||||
self.username = f"uid=mock_connection,ou=Services,{base_dn}"
|
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
|
@property
|
||||||
def base_dn(self):
|
def base_dn(self):
|
||||||
return self._base_dn
|
return BASE_DN
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def connect(self, *args, **kwargs):
|
async def connect(self, *args, **kwargs):
|
||||||
conn = Namespace()
|
self.connect_called = True
|
||||||
|
|
||||||
async def _search(*args, **kwargs):
|
async def _search(*a, **kw):
|
||||||
self.called_with_args["search"] = {"args": args, "kwargs": kwargs}
|
self.search_event.set()
|
||||||
if "Services" in self.args:
|
return self.return_value
|
||||||
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
|
|
||||||
|
|
||||||
conn.search = _search
|
async def _add(*a, **kw):
|
||||||
|
self.add_event.set()
|
||||||
|
return self.return_value
|
||||||
|
|
||||||
async def _add(*args, **kwargs):
|
async def _modify(*a, **kw):
|
||||||
self.called_with_args["add"] = {"args": args, "kwargs": kwargs}
|
return self.return_value
|
||||||
return
|
|
||||||
|
|
||||||
conn.add = _add
|
async def _delete(*a, **kw):
|
||||||
|
self.delete_event.set()
|
||||||
|
return self.return_value
|
||||||
|
|
||||||
async def _modify(*args, **kwargs):
|
self.conn.search = mock.MagicMock(side_effect=_search)
|
||||||
self.called_with_args["modify"] = {"args": args, "kwargs": kwargs}
|
self.conn.add = mock.MagicMock(side_effect=_add)
|
||||||
return
|
self.conn.modify = mock.MagicMock(side_effect=_modify)
|
||||||
|
self.conn.delete = mock.MagicMock(side_effect=_delete)
|
||||||
|
|
||||||
conn.modify = _modify
|
yield self.conn
|
||||||
|
|
||||||
yield conn
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
cl = mock.MagicMock()
|
||||||
def client_fixture():
|
cl.base_dn = BASE_DN
|
||||||
return MockClient(BASE_DN)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.mark.parametrize(
|
||||||
def client_fixture_multi():
|
"input_obj, expected_result",
|
||||||
res = Namespace()
|
[
|
||||||
res.users = MockClient(BASE_DN, "Users")
|
(User(cl, "test_user"), f"uid=test_user,ou=Hackers,{BASE_DN}"),
|
||||||
res.services = MockClient(BASE_DN, "Services")
|
(
|
||||||
res.groups = MockClient(BASE_DN, "Groups")
|
LDAPEntry(f"uid=test_user,ou=Hackers,{BASE_DN}"),
|
||||||
return res
|
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 test_get_dn_raises():
|
||||||
def lineage_fixture():
|
with pytest.raises(ValueError) as e:
|
||||||
class Grand:
|
_ = get_dn(object)
|
||||||
@classmethod
|
|
||||||
def name(cls):
|
|
||||||
return cls.__name__
|
|
||||||
|
|
||||||
class Ma(Grand):
|
assert "Unacceptable input:" in str(e.value)
|
||||||
pass
|
|
||||||
|
|
||||||
class Child(Ma):
|
|
||||||
pass
|
|
||||||
|
|
||||||
grand = Grand()
|
|
||||||
ma = Ma()
|
|
||||||
child = Child()
|
|
||||||
|
|
||||||
return Grand, Ma, Child, grand, ma, child
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_class():
|
def test_repr():
|
||||||
c = MockClient(BASE_DN)
|
_cl = MockClient(return_value=None)
|
||||||
|
u = User(_cl, "test_user")
|
||||||
|
|
||||||
assert get_class(c) is MockClient
|
assert repr(u) == f"<User uid=test_user,ou=Hackers,{BASE_DN}>"
|
||||||
|
|
||||||
|
|
||||||
def test_recall(lineage_fixture):
|
def test_str():
|
||||||
Grand, Ma, Child, grand, ma, child = lineage_fixture
|
_cl = MockClient(return_value=None)
|
||||||
|
u = User(_cl, "test_user")
|
||||||
|
|
||||||
assert object is recall(grand)
|
assert str(u) == f"<User uid=test_user,ou=Hackers,{BASE_DN}>"
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def test_call_if_callable():
|
@pytest.mark.parametrize(
|
||||||
class Dummy:
|
"input_obj",
|
||||||
classattr = "classattr"
|
[
|
||||||
|
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
|
assert u == input_obj
|
||||||
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"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_iter_children():
|
async def test_User_add():
|
||||||
LIST = [1, 2, 3, 4]
|
_cl = MockClient(return_value=None)
|
||||||
|
u = User(_cl, "test_user")
|
||||||
|
|
||||||
async def _async_gen():
|
assert u.dn == f"uid=test_user,ou=Hackers,{BASE_DN}"
|
||||||
for i in LIST:
|
|
||||||
yield i
|
|
||||||
|
|
||||||
ALIST = _async_gen()
|
_ = await u.save()
|
||||||
|
|
||||||
assert LIST == await iter_children(ALIST)
|
assert _cl.connect_called
|
||||||
|
assert await _cl.connect_called_with_add()
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_Entry_describe(client_fixture):
|
async def test_User_modify():
|
||||||
e = Entry(client_fixture)
|
"""
|
||||||
|
This test does not use the MockClient check facilities because
|
||||||
assert USER_LIST == await e.describe()
|
of implementation details of the Entry class.
|
||||||
|
"""
|
||||||
|
_cl = MockClient(
|
||||||
@pytest.mark.asyncio
|
return_value=[
|
||||||
async def test_build_heritage(client_fixture):
|
LDAPEntry(f"uid=test_user,ou=Hackers,{BASE_DN}"),
|
||||||
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"<Dummy({self.id})>"
|
|
||||||
|
|
||||||
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"<Hackers {h.kind}=Hackers,{BASE_DN}>"
|
|
||||||
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_Hackers_get_by_attr_empty(client_fixture):
|
async def test_User_delete():
|
||||||
h = Hackers(client_fixture)
|
_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
|
@pytest.mark.asyncio
|
||||||
async def test_Hackers_get_by_uid(client_fixture):
|
async def test_User_get_invalid_attr_raises():
|
||||||
h = Hackers(client_fixture)
|
_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)
|
assert "iDoNotExist" in str(ex.value)
|
||||||
|
assert "is not an allowed ldap attribute" in str(ex.value)
|
||||||
|
|
||||||
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"<Services {r.kind}=Services,{BASE_DN}>"
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_Robots_anext(client_fixture_multi):
|
async def test_User_set_invalid_attr_raises():
|
||||||
r = Robots(client_fixture_multi.services)
|
_cl = MockClient(return_value=None)
|
||||||
|
u = User(_cl, "test_user")
|
||||||
|
|
||||||
exp_res = [
|
with pytest.raises(PhiCannotExecute) as ex:
|
||||||
Service(el["uid"][0], client_fixture_multi.services) for el in SERVICE_LIST
|
u["iDoNotExist"] = "hello"
|
||||||
]
|
|
||||||
|
|
||||||
assert exp_res == [el async for el in r]
|
assert "iDoNotExist" in str(ex.value)
|
||||||
|
assert "is not an allowed ldap attribute" in str(ex.value)
|
||||||
|
|
||||||
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"<Groups {g.kind}=Groups,{BASE_DN}>"
|
|
||||||
|
|
||||||
|
|
||||||
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"<User({c.name}) {c.dn}>"
|
|
||||||
|
|
||||||
|
|
||||||
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"<Service({c.name}) {c.dn}>"
|
|
||||||
|
|
||||||
|
|
||||||
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"<Group({c.name}) {c.dn}>"
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
|
@ -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"<User uid=test_user,ou=Hackers,{BASE_DN}>"
|
|
||||||
|
|
||||||
|
|
||||||
def test_str():
|
|
||||||
_cl = MockClient(return_value=None)
|
|
||||||
u = User(_cl, "test_user")
|
|
||||||
|
|
||||||
assert str(u) == f"<User uid=test_user,ou=Hackers,{BASE_DN}>"
|
|
||||||
|
|
||||||
|
|
||||||
@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)
|
|
|
@ -5,7 +5,7 @@ import asyncio
|
||||||
from async_generator import asynccontextmanager
|
from async_generator import asynccontextmanager
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from phi.async_ldap.new_model import (
|
from phi.async_ldap.model import (
|
||||||
Hackers,
|
Hackers,
|
||||||
User,
|
User,
|
||||||
Robots,
|
Robots,
|
||||||
|
|
|
@ -1,440 +1,137 @@
|
||||||
# -*- encoding: utf-8 -*-
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
import asyncio
|
from bonsai import LDAPEntry
|
||||||
import logging
|
from multidict import MultiDict
|
||||||
import typing as T
|
|
||||||
|
|
||||||
from bonsai import LDAPEntry, LDAPModOp, NoSuchObjectError # type: ignore
|
from phi.async_ldap import mixins
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def get_class(obj):
|
def parse_dn(dn):
|
||||||
"""
|
return MultiDict(e.split("=") for e in dn.split(","))
|
||||||
Return the input if input is a class, else tryes to get the class from the
|
|
||||||
`__class__` method.
|
|
||||||
"""
|
def get_dn(obj):
|
||||||
if type(obj) is type:
|
if isinstance(obj, mixins.Entry):
|
||||||
|
return obj.dn
|
||||||
|
elif isinstance(obj, LDAPEntry):
|
||||||
|
return obj["dn"]
|
||||||
|
elif isinstance(obj, str):
|
||||||
return obj
|
return obj
|
||||||
return obj.__class__
|
else:
|
||||||
|
raise ValueError(f"Unacceptable input: {obj}")
|
||||||
|
|
||||||
|
|
||||||
def recall(cls):
|
class User(mixins.Member, mixins.Entry, mixins.Singleton):
|
||||||
"""
|
object_class = [
|
||||||
Prints the name of the parent class.
|
"inetOrgPerson",
|
||||||
"""
|
"simpleSecurityObject",
|
||||||
_cls = get_class(cls)
|
"organizationalPerson",
|
||||||
return _cls.__bases__[0]
|
"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):
|
async def groups(self):
|
||||||
"""
|
return [g async for g in self.iter_groups()]
|
||||||
Tell between methods and properties. Call if method.
|
|
||||||
"""
|
|
||||||
_attr = getattr(cls, attr)
|
|
||||||
if "__call__" in dir(_attr):
|
|
||||||
return _attr()
|
|
||||||
return _attr
|
|
||||||
|
|
||||||
|
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):
|
async def iter_groups(self): # To be monkeypatched later
|
||||||
return [child async for child in children]
|
pass # pragma: no cover
|
||||||
|
|
||||||
|
async def groups(self):
|
||||||
|
return [g async for g in self.iter_groups()]
|
||||||
|
|
||||||
class Entry(object):
|
async def delete(self):
|
||||||
"""
|
async for group in self.iter_groups():
|
||||||
LDAP Entry. Interface to LDAP.
|
await group.remove_member(self)
|
||||||
"""
|
await super().delete()
|
||||||
|
|
||||||
kind: T.Union[None, str] = None
|
|
||||||
_name: T.Union[None, str] = None
|
|
||||||
|
|
||||||
@classmethod
|
class Robots(mixins.OrganizationalUnit, mixins.Entry, mixins.Singleton):
|
||||||
def name(cls):
|
_instances = dict() # type: ignore
|
||||||
if "_name" in dir(cls) and cls._name is not None:
|
ou = "Robots"
|
||||||
return cls._name
|
child_class = Service
|
||||||
return cls.__name__
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def qualified_name(cls):
|
|
||||||
return "{}={}".format(cls.kind, cls.name())
|
|
||||||
|
|
||||||
def __init__(self, client):
|
class Group(mixins.Member, mixins.Entry, mixins.Singleton):
|
||||||
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()
|
|
||||||
object_class = ["groupOfNames", "top"]
|
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):
|
async def add_member(self, member):
|
||||||
return singletonize(cls, f"{name}-{id(client)}")
|
member_dn = get_dn(member)
|
||||||
|
self._entry["member"].append(member_dn)
|
||||||
|
await self.modify()
|
||||||
|
|
||||||
def __init__(self, client, name, *args, **kwargs):
|
async def remove_member(self, member):
|
||||||
super().__init__(client, *args, **kwargs)
|
new_group_members = [get_dn(m) async for m in self.get_members() if member != m]
|
||||||
self._name = name
|
if len(new_group_members) == 0:
|
||||||
self._entry = LDAPEntry(self.dn)
|
await self.delete()
|
||||||
|
self.empty = True
|
||||||
|
else:
|
||||||
|
self._entry["member"] = new_group_members
|
||||||
|
await self.modify()
|
||||||
|
|
||||||
def __repr__(self):
|
async def get_members(self):
|
||||||
return f"<{get_class(self).__name__}({self.name}) {self.dn}>"
|
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
|
class Congregations(mixins.OrganizationalUnit, mixins.Entry, mixins.Singleton):
|
||||||
def name(self):
|
_instances = dict() # type: ignore
|
||||||
return self._name
|
ou = "Congregations"
|
||||||
|
child_class = Group
|
||||||
|
|
||||||
@name.setter
|
|
||||||
def name(self, name):
|
# We define this async method here **after** `User`, `Service` and `Group` have been
|
||||||
raise RuntimeError("Name property is not modifiable.")
|
# 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
|
||||||
|
|
|
@ -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
|
|
|
@ -3,7 +3,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
from pprint import pprint as pp
|
from pprint import pprint as pp
|
||||||
|
|
||||||
from phi.async_ldap.new_model import (
|
from phi.async_ldap.model import (
|
||||||
Hackers,
|
Hackers,
|
||||||
User,
|
User,
|
||||||
Robots,
|
Robots,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user