Massive code update

This commit is contained in:
sfigato 2020-11-11 22:25:02 +01:00
parent d024232d66
commit ac0ab02d6e
Signed by: blallo
GPG Key ID: 0CBE577C9B72DC3F
8 changed files with 479 additions and 70 deletions

View File

@ -30,7 +30,6 @@ from phi.async_ldap.model import (
)
from phi.security import hash_pass
BASE_DN = "dc=test,dc=abbiamoundominio,dc=org"
USER_LIST = [{"uid": ["conte_mascetti"]}, {"uid": ["perozzi"]}, {"uid": ["necchi"]}]
SERVICE_LIST = [{"uid": ["phi"]}, {"uid": ["irc"]}]

View File

@ -0,0 +1,96 @@
# -*- 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
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()
def connect_called_with_search(self):
self.conn.search.assert_called()
def connect_called_with_add(self):
self.conn.add.assert_called()
def connect_called_with_modify(self):
self.conn.modify.assert_called()
def connect_called_with_delete(self):
self.conn.delete.assert_called()
@property
def base_dn(self):
return BASE_DN
@asynccontextmanager
async def connect(self, *args, **kwargs):
self.connect_called = True
async def _search(*a, **kw):
return self.return_value
async def _add(*a, **kw):
return self.return_value
async def _modify(*a, **kw):
return self.return_value
async def _delete(*a, **kw):
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)
@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
_cl.connect_called_with_add()

View File

@ -11,31 +11,31 @@ objectClass: organizationalUnit
objectClass: top
ou: Hackers
dn: ou=Services,dc=unit,dc=macaomilano,dc=org
dn: ou=Robots,dc=unit,dc=macaomilano,dc=org
objectClass: top
objectClass: organizationalUnit
ou: Services
ou: Robots
dn: ou=Groups,dc=unit,dc=macaomilano,dc=org
dn: ou=Congregations,dc=unit,dc=macaomilano,dc=org
objectClass: top
objectClass: organizationalUnit
ou: Groups
ou: Congregations
dn: uid=phi,ou=Services,dc=unit,dc=macaomilano,dc=org
dn: uid=phi,ou=Robots,dc=unit,dc=macaomilano,dc=org
objectClass: account
objectClass: simpleSecurityObject
objectClass: top
uid: phi
userPassword: {SHA}REu9CtcqSaA1c5J+sEYlTgg0H+M=
dn: uid=irc,ou=Services,dc=unit,dc=macaomilano,dc=org
dn: uid=irc,ou=Robots,dc=unit,dc=macaomilano,dc=org
objectClass: account
objectClass: simpleSecurityObject
objectClass: top
uid: irc
userPassword: {SHA}0WvxFW9MSsesf55SOh4vnuwdkgY=
dn: uid=git,ou=Services,dc=unit,dc=macaomilano,dc=org
dn: uid=git,ou=Robots,dc=unit,dc=macaomilano,dc=org
objectClass: account
objectClass: simpleSecurityObject
objectClass: top
@ -47,9 +47,9 @@ objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
memberOf: cn=Admins,ou=Groups,dc=unit,dc=macaomilano,dc=org
memberOf: cn=GitUsers,ou=Groups,dc=unit,dc=macaomilano,dc=org
memberOf: cn=IRCUsers,ou=Groups,dc=unit,dc=macaomilano,dc=org
memberOf: cn=Admins,ou=Congregations,dc=unit,dc=macaomilano,dc=org
memberOf: cn=GitUsers,ou=Congregations,dc=unit,dc=macaomilano,dc=org
memberOf: cn=IRCUsers,ou=Congregations,dc=unit,dc=macaomilano,dc=org
cn: Raffaello
sn: Mascetti
mail: rmascetti@autistici.org
@ -61,8 +61,8 @@ objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
memberOf: cn=GitUsers,ou=Groups,dc=unit,dc=macaomilano,dc=org
memberOf: cn=IRCUsers,ou=Groups,dc=unit,dc=macaomilano,dc=org
memberOf: cn=GitUsers,ou=Congregations,dc=unit,dc=macaomilano,dc=org
memberOf: cn=IRCUsers,ou=Congregations,dc=unit,dc=macaomilano,dc=org
cn: Guido
sn: Necchi
mail: gnecchi@autistici.org
@ -74,20 +74,20 @@ objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
memberOf: cn=GitUsers,ou=Groups,dc=unit,dc=macaomilano,dc=org
memberOf: cn=GitUsers,ou=Congregations,dc=unit,dc=macaomilano,dc=org
cn: Giorgio
sn: Perozzi
mail: gperozzi@autistici.org
uid: perozzi
userPassword: {SHA}0+CRQKqsTj1I82PHxvZ4ebbddXQ=
dn: cn=Admins,ou=Groups,dc=unit,dc=macaomilano,dc=org
dn: cn=Admins,ou=Congregations,dc=unit,dc=macaomilano,dc=org
member: uid=conte_mascetti,ou=Hackers,dc=unit,dc=macaomilano,dc=org
cn: Admins
objectClass: groupOfNames
objectClass: top
dn: cn=GitUsers,ou=Groups,dc=unit,dc=macaomilano,dc=org
dn: cn=GitUsers,ou=Congregations,dc=unit,dc=macaomilano,dc=org
member: uid=conte_mascetti,ou=Hackers,dc=unit,dc=macaomilano,dc=org
member: uid=necchi,ou=Hackers,dc=unit,dc=macaomilano,dc=org
member: uid=perozzi,ou=Hackers,dc=unit,dc=macaomilano,dc=org
@ -95,7 +95,7 @@ cn: GitUsers
objectClass: groupOfNames
objectClass: top
dn: cn=IRCUsers,ou=Groups,dc=unit,dc=macaomilano,dc=org
dn: cn=IRCUsers,ou=Congregations,dc=unit,dc=macaomilano,dc=org
member: uid=conte_mascetti,ou=Hackers,dc=unit,dc=macaomilano,dc=org
member: uid=necchi,ou=Hackers,dc=unit,dc=macaomilano,dc=org
cn: IRCUsers

View File

@ -1,5 +1,7 @@
# -*- encoding: utf-8 -*-
from bonsai import LDAPEntry, LDAPModOp, NoSuchObjectError # type: ignore
from bonsai.ldapvaluelist import LDAPValueList
import bonsai.errors
from phi.exceptions import (
PhiAttributeMissing,
@ -7,10 +9,10 @@ from phi.exceptions import (
PhiEntryExists,
PhiUnauthorized,
PhiUnexpectedRuntimeValue,
PhiCannotExecute,
)
ATTR_TAG = "_ldap_"
ATTR_TAG_LEN = len(ATTR_TAG)
from phi.security import hash_pass, handle_password
async def build_heritage(obj, child_class, attribute_id="uid"):
@ -21,18 +23,20 @@ async def build_heritage(obj, child_class, attribute_id="uid"):
async for child in obj.get_children():
if attribute_id in child:
_name = child[attribute_id][0]
yield child_class(_name, obj.client)
yield child_class(obj.client, _name)
class Singleton(object):
"""
to singletonize a class. The class is crafted to be used with the mixins
Mixin to singletonize a class. The class is crafted to be used with the mixins
that implement the compatible __init__.
"""
def __new__(cls, client, *args):
if "name" in args:
def __new__(cls, client, *args, **kwargs):
if "name" in kwargs:
name = f"{cls.__name__}-{args['name']}-{id(client)}"
elif args:
name = f"{cls.__name__}-{args[0]}-{id(client)}"
else:
name = f"{cls.__name__}-{id(client)}"
if name not in cls._instances:
@ -40,13 +44,26 @@ class Singleton(object):
return cls._instances[name]
def get_value(obj, attr):
"""
Return the tuple (attribute_name, attribute_value) from obj. Extract the value,
either it being a constant or the result of a function call.
"""
if not isinstance(getattr(type(obj), attr), property) and callable(
getattr(type(obj), attr)
):
return attr, getattr(obj, attr)()
else:
return attr, getattr(obj, attr)
class Entry(object):
"""
to interact with LDAP.
Mixin to interact with LDAP.
"""
def get_all_ldap_attributes(self):
return [attr for attr in dir(self) if attr.startswith(ATTR_TAG)]
return [get_value(self, attr) for attr in self.ldap_attributes]
def __repr__(self):
return f"<{self.__class__.__name__} {self.dn}>"
@ -55,24 +72,17 @@ class Entry(object):
return f"<{self.__class__.__name__} {self.dn}>"
def __iter__(self):
for attr in self.get_all_ldap_attributes():
if not isinstance(getattr(type(self), attr), property) and callable(
getattr(type(self), attr)
):
yield attr[ATTR_TAG_LEN:], getattr(self, attr)()
else:
yield attr[ATTR_TAG_LEN:], getattr(self, attr)
for k, v in self.get_all_ldap_attributes():
yield k, v
def __dict__(self):
return dict(self)
async def _create_new(self):
entry = LDAPEntry(self.dn)
entry["objectClass"] = self.object_class
entry.update(self.get_all_ldap_attributes())
self._entry["objectClass"] = self.object_class
async with self.client.connect(is_async=True) as conn:
await conn.add(entry)
return entry
await conn.add(self._entry)
return self._entry
async def _get(self):
async with self.client.connect(is_async=True) as conn:
@ -80,27 +90,50 @@ class Entry(object):
# the one we are interested.
_res = await conn.search(self.dn, 0)
if len(_res) == 0:
return
raise PhiEntryDoesNotExist()
elif len(_res) > 1:
raise PhiUnexpectedRuntimeValue(
"return value should be no more than one", res
)
return _res[0]
async def _modify(self):
_ = await self._get()
async with self.client.connect(is_async=True) as conn:
self._entry.connection = conn
await self._entry.modify()
async def _delete(self):
async with self.client.connect(is_async=True) as conn:
await conn.delete(self.dn)
async def describe(self):
res = dict(await self._get())
res["dn"] = self.dn
res["name"] = self.name
return res
return dict(await self._get())
@property
def delete_cascade(self):
if hasattr(self, "_delete_cascade"):
return self._delete_cascade
return False
@delete_cascade.setter
def delete_cascade(self, value):
if not isinstance(value, bool):
raise ValueError("delete_cascade must be a boolean")
self._delete_cascade = value
class OrganizationalUnit(object):
def __init__(self, client):
object_class = ["organizationalUnit", "top"]
def __init__(self, client, **kwargs):
self.client = client
self.base_dn = client.base_dn
self.name = self.__class__.__name__
self.children = build_heritage(self, getattr(self, "child_class"))
self.children = build_heritage(self, self.child_class, self.child_class.id_tag)
self._entry = LDAPEntry(self.dn)
for k, v in kwargs.items():
self._entry[k] = v
def __aiter__(self):
return self
@ -109,28 +142,117 @@ class OrganizationalUnit(object):
try:
return await self.children.__anext__()
except StopAsyncIteration:
self.children = build_heritage(self, getattr(self, "child_class"))
self.children = build_heritage(
self, self.child_class, self.child_class.id_tag
)
raise
async def get_children(self):
async with self.client.connect(is_async=True) as conn:
for el in await conn.search(self.dn, 2):
for el in await conn.search(self.dn, 1):
yield el
@property
def dn(self):
return f"ou={self.ou_name},{self.base_dn}"
return f"ou={self.ou},{self.base_dn}"
async def save(self):
try:
await self._create_new()
except bonsai.errors.AlreadyExists:
raise PhiEntryExists(self.dn)
async def search(self, member_name):
result = None
async with self.client.connect(is_async=True) as conn:
result = await conn.search(
f"{self.child_class.id_tag}={member_name},{self.dn}", 0
)
if not result:
raise PhiEntryDoesNotExist(
f"{self.child_class.id_tag}={member_name},{self.dn}"
)
if isinstance(result, list) and len(result) > 1:
raise PhiUnexpectedRuntimeValue(
"return value should be no more than one", result
)
return self.child_class(self.client, member_name, **result[0])
async def delete(self):
"""
Delete all the members of this OU only if `delete_cascade` is set to `True`,
raises otherwise.
"""
if self.delete_cascade:
async for member in self:
await member.delete()
else:
raise PhiCannotExecute("Cannot delete an OU and delete_cascade is not set")
class Child(object):
object_class = ["inetOrgPerson", "organizationalPerson", "person", "top"]
def hydrate(obj, data):
for k, v in data.items():
if k in obj.ldap_attributes:
if isinstance(v, list) and not isinstance(v, LDAPValueList):
obj._entry[k] = []
for _v in v:
obj._entry[k].append(_v.dn)
elif k == "userPassword":
obj._entry[k] = handle_password(v)
else:
obj._entry[k] = v
def __init__(self, client, name):
class Member(object):
def __init__(self, client, name, **kwargs):
self.client = client
self.base_dn = client.base_dn
self.name = name
self._entry = LDAPEntry(self.dn)
if kwargs:
hydrate(self, kwargs)
def __eq__(self, other):
if isinstance(other, type(self)):
return other.dn == self.dn
elif isinstance(other, str):
return other == self.dn
elif isinstance(other, LDAPEntry):
return str(other) == self.dn
else:
return False
@property
def dn(self):
return f"{self.id_tag}={self.name},ou={self.ou_name},{self.base_dn}"
return f"{self.id_tag}={self.name},ou={self.ou},{self.base_dn}"
def __setitem__(self, attr, val):
if attr not in self.ldap_attributes:
raise PhiCannotExecute(
f"{attr} is not an allowed ldap_attribute: {self.ldap_attributes}"
)
self._entry[attr] = val
def __getitem__(self, attr):
if attr not in self.ldap_attributes:
raise PhiCannotExecute(
f"{attr} is not an allowed ldap_attribute: {self.ldap_attributes}"
)
return self._entry[attr][0]
async def save(self):
try:
await self._create_new()
except bonsai.errors.AlreadyExists:
raise PhiEntryExists(self.dn)
async def modify(self):
await self._modify()
async def delete(self):
await self._delete()
async def sync(self):
res = await self._get()
hydrate(self, res)
return self

View File

@ -1,15 +1,79 @@
# -*- encoding: utf-8 -*-
from bonsai import LDAPEntry
from multidict import MultiDict
from phi.async_ldap import mixins
class User(mixins.Singleton, mixins.Entry, mixins.Child):
_instances = dict()
class User(mixins.Singleton, mixins.Entry, mixins.Member):
object_class = [
"inetOrgPerson",
"simpleSecurityObject",
"organizationalPerson",
"person",
"top",
]
_instances = dict() # type: ignore
id_tag = "uid"
ou_name = "Hackers"
ou = "Hackers"
ldap_attributes = ["uid", "ou", "cn", "sn", "mail", "userPassword"]
class Hackers(mixins.Singleton, mixins.Entry, mixins.OrganizationalUnit):
_instances = dict()
ou_name = "Hackers"
_instances = dict() # type: ignore
ou = "Hackers"
child_class = User
class Service(mixins.Singleton, mixins.Entry, mixins.Member):
object_class = ["simpleSecurityObject", "account", "top"]
_instances = dict() # type: ignore
id_tag = "uid"
ou = "Robots"
ldap_attributes = ["uid", "ou", "userPassword"]
class Robots(mixins.Singleton, mixins.Entry, mixins.OrganizationalUnit):
_instances = dict() # type: ignore
ou = "Robots"
child_class = Service
class Group(mixins.Singleton, mixins.Entry, mixins.Member):
object_class = ["groupOfNames", "top"]
_instances = dict() # type: ignore
id_tag = "cn"
ou = "Congregations"
ldap_attributes = ["uid", "ou", "member"]
memeber_classes = {"Hackers": User, "Robots": Service}
async def add_member(self, member):
self._entry["member"].append(get_dn(member))
await self.modify()
async def remove_member(self, member):
self._entry["member"] = [get_dn(m) for m in self.get_members() if member != m]
await self.modify()
def get_members(self):
for member in self._entry.get("member", []):
dn = MultiDict(e.split("=") for e in member.split(","))
yield self.memeber_classes.get(dn["ou"])(self.client, dn["uid"])
class Congregations(mixins.Singleton, mixins.Entry, mixins.OrganizationalUnit):
_instances = dict() # type: ignore
ou = "Congregations"
child_class = Group
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}")

View File

@ -5,11 +5,23 @@ class PhiEntryExists(Exception):
def __init__(self, dn):
self.dn = dn
def __str__(self):
return f"Entry exists yet ({self.dn})"
def __repr__(self):
return f"PhiEntryExists({self.dn})"
class PhiEntryDoesNotExist(Exception):
def __init__(self, dn):
self.dn = dn
def __str__(self):
return f"Entry does not exist ({self.dn})"
def __repr__(self):
return f"PhiEntryDoesNotExist({self.dn})"
class PhiAttributeMissing(Exception):
def __init__(self, dn, attr):
@ -28,3 +40,15 @@ class PhiUnexpectedRuntimeValue(RuntimeWarning):
super().__init__(self)
self.msg = msg
self.result = result
class PhiCannotExecute(RuntimeWarning):
def __init__(self, msg):
super().__init__(self)
self.msg = msg
def __str__(self):
return f"Cannot execute: {self.msg}"
def __repr__(self):
return f"PhiCannotExecute({self.msg})"

View File

@ -1,4 +1,5 @@
# -*- encoding: utf-8 -*-
from bonsai.ldapvaluelist import LDAPValueList
from passlib.hash import (
ldap_sha1,
ldap_bcrypt,
@ -8,7 +9,6 @@ from passlib.hash import (
ldap_pbkdf2_sha512,
)
HASH_CALLABLE = {
"sha1": ldap_sha1.hash,
"bcrypt": ldap_bcrypt.hash,
@ -21,3 +21,9 @@ HASH_CALLABLE = {
def hash_pass(password, method="sha1"):
return HASH_CALLABLE[method](password)
def handle_password(password, method="sha1"):
if isinstance(password, LDAPValueList):
return password
return hash_pass(password, method)

View File

@ -3,19 +3,17 @@
import asyncio
from pprint import pprint as pp
from phi.async_ldap.model import (
# Hackers,
# User,
from phi.async_ldap.new_model import (
Hackers,
User,
Robots,
Service,
Congregations,
Group,
build_heritage,
iter_children,
Congregations,
)
from phi.async_ldap.new_model import Hackers, User
from phi.async_ldap.mixins import build_heritage
from phi.async_ldap.client import AsyncClient
import phi.exceptions as e
async def dlv(h, cls):
@ -47,15 +45,115 @@ async def get_all_children():
return (hackers, robots, groups)
async def print_async(awaitable):
async def get_members(group):
return [el async for el in group]
async def print_async(label, awaitable):
print(label)
result = await awaitable
pp(result)
async def describe(obj):
results = await obj.describe()
return results
return await obj.describe()
asyncio.run(print_async(describe(Hackers(cl))))
asyncio.run(print_async(describe(User(cl, "conte_mascetti"))))
async def _await(awaitable):
return await awaitable
def sync_await(awaitable):
return asyncio.run(_await(awaitable))
h = Hackers(cl)
r = Robots(cl)
c = Congregations(cl)
# asyncio.run(print_async("hackers:", describe(h)))
# asyncio.run(print_async("conte_mascetti:", describe(User(cl, "conte_mascetti"))))
# asyncio.run(print_async("robots:", describe(r)))
# asyncio.run(print_async("phi:", describe(Service(cl, "phi"))))
# asyncio.run(print_async("congregations:", describe(c)))
# asyncio.run(print_async("GitUsers:", describe(Group(cl, "GitUsers"))))
# asyncio.run(print_async("Hackers members:", get_members(h)))
# asyncio.run(print_async("Robots members:", get_members(r)))
# asyncio.run(print_async("Congregations members:", get_members(c)))
#
async def add_new(obj, name, **kw):
try:
_new = obj(cl, name, **kw)
await _new.save()
except e.PhiEntryExists as err:
print(f"Failed add: {repr(err)}")
async def safe_search(group, name):
try:
res = await group.search(name)
print("Search result:", res)
return res
except e.PhiEntryDoesNotExist as err:
print(f"Failed search: {repr(err)}")
async def safe_delete(obj, cascade=None):
try:
if cascade:
obj.delete_cascade = cascade
await obj.delete()
except Exception as err:
print(f"Failed delete: {repr(err)}")
async def add_member(group, member):
await group.add_member(member)
async def remove_member(group, member):
await group.remove_member(member)
# asyncio.run(safe_search(h, "pippo"))
# asyncio.run(
# add_new(User, "pippo", cn="Pippo (Goofy)", sn="Pippo", mail="pippo@unit.info")
# )
# asyncio.run(safe_search(h, "pippo"))
# asyncio.run(
# add_new(User, "pippo", cn="Pippo (Goofy)", sn="Pippo", mail="pippo@unit.net")
# )
# asyncio.run(safe_delete(asyncio.run(safe_search(h, "pippo"))))
# asyncio.run(print_async("Hackers members:", get_members(h)))
# asyncio.run(safe_delete(h))
# asyncio.run(print_async("Hackers members:", get_members(h)))
# asyncio.run(safe_delete(h, True))
# asyncio.run(print_async("Hackers members:", get_members(h)))
# asyncio.run(safe_search(r, "phi"))
# asyncio.run(print_async("Robots members:", get_members(r)))
# asyncio.run(add_new(Service, "db", userPassword="lolpassword"))
# asyncio.run(print_async("Robots members:", get_members(r)))
asyncio.run(safe_search(c, "GitUsers"))
asyncio.run(print_async("Congregations members:", get_members(c)))
asyncio.run(
add_new(Group, "naughty", member=[User(cl, "conte_mascetti"), User(cl, "necchi")])
)
asyncio.run(print_async("Congregations members:", get_members(c)))
asyncio.run(safe_delete(Group(cl, "naughty")))
asyncio.run(print_async("Congregations members:", get_members(c)))
asyncio.run(
add_new(Group, "naughty", member=[User(cl, "conte_mascetti"), User(cl, "necchi")])
)
asyncio.run(print_async("Congregations members:", get_members(c)))
print("==> HERE <==")
naughty = sync_await(Group(cl, "naughty").sync())
print("NAUGHTY =>>", [m for m in naughty.get_members()])
asyncio.run(add_member(naughty, User(cl, "perozzi")))
print("NAUGHTY =>>", [m for m in naughty.get_members()])
asyncio.run(remove_member(naughty, User(cl, "conte_mascetti")))
print("NAUGHTY =>>", [m for m in naughty.get_members()])