Compare commits
83 Commits
master
...
feat/init_
Author | SHA1 | Date | |
---|---|---|---|
b376d1f270 | |||
fa7b633a16 | |||
d51acc6999 | |||
d414634f42 | |||
a222fe9560 | |||
7224cd73f2 | |||
9fdef6f2e5 | |||
593255e853 | |||
ef08fe5997 | |||
714f3221f9 | |||
7201d9a529 | |||
297b31ab90 | |||
b704ea4d10 | |||
b0d2f759ea | |||
d6aac65bba | |||
c0d835d8d1 | |||
2932957afb | |||
077c53c5be | |||
e6cee09429 | |||
c5a4b86349 | |||
b0e5d00994 | |||
401f01f110 | |||
30c46c059a | |||
b400127cc6 | |||
6bd1beba9e | |||
e202d54a7d | |||
67c7830ba4 | |||
95bc52ebd8 | |||
ac0ab02d6e | |||
d024232d66 | |||
166b542846 | |||
d87c89a075 | |||
b7316ff513 | |||
67c83975d1 | |||
205c87dc49 | |||
c05b023bdb | |||
1b97d6d7ca | |||
b979002f78 | |||
e41e03f464 | |||
d9a6db63d7 | |||
6fea75022f | |||
12f37b3a55 | |||
a0cf7e9603 | |||
8779db9ca0 | |||
e454fbd84a | |||
66885641c4 | |||
a22f459915 | |||
08c45b54f2 | |||
2bc7f0b75f | |||
d9e8eb23e3 | |||
ce4c085e3f | |||
7634f0f530 | |||
79f682cbb7 | |||
2b46eb0353 | |||
fed48022af | |||
3454c194b1 | |||
f05fe8a0d5 | |||
ec472218c4 | |||
79d7dcc653 | |||
fe3450b886 | |||
f558492975 | |||
706f109faf | |||
0f7882a387 | |||
1be1aac9d0 | |||
7e6b757e3a | |||
ed8af40392 | |||
2cf07d6732 | |||
8dce6566ee | |||
b466bf8ed2 | |||
d0cba75ee0 | |||
fd729170d3 | |||
c4046a83ff | |||
4e2cadaa92 | |||
b1837f80e4 | |||
27fc927254 | |||
a434ff9b4c | |||
422d238fc1 | |||
b0f312284d | |||
543416368b | |||
bdd4a39531 | |||
e67b97b214 | |||
c8eb5c2dd4 | |||
80fb51f7de |
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -90,6 +90,9 @@ ENV/
|
|||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Personal local venv standard ("*" matches the version number)
|
||||
/cpy*
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
|
27
Pipfile
27
Pipfile
|
@ -4,9 +4,30 @@ url = "https://pypi.org/simple"
|
|||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
ipython = "*"
|
||||
pytest = "*"
|
||||
ipdb = "*"
|
||||
async-generator = "*"
|
||||
pytest-cov = "*"
|
||||
pytest-asyncio = "*"
|
||||
pytest-aiohttp = "*"
|
||||
mock = "*"
|
||||
yarl = {editable = true, path = "."}
|
||||
pytest-integration = "*"
|
||||
iniconfig = "*"
|
||||
|
||||
[packages]
|
||||
phi = {editable = true,path = "."}
|
||||
phi = {editable = true, path = "."}
|
||||
aiohttp = "==3.8.1"
|
||||
click = "==8"
|
||||
pyYAML = "*"
|
||||
bonsai = "==1.3.0"
|
||||
passlib = "==1.7.1"
|
||||
bcrypt = "==3.2.0"
|
||||
multidict = "*"
|
||||
iniconfig = "*"
|
||||
aiohttp-session = {version = "==2.11.0", extras = ["secure"]}
|
||||
jinja2 = "==3"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
[pipenv]
|
||||
allow_prereleases = true
|
||||
|
|
1637
Pipfile.lock
generated
1637
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
49
README.md
49
README.md
|
@ -8,51 +8,4 @@ APIs for the Unit hacklab.
|
|||
|
||||
Requirements:
|
||||
|
||||
* Python >= 3.5
|
||||
|
||||
|
||||
Create a virtual environment and activate it (optional):
|
||||
```
|
||||
virtualenv --python=/usr/bin/python3 env
|
||||
source env/bin/activate
|
||||
```
|
||||
|
||||
Run the setup:
|
||||
```
|
||||
python setup.py install
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
In the ldap section of `config.yml` change host, port and password according to
|
||||
your setup.
|
||||
|
||||
|
||||
## Command Line
|
||||
|
||||
```
|
||||
usage: phicli [-h] [--config config.yml]
|
||||
{showuser,adduser,deluser,showgroup,listgroups,addtogroup} ...
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--config config.yml custom configuration file
|
||||
|
||||
actions:
|
||||
showuser dispaly user fields
|
||||
adduser add a new user
|
||||
deluser delete an user
|
||||
showgroup show a group
|
||||
listgroups list all groups
|
||||
addtogroup add an user to a group
|
||||
```
|
||||
|
||||
```
|
||||
phicli showuser [-h] user_id
|
||||
phicli adduser [-h] user_id
|
||||
phicli deluser [-h] user_id
|
||||
|
||||
phicli showgroup [-h] common_name
|
||||
phicli listgroups [-h]
|
||||
phicli addtogroup [-h] user_id group_common_name
|
||||
```
|
||||
* Python >= 3.9
|
||||
|
|
117
async_tests/test_async_ldap_client.py
Normal file
117
async_tests/test_async_ldap_client.py
Normal file
|
@ -0,0 +1,117 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from contextlib import contextmanager
|
||||
import logging
|
||||
|
||||
import mock
|
||||
import pytest
|
||||
|
||||
from phi.async_ldap.client import (
|
||||
parse_host,
|
||||
checked_port,
|
||||
compose_dn_username,
|
||||
AsyncClient,
|
||||
)
|
||||
|
||||
BASE_DN = "dc=unit,dc=macaomilano,dc=org"
|
||||
|
||||
|
||||
@contextmanager
|
||||
def does_not_raise():
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_url, exp_proto, exp_addr, exp_port",
|
||||
[
|
||||
("1.3.1.2", "ldap", "1.3.1.2", 389),
|
||||
("ldap://localhost:1312", "ldap", "localhost", 1312),
|
||||
("localhost:1312", "ldap", "localhost", 1312),
|
||||
("localhost", "ldap", "localhost", 389),
|
||||
("ldap://localhost", "ldap", "localhost", 389),
|
||||
("ldaps://localhost", "ldaps", "localhost", 636),
|
||||
("ldaps://localhost:1312", "ldaps", "localhost", 1312),
|
||||
],
|
||||
)
|
||||
def test_parse_host(test_url, exp_proto, exp_addr, exp_port):
|
||||
proto, addr, port = parse_host(test_url)
|
||||
|
||||
assert proto == exp_proto
|
||||
assert addr == exp_addr
|
||||
assert port == exp_port
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"manual, auto, exp_port", [(None, 389, 389), (1312, 389, 1312), (1312, 1312, 1312)]
|
||||
)
|
||||
def test_checked_port(manual, auto, exp_port, caplog):
|
||||
port = checked_port(manual, auto)
|
||||
if manual and manual != auto:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
"The former prevails" in caplog.text
|
||||
|
||||
assert port == exp_port
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"username, base_dn, ou, attribute_id, exp_dn",
|
||||
[
|
||||
(
|
||||
f"uid=conte_mascetti,{BASE_DN}",
|
||||
BASE_DN,
|
||||
None,
|
||||
"uid",
|
||||
f"uid=conte_mascetti,{BASE_DN}",
|
||||
),
|
||||
("root", BASE_DN, None, "cn", f"cn=root,{BASE_DN}"),
|
||||
("necchi", BASE_DN, "Hackers", "uid", f"uid=necchi,ou=Hackers,{BASE_DN}"),
|
||||
("perozzi", BASE_DN, "Phrackers", "cn", f"cn=perozzi,ou=Phrackers,{BASE_DN}"),
|
||||
],
|
||||
)
|
||||
def test_compose_dn_username(username, base_dn, ou, attribute_id, exp_dn):
|
||||
dn = compose_dn_username(username, base_dn, ou, attribute_id)
|
||||
|
||||
assert dn == exp_dn
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url, encryption, validate, ca_cert, expectation",
|
||||
[
|
||||
("localhost", None, False, None, does_not_raise()),
|
||||
("localhost", True, False, None, does_not_raise()),
|
||||
("localhost", False, True, None, does_not_raise()),
|
||||
("localhost", True, True, "path/to/cert.pem", does_not_raise()),
|
||||
("ldaps://localhost", False, False, None, pytest.raises(ValueError)),
|
||||
],
|
||||
)
|
||||
def test_AsyncClient_init(url, encryption, validate, ca_cert, expectation):
|
||||
with expectation as exp:
|
||||
cl = AsyncClient(
|
||||
host=url,
|
||||
port=389,
|
||||
encryption=encryption,
|
||||
ciphers=None,
|
||||
validate=validate,
|
||||
ca_cert=ca_cert,
|
||||
username="conte_mascetti",
|
||||
password="pass",
|
||||
base_dn=BASE_DN,
|
||||
ou="Hackers",
|
||||
)
|
||||
|
||||
if exp is not None:
|
||||
assert "Incompatible provided protocol" in str(exp.value)
|
||||
return
|
||||
|
||||
assert cl.base_dn == BASE_DN
|
||||
assert url in cl.full_uri
|
||||
assert "389" in cl.full_uri
|
||||
assert cl._tls if encryption else not cl._tls
|
||||
if validate:
|
||||
assert cl.cert_policy == -1
|
||||
else:
|
||||
assert cl.cert_policy == 0
|
||||
if ca_cert:
|
||||
assert cl.ca_cert == ca_cert
|
||||
else:
|
||||
assert cl.ca_cert == ""
|
196
async_tests/test_async_ldap_model.py
Normal file
196
async_tests/test_async_ldap_model.py
Normal file
|
@ -0,0 +1,196 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
import asyncio
|
||||
|
||||
from async_generator import asynccontextmanager
|
||||
from bonsai import LDAPEntry
|
||||
import mock
|
||||
import pytest
|
||||
|
||||
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=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)
|
21
config.yml
21
config.yml
|
@ -3,6 +3,8 @@ core:
|
|||
listen:
|
||||
host: 127.0.0.1
|
||||
port: 8080
|
||||
# generated with: openssl rand -hex 16
|
||||
cookiestore_secret: "e41133b5cfdd8660815b8d5cc2c74843"
|
||||
|
||||
|
||||
ldap:
|
||||
|
@ -11,13 +13,13 @@ ldap:
|
|||
|
||||
encryption: TLSv1.2 # Can either be None or TLSv1.2. Default: None
|
||||
ciphers: "HIGH"
|
||||
validate: True # Can either be True or False. Default: False
|
||||
validate: False # Can either be True or False. Default: False
|
||||
ca_certs: openldap/cert.pem
|
||||
|
||||
# username: uid=phi,ou=Services,dc=unit,dc=macaomilano,dc=org
|
||||
# password: phi
|
||||
username: cn=root,dc=unit,dc=macaomilano,dc=org
|
||||
password: root
|
||||
# username: cn=root,dc=unit,dc=macaomilano,dc=org
|
||||
# password: root
|
||||
|
||||
base_dn: dc=unit,dc=macaomilano,dc=org
|
||||
|
||||
|
@ -40,11 +42,16 @@ logging:
|
|||
|
||||
loggers:
|
||||
phi:
|
||||
level: WARNING
|
||||
level: DEBUG
|
||||
handlers: [console, file]
|
||||
asyncio:
|
||||
level: DEBUG
|
||||
handlers: [console, file]
|
||||
propagate: yes
|
||||
aiohttp:
|
||||
level: WARNING
|
||||
level: DEBUG
|
||||
handlers: [console, file]
|
||||
ldap3:
|
||||
level: WARNING
|
||||
propagate: yes
|
||||
bonsai:
|
||||
level: DEBUG
|
||||
handlers: [console, file]
|
||||
|
|
361
integration_tests/test_model.py
Normal file
361
integration_tests/test_model.py
Normal file
|
@ -0,0 +1,361 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import asyncio
|
||||
|
||||
from async_generator import asynccontextmanager
|
||||
import pytest
|
||||
|
||||
from phi.async_ldap.model import (
|
||||
Hackers,
|
||||
User,
|
||||
Robots,
|
||||
Service,
|
||||
Group,
|
||||
Roles,
|
||||
)
|
||||
from phi.async_ldap.mixins import build_heritage
|
||||
from phi.async_ldap.client import AsyncClient
|
||||
import phi.exceptions as e
|
||||
|
||||
BASE_DN = "dc=unit,dc=macaomilano,dc=org"
|
||||
|
||||
cl = AsyncClient(
|
||||
"ldap://localhost",
|
||||
port=389,
|
||||
encryption=True,
|
||||
# validate=True,
|
||||
ca_cert="../openldap/cert.pem",
|
||||
username="root",
|
||||
password="root",
|
||||
base_dn=BASE_DN,
|
||||
attribute_id="cn",
|
||||
)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def clean_db():
|
||||
h = Hackers(cl)
|
||||
r = Robots(cl)
|
||||
c = Roles(cl)
|
||||
h.delete_cascade = True
|
||||
r.delete_cascade = True
|
||||
c.delete_cascade = True
|
||||
await h.delete()
|
||||
await r.delete()
|
||||
await c.delete()
|
||||
yield
|
||||
await h.delete()
|
||||
await r.delete()
|
||||
await c.delete()
|
||||
|
||||
|
||||
async def init_achilles():
|
||||
u = User(cl, "achilles")
|
||||
u["cn"] = "Achilles"
|
||||
u["sn"] = "achilles"
|
||||
u["mail"] = "achilles@phthia.gr"
|
||||
u["userPassword"] = "Patroclus123"
|
||||
|
||||
await u.save()
|
||||
|
||||
return u
|
||||
|
||||
|
||||
async def init_patroclus():
|
||||
u = User(cl, "patroclus")
|
||||
u["cn"] = "Patroclus"
|
||||
u["sn"] = "patroclus"
|
||||
u["mail"] = "patroclus@phthia.gr"
|
||||
u["userPassword"] = "WannabeAnHero"
|
||||
|
||||
await u.save()
|
||||
|
||||
return u
|
||||
|
||||
|
||||
async def init_athena():
|
||||
s = Service(cl, "athena")
|
||||
s["userPassword"] = "ἁ θεονόα"
|
||||
|
||||
await s.save()
|
||||
|
||||
return s
|
||||
|
||||
|
||||
async def init_group(group_name, members):
|
||||
g = Group(cl, group_name, member=members)
|
||||
|
||||
await g.save()
|
||||
|
||||
return g
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration_test
|
||||
async def test_User_init():
|
||||
async with clean_db():
|
||||
u = await init_achilles()
|
||||
|
||||
h = Hackers(cl)
|
||||
|
||||
res = await h.search("achilles")
|
||||
|
||||
assert u == res
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration_test
|
||||
async def test_User_exists():
|
||||
async with clean_db():
|
||||
u1 = await init_achilles()
|
||||
u2 = User(cl, "enea")
|
||||
|
||||
assert await u1.exists()
|
||||
assert not await u2.exists()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration_test
|
||||
async def test_User_double_save_raises():
|
||||
async with clean_db():
|
||||
u = await init_achilles()
|
||||
# Read all the data from the db
|
||||
await u.sync()
|
||||
|
||||
with pytest.raises(e.PhiEntryExists) as ex:
|
||||
await u.save()
|
||||
|
||||
assert u.dn in str(ex.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration_test
|
||||
async def test_User_describe():
|
||||
async with clean_db():
|
||||
u = await init_achilles()
|
||||
|
||||
res = await u.describe()
|
||||
|
||||
assert res == {
|
||||
"uid": "achilles",
|
||||
"cn": "Achilles",
|
||||
"sn": "achilles",
|
||||
"dn": f"uid=achilles,ou=Hackers,{BASE_DN}",
|
||||
"mail": "achilles@phthia.gr",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration_test
|
||||
async def test_User_modify():
|
||||
async with clean_db():
|
||||
u = await init_achilles()
|
||||
NEW_EMAIL = "a@myrmidons.mil"
|
||||
u["mail"] = NEW_EMAIL
|
||||
await u.modify()
|
||||
|
||||
h = Hackers(cl)
|
||||
|
||||
res = await h.search("achilles")
|
||||
await u.sync()
|
||||
|
||||
assert u["mail"] == res["mail"] == NEW_EMAIL
|
||||
for attr in u.ldap_attributes:
|
||||
assert u[attr] == res[attr]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration_test
|
||||
async def test_User_modify_raises():
|
||||
"""Modifying a not-yet-existing user raises."""
|
||||
async with clean_db():
|
||||
u = User(cl, "enea")
|
||||
|
||||
with pytest.raises(e.PhiEntryDoesNotExist) as ex:
|
||||
await u.modify()
|
||||
|
||||
assert u.dn in str(ex.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration_test
|
||||
async def test_User_delete():
|
||||
async with clean_db():
|
||||
u = await init_achilles()
|
||||
await u.delete()
|
||||
|
||||
h = Hackers(cl)
|
||||
|
||||
with pytest.raises(e.PhiEntryDoesNotExist) as ex:
|
||||
await h.search("achilles")
|
||||
|
||||
assert u.dn in str(ex.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration_test
|
||||
async def test_Service_init():
|
||||
async with clean_db():
|
||||
s = await init_athena()
|
||||
|
||||
r = Robots(cl)
|
||||
|
||||
res = await r.search("athena")
|
||||
|
||||
assert s == res
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration_test
|
||||
async def test_Service_describe():
|
||||
async with clean_db():
|
||||
s = await init_athena()
|
||||
|
||||
res = await s.describe()
|
||||
|
||||
assert res == {
|
||||
"ou": "Robots",
|
||||
"uid": "athena",
|
||||
"dn": f"uid=athena,ou=Robots,{BASE_DN}",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration_test
|
||||
async def test_Group_init():
|
||||
async with clean_db():
|
||||
u = await init_achilles()
|
||||
g = await init_group("achaeans", [u])
|
||||
|
||||
c = Roles(cl)
|
||||
|
||||
res = await c.search("achaeans")
|
||||
|
||||
assert g == res
|
||||
assert [u] == [a async for a in g.get_members()]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration_test
|
||||
async def test_Group_describe():
|
||||
async with clean_db():
|
||||
u1 = await init_achilles()
|
||||
u2 = await init_patroclus()
|
||||
g = await init_group("achaeans", [u1, u2])
|
||||
|
||||
res = await g.describe()
|
||||
|
||||
assert res == {
|
||||
"ou": "Roles",
|
||||
"cn": "achaeans",
|
||||
"dn": f"cn=achaeans,ou=Roles,{BASE_DN}",
|
||||
"member": [u1.dn, u2.dn],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration_test
|
||||
async def test_Group_add_member():
|
||||
async with clean_db():
|
||||
u = await init_achilles()
|
||||
a = await init_athena()
|
||||
g1 = await init_group("achaeans", [u])
|
||||
g2 = await init_group("gods", [u])
|
||||
|
||||
await g2.add_member(a)
|
||||
|
||||
m1 = [m async for m in g1.get_members()]
|
||||
m2 = [m async for m in g2.get_members()]
|
||||
|
||||
assert u in m1
|
||||
assert u in m2
|
||||
assert a not in m1
|
||||
assert a in m2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration_test
|
||||
async def test_Group_remove_member():
|
||||
async with clean_db():
|
||||
u = await init_achilles()
|
||||
a = await init_athena()
|
||||
g = await init_group("achaeans", [u, a])
|
||||
|
||||
m = [a async for a in g.get_members()]
|
||||
|
||||
assert u in m
|
||||
assert a in m
|
||||
|
||||
await g.remove_member(a)
|
||||
|
||||
assert [u] == [el async for el in g.get_members()]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration_test
|
||||
async def test_User_groups():
|
||||
async with clean_db():
|
||||
u = await init_achilles()
|
||||
a = await init_athena()
|
||||
g1 = await init_group("achaeans", [u])
|
||||
g2 = await init_group("gods", [u, a])
|
||||
|
||||
res1 = await u.groups()
|
||||
res2 = await a.groups()
|
||||
|
||||
assert g1 in res1
|
||||
assert g2 in res1
|
||||
assert g1 not in res2
|
||||
assert g2 in res2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration_test
|
||||
async def test_OU_delete_raises():
|
||||
async with clean_db():
|
||||
u1 = await init_achilles()
|
||||
u2 = await init_patroclus()
|
||||
a = await init_athena()
|
||||
g1 = await init_group("achaeans", [u1, u2])
|
||||
g2 = await init_group("gods", [a, u1])
|
||||
h = Hackers(cl)
|
||||
_saved_val = h.delete_cascade
|
||||
h.delete_cascade = False
|
||||
|
||||
assert not h.delete_cascade
|
||||
|
||||
with pytest.raises(e.PhiCannotExecute) as ex:
|
||||
await h.delete()
|
||||
|
||||
assert "delete_cascade is not set" in str(ex.value)
|
||||
|
||||
h.delete_cascade = _saved_val
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration_test
|
||||
async def test_OU_delete_cascade():
|
||||
async with clean_db():
|
||||
u1 = await init_achilles()
|
||||
u2 = await init_patroclus()
|
||||
a = await init_athena()
|
||||
g1 = await init_group("achaeans", [u1, u2])
|
||||
g2 = await init_group("gods", [a, u1])
|
||||
h = Hackers(cl)
|
||||
_saved_val = h.delete_cascade
|
||||
h.delete_cascade = True
|
||||
|
||||
assert h.delete_cascade
|
||||
|
||||
await h.delete()
|
||||
g2_members = [e async for e in g2.get_members()]
|
||||
h_members = [e async for e in h]
|
||||
|
||||
assert not await u1.exists()
|
||||
assert not await u2.exists()
|
||||
assert h_members == []
|
||||
assert not await g1.exists()
|
||||
assert u1 not in g2_members
|
||||
assert a in g2_members
|
||||
|
||||
h.delete_cascade = _saved_val
|
|
@ -1,5 +1,7 @@
|
|||
FROM alpine:3.7
|
||||
|
||||
ENV LDAPTLS_REQCERT=never
|
||||
|
||||
RUN apk add --no-cache \
|
||||
openldap \
|
||||
openldap-back-mdb \
|
||||
|
|
|
@ -16,6 +16,8 @@ gen-cert:
|
|||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
docker rm $(CONTAINER) || true
|
||||
docker rmi unit/slapd
|
||||
rm -f key.pem cert.pem
|
||||
|
||||
.PHONY: run
|
||||
|
@ -35,6 +37,10 @@ prepare:
|
|||
run-bg:
|
||||
make prepare
|
||||
|
||||
.PHONY: logs
|
||||
logs:
|
||||
docker logs -f phi_slapd
|
||||
|
||||
.PHONY: stop
|
||||
stop: is-running
|
||||
docker stop $(CONTAINER)
|
||||
|
|
|
@ -11,36 +11,93 @@ 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=Roles,dc=unit,dc=macaomilano,dc=org
|
||||
objectClass: top
|
||||
objectClass: organizationalUnit
|
||||
ou: Groups
|
||||
ou: Roles
|
||||
|
||||
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=Robots,dc=unit,dc=macaomilano,dc=org
|
||||
objectClass: account
|
||||
objectClass: simpleSecurityObject
|
||||
objectClass: top
|
||||
uid: irc
|
||||
userPassword: {SHA}0WvxFW9MSsesf55SOh4vnuwdkgY=
|
||||
|
||||
dn: uid=git,ou=Robots,dc=unit,dc=macaomilano,dc=org
|
||||
objectClass: account
|
||||
objectClass: simpleSecurityObject
|
||||
objectClass: top
|
||||
uid: git
|
||||
userPassword: {SHA}RvGgvVWSovkkTKMhsSmQKga1PgM=
|
||||
|
||||
dn: uid=conte_mascetti,ou=Hackers,dc=unit,dc=macaomilano,dc=org
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: organizationalPerson
|
||||
objectClass: person
|
||||
objectClass: top
|
||||
memberOf: cn=Admins,ou=Roles,dc=unit,dc=macaomilano,dc=org
|
||||
memberOf: cn=GitUsers,ou=Roles,dc=unit,dc=macaomilano,dc=org
|
||||
memberOf: cn=IRCUsers,ou=Roles,dc=unit,dc=macaomilano,dc=org
|
||||
cn: Raffaello
|
||||
sn: Mascetti
|
||||
mail: rmascetti@autistici.org
|
||||
uid: conte_mascetti
|
||||
userPassword: {SHA}oLY7P6V+DWaMJhix7vbMYGIfA+E=
|
||||
|
||||
dn: cn=WikiUsers,ou=Groups,dc=unit,dc=macaomilano,dc=org
|
||||
dn: uid=necchi,ou=Hackers,dc=unit,dc=macaomilano,dc=org
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: organizationalPerson
|
||||
objectClass: person
|
||||
objectClass: top
|
||||
memberOf: cn=GitUsers,ou=Roles,dc=unit,dc=macaomilano,dc=org
|
||||
memberOf: cn=IRCUsers,ou=Roles,dc=unit,dc=macaomilano,dc=org
|
||||
cn: Guido
|
||||
sn: Necchi
|
||||
mail: gnecchi@autistici.org
|
||||
uid: necchi
|
||||
userPassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
|
||||
|
||||
dn: uid=perozzi,ou=Hackers,dc=unit,dc=macaomilano,dc=org
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: organizationalPerson
|
||||
objectClass: person
|
||||
objectClass: top
|
||||
memberOf: cn=GitUsers,ou=Roles,dc=unit,dc=macaomilano,dc=org
|
||||
cn: Giorgio
|
||||
sn: Perozzi
|
||||
mail: gperozzi@autistici.org
|
||||
uid: perozzi
|
||||
userPassword: {SHA}0+CRQKqsTj1I82PHxvZ4ebbddXQ=
|
||||
|
||||
dn: cn=Admins,ou=Roles,dc=unit,dc=macaomilano,dc=org
|
||||
member: uid=conte_mascetti,ou=Hackers,dc=unit,dc=macaomilano,dc=org
|
||||
cn: Admins
|
||||
objectClass: groupOfNames
|
||||
objectClass: top
|
||||
cn: WikiUsers
|
||||
|
||||
dn: cn=GitUsers,ou=Roles,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
|
||||
cn: GitUsers
|
||||
objectClass: groupOfNames
|
||||
objectClass: top
|
||||
|
||||
dn: cn=IRCUsers,ou=Roles,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
|
||||
objectClass: groupOfNames
|
||||
objectClass: top
|
||||
|
|
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta:__legacy__"
|
|
@ -1,7 +1,11 @@
|
|||
[aliases]
|
||||
test=pytest
|
||||
|
||||
[pycodestyle]
|
||||
max-line-length=88
|
||||
|
||||
[flake8]
|
||||
max-line-length=88
|
||||
exclude =
|
||||
.git,
|
||||
__pycache__,
|
||||
|
|
43
setup.py
43
setup.py
|
@ -1,22 +1,29 @@
|
|||
from setuptools import setup
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name='phi',
|
||||
version='0.0.1',
|
||||
|
||||
description='Post-Human Interface',
|
||||
name="phi",
|
||||
version="0.0.1",
|
||||
description="Post-Human Interface",
|
||||
# license='',
|
||||
url='https://git.abbiamoundominio.org/unit/phi',
|
||||
|
||||
author='unit',
|
||||
author_email='unit@paranoici.org',
|
||||
|
||||
package_dir={'': 'src'},
|
||||
packages=['phi', 'phi.api', 'phi.ldap'],
|
||||
scripts=['src/phid', 'src/phicli'],
|
||||
|
||||
setup_requires=['pytest-runner'],
|
||||
install_requires=['pyYAML', 'ldap3'],
|
||||
tests_require=['pytest']
|
||||
url="https://git.abbiamoundominio.org/unit/phi",
|
||||
author="unit",
|
||||
author_email="unit@paranoici.org",
|
||||
package_dir={"": "src"},
|
||||
packages=find_packages("src"),
|
||||
entry_points={
|
||||
"console_scripts": ["phid=phi.web.app:cli", "phiadm=phi.cli.adm:cli"]
|
||||
},
|
||||
setup_requires=["pytest-runner"],
|
||||
install_requires=[
|
||||
"aiohttp==3.8.1",
|
||||
"aiohttp_session[secure]==2.11.0",
|
||||
"click==8",
|
||||
"pyYAML",
|
||||
"bonsai==1.3.0",
|
||||
"passlib==1.7.1",
|
||||
"bcrypt==3.2.0",
|
||||
"jinja2==3",
|
||||
],
|
||||
tests_require=["pytest", "pytest-aiohttp", "pytest-asyncio", "async-generator"],
|
||||
python_requires=">=3.9",
|
||||
)
|
||||
|
|
|
@ -1,30 +1,21 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
from aiohttp import web
|
||||
|
||||
from phi.logging import get_logger
|
||||
from phi.ldap.client import Client
|
||||
from phi.api.routes import api_routes
|
||||
from phi.web.auth_middleware import authenticated
|
||||
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
def api_startup(app):
|
||||
app['ldap_client'].open()
|
||||
|
||||
|
||||
def api_shutdown(app):
|
||||
app['ldap_client'].close()
|
||||
|
||||
|
||||
def api_app(config):
|
||||
def api_app(store):
|
||||
log.info("Initializing API sub-app.")
|
||||
|
||||
app = web.Application()
|
||||
app = web.Application(middlewares=[authenticated])
|
||||
|
||||
ldap_client = Client(**config.get('ldap', {}))
|
||||
app['ldap_client'] = ldap_client
|
||||
|
||||
app.on_startup.append(api_startup)
|
||||
app.on_shutdown.append(api_shutdown)
|
||||
app["store"] = store
|
||||
app["log"] = log
|
||||
|
||||
app.router.add_routes(api_routes)
|
||||
|
||||
|
|
|
@ -1,24 +1,131 @@
|
|||
from aiohttp.web import json_response, View
|
||||
from aiohttp.web import HTTPNotFound, HTTPUnprocessableEntity
|
||||
from aiohttp.web import (
|
||||
HTTPNotFound,
|
||||
HTTPUnprocessableEntity,
|
||||
HTTPServerError,
|
||||
HTTPNoContent,
|
||||
HTTPBadRequest,
|
||||
HTTPCreated,
|
||||
)
|
||||
|
||||
from phi.logging import get_logger
|
||||
from phi.ldap.user import get_user_by_uid
|
||||
from phi.api.utils import serialize
|
||||
# from phi.api.utils import serialize
|
||||
from phi.async_ldap.model import User
|
||||
from phi.exceptions import (
|
||||
PhiEntryDoesNotExist,
|
||||
PhiUnexpectedRuntimeValue,
|
||||
PhiCannotExecute,
|
||||
)
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
class User(View):
|
||||
class UserView(View):
|
||||
def get_log(self):
|
||||
return self.request.app["log"]
|
||||
|
||||
def get_uid(self):
|
||||
try:
|
||||
return self.request.match_info["uid"]
|
||||
except KeyError:
|
||||
raise HTTPUnprocessableEntity
|
||||
|
||||
async def find_user(self, uid):
|
||||
log = self.get_log()
|
||||
|
||||
try:
|
||||
user = await self.request.app["users"].search(uid)
|
||||
log.info("Found user %s", user)
|
||||
except PhiEntryDoesNotExist as e:
|
||||
log.info(e)
|
||||
raise HTTPNotFound
|
||||
|
||||
return user
|
||||
|
||||
async def get(self):
|
||||
uid = self.request.match_info.get('uid', None)
|
||||
log = self.get_log()
|
||||
log.debug(f"{self.__class__.__name__}.get")
|
||||
uid = self.get_uid()
|
||||
|
||||
if uid is None:
|
||||
return HTTPUnprocessableEntity()
|
||||
user = await self.find_user(uid)
|
||||
|
||||
client = self.request.app['ldap_client']
|
||||
user = get_user_by_uid(client, uid)
|
||||
result = await user.describe()
|
||||
log.debug("Returning result %s", result)
|
||||
|
||||
if not user:
|
||||
return HTTPNotFound()
|
||||
return json_response(result)
|
||||
|
||||
return json_response(serialize(user))
|
||||
async def post(self):
|
||||
log = self.get_log()
|
||||
log.debug(f"{self.__class__.__name__}.post")
|
||||
uid = self.get_uid()
|
||||
|
||||
user = await self.find_user(uid)
|
||||
|
||||
try:
|
||||
user = await self.request.app["users"].search(uid)
|
||||
log.info("Found user %s", user)
|
||||
except PhiEntryDoesNotExist as e:
|
||||
log.debug(e)
|
||||
raise HTTPNotFound(text=str(e))
|
||||
except PhiUnexpectedRuntimeValue as e:
|
||||
log.error("Exception: %s", e)
|
||||
raise HTTPServerError
|
||||
|
||||
body = await self.request.json()
|
||||
|
||||
for k, v in body.items():
|
||||
try:
|
||||
user[k] = v
|
||||
except PhiCannotExecute as e:
|
||||
err = str(e)
|
||||
log.error(err)
|
||||
raise HTTPBadRequest(text=err)
|
||||
|
||||
try:
|
||||
await user.modify()
|
||||
except Exception as e:
|
||||
log.error("Exception: %s", e)
|
||||
|
||||
raise HTTPNoContent
|
||||
|
||||
async def put(self):
|
||||
log = self.get_log()
|
||||
log.debug(f"{self.__class__.__name__}.put")
|
||||
uid = self.get_uid()
|
||||
|
||||
user = User(self.request.app["ldap_client"], uid)
|
||||
|
||||
body = await self.request.json()
|
||||
|
||||
try:
|
||||
for k, v in body.items():
|
||||
user[k] = v
|
||||
|
||||
log.debug(f"Saving: {user}")
|
||||
await user.save()
|
||||
except PhiCannotExecute as e:
|
||||
err = str(e)
|
||||
log.error(err)
|
||||
raise HTTPBadRequest(text=err)
|
||||
|
||||
log.info(f"New user saved: {user}")
|
||||
|
||||
raise HTTPCreated
|
||||
|
||||
async def delete(self):
|
||||
log = self.get_log()
|
||||
log.debug(f"{self.__class__.__name__}.delete")
|
||||
uid = self.get_uid()
|
||||
|
||||
try:
|
||||
user = await self.request.app["users"].search(uid)
|
||||
log.info("Found user %s", user)
|
||||
await user.delete()
|
||||
except PhiEntryDoesNotExist as e:
|
||||
log.debug(e)
|
||||
raise HTTPNotFound(text=str(e))
|
||||
except PhiUnexpectedRuntimeValue as e:
|
||||
log.error("Exception: %s", e)
|
||||
raise HTTPServerError
|
||||
|
||||
raise HTTPNoContent
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
from aiohttp.web import route
|
||||
|
||||
from phi.api.rest import User
|
||||
from aiohttp.web import view
|
||||
|
||||
from phi.api.rest import UserView
|
||||
|
||||
api_routes = [
|
||||
route('*', '/user', User),
|
||||
route('*', '/user/', User),
|
||||
route('*', '/user/{uid}', User)
|
||||
view("/user", UserView),
|
||||
view("/user/", UserView),
|
||||
view("/user/{uid}", UserView),
|
||||
]
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from datetime import datetime
|
||||
|
||||
|
||||
def serialize(d):
|
||||
return {k: (v.isoformat() if isinstance(v, datetime) else v)
|
||||
for k, v in d.items()}
|
||||
def serialize(obj):
|
||||
return {
|
||||
k: (v.isoformat() if isinstance(v, datetime) else v)
|
||||
for k, v in dict(obj).items()
|
||||
}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
from asyncio import get_event_loop
|
||||
from aiohttp import web
|
||||
|
||||
from phi.api.app import api_app
|
||||
|
||||
|
||||
def setup_app(config):
|
||||
loop = get_event_loop()
|
||||
|
||||
app = web.Application(loop=loop)
|
||||
app['config'] = config
|
||||
|
||||
api = api_app(config)
|
||||
app.add_subapp('/api', api)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def run_app(app):
|
||||
web.run_app(app,
|
||||
host=app['config']['core']['listen'].get('host', '127.0.0.1'),
|
||||
port=app['config']['core']['listen'].get('port', '8080'))
|
135
src/phi/async_ldap/client.py
Normal file
135
src/phi/async_ldap/client.py
Normal file
|
@ -0,0 +1,135 @@
|
|||
from urllib.parse import urlparse
|
||||
|
||||
from bonsai import LDAPClient
|
||||
|
||||
from phi.logging import get_logger
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
def parse_host(host):
|
||||
"""
|
||||
Helper function to decompose the host in the address
|
||||
and the (optional) protocol and the (optional) port.
|
||||
If missing, protocol defaults to "ldap" and port to 389,
|
||||
in case protocol is missing or is "ldap", or 636, in case
|
||||
protocol is "ldaps".
|
||||
"""
|
||||
if "://" not in host:
|
||||
host = f"//{host}"
|
||||
|
||||
p = urlparse(host)
|
||||
if p.scheme is not None and p.scheme != "":
|
||||
proto = p.scheme
|
||||
else:
|
||||
proto = "ldap"
|
||||
|
||||
if p.port is not None:
|
||||
port = p.port
|
||||
else:
|
||||
port = None
|
||||
|
||||
if port is not None:
|
||||
addr = p.netloc.split(":")[0]
|
||||
else:
|
||||
addr = p.netloc
|
||||
if proto == "ldap":
|
||||
port = 389
|
||||
elif proto == "ldaps":
|
||||
port = 636
|
||||
|
||||
return proto, addr, port
|
||||
|
||||
|
||||
def checked_port(provided, auto):
|
||||
"""
|
||||
Check consistency of ports given via the connection string
|
||||
and the explicit parameter.
|
||||
"""
|
||||
_provided = provided is not None
|
||||
|
||||
if _provided and provided != auto:
|
||||
log.warning(
|
||||
"Explicitly provided port ({}) does not match "
|
||||
"the automatically provided one ({}). The former prevails.".format(
|
||||
provided, auto
|
||||
)
|
||||
)
|
||||
return provided
|
||||
|
||||
if _provided:
|
||||
return provided
|
||||
|
||||
return auto
|
||||
|
||||
|
||||
def compose_dn_username(username, base_dn, ou=None, attribute_id=None):
|
||||
"""
|
||||
Output the distinguished name of the user to use as login.
|
||||
"""
|
||||
if base_dn in username:
|
||||
return username
|
||||
|
||||
if ou is None:
|
||||
return f"{attribute_id}={username},{base_dn}"
|
||||
|
||||
return f"{attribute_id}={username},ou={ou},{base_dn}"
|
||||
|
||||
|
||||
class AsyncClient(LDAPClient):
|
||||
"""
|
||||
Wrapper around LDAPClient.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host=None,
|
||||
port=None,
|
||||
encryption=None,
|
||||
ciphers=None,
|
||||
validate=False,
|
||||
ca_cert=None,
|
||||
username=None,
|
||||
password=None,
|
||||
base_dn=None,
|
||||
attribute_id="uid",
|
||||
ou=None,
|
||||
method="SIMPLE",
|
||||
**kwargs,
|
||||
):
|
||||
self.proto, self.host, _port = parse_host(host)
|
||||
self.port = checked_port(port, _port)
|
||||
self.full_uri = "{}://{}:{}".format(self.proto, self.host, self.port)
|
||||
self.base_dn = base_dn
|
||||
|
||||
if encryption:
|
||||
self._tls = True
|
||||
else:
|
||||
if self.proto == "ldaps":
|
||||
raise ValueError(
|
||||
'Incompatible provided protocol ("%s") and encryption configuration: TLS=%s',
|
||||
self.proto,
|
||||
encryption,
|
||||
)
|
||||
self._tls = False
|
||||
|
||||
super().__init__(self.full_uri, self._tls)
|
||||
log.info(
|
||||
"Connected at %s (TLS -> %s)", self.full_uri, "ON" if self.tls else "OFF"
|
||||
)
|
||||
|
||||
if not validate:
|
||||
self.set_cert_policy("never")
|
||||
|
||||
if ca_cert is not None:
|
||||
self.set_ca_cert(ca_cert)
|
||||
|
||||
self.username = compose_dn_username(username, self.base_dn, ou, attribute_id)
|
||||
self.password = password
|
||||
self.method = method
|
||||
|
||||
self.set_auto_page_acquire(True)
|
||||
self.set_credentials(self.method, user=self.username, password=self.password)
|
||||
|
||||
def __repr__(self):
|
||||
return f"AsyncClient[{self.full_uri}]<{self.username}>"
|
344
src/phi/async_ldap/mixins.py
Normal file
344
src/phi/async_ldap/mixins.py
Normal file
|
@ -0,0 +1,344 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
from bonsai import LDAPEntry, LDAPModOp, NoSuchObjectError # type: ignore
|
||||
from bonsai.ldapvaluelist import LDAPValueList
|
||||
import bonsai.errors
|
||||
|
||||
from phi.exceptions import (
|
||||
PhiEntryDoesNotExist,
|
||||
PhiEntryExists,
|
||||
PhiUnexpectedRuntimeValue,
|
||||
PhiCannotExecute,
|
||||
)
|
||||
|
||||
from phi.security import hash_pass, handle_password
|
||||
|
||||
|
||||
def de_listify(elems):
|
||||
if not isinstance(elems, list):
|
||||
return elems
|
||||
if len(elems) == 1:
|
||||
return elems[0]
|
||||
else:
|
||||
return elems
|
||||
|
||||
|
||||
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(obj.client, _name)
|
||||
|
||||
|
||||
class Singleton(object):
|
||||
"""
|
||||
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, **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 not hasattr(cls, "_instances"):
|
||||
cls._instances = dict()
|
||||
if name not in cls._instances:
|
||||
cls._instances[name] = object.__new__(cls)
|
||||
return cls._instances[name]
|
||||
|
||||
|
||||
class Entry(object):
|
||||
"""
|
||||
Mixin to interact with LDAP.
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.dn}>"
|
||||
|
||||
def __str__(self):
|
||||
return f"<{self.__class__.__name__} {self.dn}>"
|
||||
|
||||
async def _create_new(self):
|
||||
self._entry["objectClass"] = self.object_class
|
||||
async with self.client.connect(is_async=True) as conn:
|
||||
await conn.add(self._entry)
|
||||
return self._entry
|
||||
|
||||
async def _get(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 in.
|
||||
_res = await conn.search(self.dn, 0)
|
||||
if len(_res) == 0:
|
||||
raise PhiEntryDoesNotExist(self.dn)
|
||||
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, recursive=self.delete_cascade)
|
||||
|
||||
async def describe(self):
|
||||
_internal = await self._get()
|
||||
values = dict()
|
||||
for attr in self.ldap_attributes:
|
||||
value = _internal.get(attr)
|
||||
if value:
|
||||
values[attr] = de_listify(value)
|
||||
if "userPassword" in self.ldap_attributes:
|
||||
values.pop("userPassword")
|
||||
values["dn"] = self.dn
|
||||
return values
|
||||
|
||||
@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):
|
||||
"""
|
||||
Mixin that represents an OrganizationalUnit. It provides the methods to interact
|
||||
with the LDAP db _and_ to supervise its `Member`s.
|
||||
To properly use it, one must specify the `ou` and `child_class` class attributes
|
||||
when inheriting.
|
||||
"""
|
||||
|
||||
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, self.child_class, self.child_class.id_tag)
|
||||
self._entry = LDAPEntry(self.dn)
|
||||
for k, v in kwargs.items():
|
||||
if k in self.ldap_attributes:
|
||||
self._entry[k] = v
|
||||
if "delete_cascade" in kwargs:
|
||||
self.delete_cascade = delete_cascade
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
try:
|
||||
return await self.children.__anext__()
|
||||
except StopAsyncIteration:
|
||||
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, 1):
|
||||
yield el
|
||||
|
||||
@property
|
||||
def dn(self):
|
||||
return f"ou={self.ou},{self.base_dn}"
|
||||
|
||||
async def save(self):
|
||||
"""
|
||||
This function iterates over the OU's children and invokes its `save` method,
|
||||
ignoring errors from yet existing ones.
|
||||
"""
|
||||
async for child in self:
|
||||
try:
|
||||
await child.save()
|
||||
except PhiEntryExists:
|
||||
pass
|
||||
|
||||
async def search(self, member_name):
|
||||
"""
|
||||
This function allows one to search through the OU's children. The search
|
||||
function is the one from the underlying library (bonsai) and is strict as such.
|
||||
"""
|
||||
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")
|
||||
|
||||
|
||||
def _hydrate(obj, data):
|
||||
"""
|
||||
Iterate over the structure of the given `data`. Using the key name, filtering
|
||||
only on the values that the given `obj` accepts (`obj.ldap_attributes`),
|
||||
appropriately set the corresponding value in the given `obj`. In particular:
|
||||
- append to lists
|
||||
- handle password setting
|
||||
- set scalars
|
||||
This is called `_hydrate` because its aim is to fill a structure (the `obj`)
|
||||
with substance.
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
class Member(object):
|
||||
"""
|
||||
Mixin that represents a generic member of an `OrganizationalUnit`.
|
||||
It provides the methods to interact with the LDAP db.
|
||||
To properly use, `ou`, `object_class` and `ldap_attributes` class attributes must
|
||||
be specified when inheriting.
|
||||
|
||||
## Usage
|
||||
|
||||
The initialization needs an `phi.async_ldap.AsyncClient` and a `name`, that is used
|
||||
as value in the identification attribute (i.e. `uid`).
|
||||
This inits an object in memory that may or may not exist in the ldap database yet.
|
||||
To test it, one can invoke the async method `exists` or may try to `sync`, handling
|
||||
the corresponding exception (`PhiEntryDoesNotExist`).
|
||||
To save a new instance, one can `save`. The instance accepts dict-like get and set
|
||||
on the aforementioned `ldap_attributes`. Once an attribute value has been modified,
|
||||
one can invoke `modify` to persist the changes.
|
||||
To remove an instance from the database, one can invoke `delete`.
|
||||
|
||||
## Comparisons
|
||||
|
||||
The comparison operation with a `Member` is quite loose: it returns `True` with
|
||||
either:
|
||||
- an instance of the same `type` (i.e. the same class whose this mixin is used
|
||||
into) whose `dn` matches
|
||||
- an `LDAPEntry` whose `dn` matches
|
||||
- a string matching the `dn`
|
||||
"""
|
||||
|
||||
def __init__(self, client, name, **kwargs):
|
||||
super().__init__()
|
||||
self.client = client
|
||||
self.base_dn = client.base_dn
|
||||
self.name = name
|
||||
self._entry = LDAPEntry(self.dn)
|
||||
self[self.id_tag] = name
|
||||
self._entry["ou"] = self.ou
|
||||
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 other["dn"] == self.dn
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def dn(self):
|
||||
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):
|
||||
"""
|
||||
This method persists on the ldap database an inited instance. Raises
|
||||
`PhiEntryExists` in case of a yet existing instance. Raises a specific error if
|
||||
the instance misses any of the needed attributes (accoding to
|
||||
`ldap_attributes`).
|
||||
"""
|
||||
try:
|
||||
await self._create_new()
|
||||
except bonsai.errors.AlreadyExists:
|
||||
raise PhiEntryExists(self.dn)
|
||||
|
||||
async def modify(self):
|
||||
"""
|
||||
This method saves the changes made to the instance on the ldap database. Raises
|
||||
`PhiEntryDoesNotExist` in case of an instance not yet persisted.
|
||||
"""
|
||||
await self._modify()
|
||||
|
||||
async def delete(self):
|
||||
"""
|
||||
This method removes the instance from the database. Raises
|
||||
`PhiEntryDoesNotExist` in case the entry does not exist.
|
||||
"""
|
||||
await self._delete()
|
||||
|
||||
async def sync(self):
|
||||
"""
|
||||
This method reads the `ldap_attributes` of an existing instance from the ldap
|
||||
database and assigns the values to `self`. It is needed at first instantiation
|
||||
of the object, in case an instance exists on the database.
|
||||
"""
|
||||
res = await self._get()
|
||||
_hydrate(self, res)
|
||||
return self
|
||||
|
||||
async def exists(self):
|
||||
"""
|
||||
This method returns `True` if the instance exists on the ldap database, `False`
|
||||
if it does not. It might raise `PhiUnexpectedRuntimeValue` if the ldap state is
|
||||
inconsistent.
|
||||
"""
|
||||
try:
|
||||
_ = await self.sync()
|
||||
return True
|
||||
except PhiEntryDoesNotExist:
|
||||
return False
|
140
src/phi/async_ldap/model.py
Normal file
140
src/phi/async_ldap/model.py
Normal file
|
@ -0,0 +1,140 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from bonsai import LDAPEntry
|
||||
from multidict import MultiDict
|
||||
|
||||
from phi.async_ldap import mixins
|
||||
from phi.exceptions import (
|
||||
PhiUnexpectedRuntimeValue,
|
||||
)
|
||||
|
||||
|
||||
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", "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
|
||||
raise NotImplemented
|
||||
|
||||
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 = "Roles"
|
||||
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 Roles(mixins.OrganizationalUnit, mixins.Entry, mixins.Singleton):
|
||||
_instances = dict() # type: ignore
|
||||
ou = "Roles"
|
||||
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
|
|
@ -1,47 +0,0 @@
|
|||
import sys
|
||||
import argparse
|
||||
import inspect
|
||||
from phi.logging import setup_logging, get_logger
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
subparses = parser.add_subparsers(title='actions', dest='action')
|
||||
|
||||
cli_callbacks = {}
|
||||
|
||||
|
||||
def register(action_info='', param_infos=[]):
|
||||
def decorator(action):
|
||||
# Get function name and arguments
|
||||
action_name = action.__name__
|
||||
param_names = inspect.getfullargspec(action)[0]
|
||||
|
||||
# Create subparser for specific action
|
||||
subparser = subparses.add_parser(action_name, help=action_info)
|
||||
|
||||
for i, name in enumerate(param_names):
|
||||
info = param_infos[i] if i<len(param_infos) else ''
|
||||
subparser.add_argument(dest=name, help=info)
|
||||
|
||||
# Register action
|
||||
cli_callbacks[action_name] = action, param_names
|
||||
return action
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def run(args):
|
||||
for action_name, (action, param_names) in cli_callbacks.items():
|
||||
if args['action'] == action_name:
|
||||
action(**{pname: args[pname] for pname in param_names})
|
||||
|
||||
|
||||
def add_arg(name, example, info):
|
||||
parser.add_argument(name, metavar=example, help=info)
|
||||
|
||||
|
||||
def get_args():
|
||||
namespace = parser.parse_args(sys.argv[1:])
|
||||
args = namespace.__dict__
|
||||
return args
|
0
src/phi/cli/__init__.py
Normal file
0
src/phi/cli/__init__.py
Normal file
119
src/phi/cli/adm.py
Normal file
119
src/phi/cli/adm.py
Normal file
|
@ -0,0 +1,119 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
import importlib.resources
|
||||
from fnmatch import fnmatch
|
||||
import re
|
||||
|
||||
import click
|
||||
|
||||
from phi.cli.utils import generate_from_templates
|
||||
from phi.security import hash_pass
|
||||
|
||||
|
||||
SERVICE_RE = re.compile(r"^(.+?):(.+)$")
|
||||
|
||||
templates = [
|
||||
f.name.strip(".j2")
|
||||
for f in importlib.resources.files("phi.cli.templates").iterdir()
|
||||
if not f.name.endswith(".py")
|
||||
]
|
||||
|
||||
|
||||
def debug(ctx, out):
|
||||
if ctx.obj["debug"]:
|
||||
click.echo(f"DEBUG: {out}", err=True)
|
||||
|
||||
|
||||
def validate_services(ctx, _, value):
|
||||
for service in value:
|
||||
if not SERVICE_RE.match(service):
|
||||
click.echo(f"Unparsable service '{service}' - must match /^(.+?):(.+)$/")
|
||||
ctx.exit()
|
||||
|
||||
return value
|
||||
|
||||
|
||||
@click.group(
|
||||
name="phiadm", help="This cli may be used to interact with a local phid instance"
|
||||
)
|
||||
@click.option("-d", "--debug", is_flag=True, help="Enable debugging information")
|
||||
@click.pass_context
|
||||
def cli(ctx, debug):
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj["debug"] = debug
|
||||
|
||||
|
||||
@cli.command(
|
||||
"generate",
|
||||
help="The base name of this LDAP directory (e.g. 'dc=example,dc=com')",
|
||||
)
|
||||
@click.option(
|
||||
"-t",
|
||||
"--template",
|
||||
type=click.STRING,
|
||||
multiple=True,
|
||||
help=f"Name of the template (allowed: {', '.join(templates)};"
|
||||
+ " accepts also glob-like patterns)",
|
||||
)
|
||||
@click.option(
|
||||
"-s",
|
||||
"--default-service",
|
||||
type=click.STRING,
|
||||
multiple=True,
|
||||
callback=validate_services,
|
||||
help="A pair <name>:<password> representing a service (and the associated password)"
|
||||
)
|
||||
@click.option(
|
||||
"--root-password",
|
||||
type=click.STRING,
|
||||
required=True,
|
||||
help="The cleartext password for the root user",
|
||||
)
|
||||
@click.option(
|
||||
"--phi-password",
|
||||
type=click.STRING,
|
||||
required=True,
|
||||
help="The cleartext password for the phi service",
|
||||
)
|
||||
@click.argument(
|
||||
"base_dn",
|
||||
type=click.STRING,
|
||||
)
|
||||
@click.pass_context
|
||||
def generate(ctx, template, default_service, root_password, phi_password, base_dn):
|
||||
config = get_config(base_dn, phi_password, root_password)
|
||||
debug(ctx, f"default_service: {default_service}")
|
||||
if default_service:
|
||||
add_default_services(config, default_service)
|
||||
debug(ctx, f"config: {config}")
|
||||
debug(ctx, f"templates: {templates}")
|
||||
|
||||
if not template:
|
||||
template = templates
|
||||
|
||||
for name, content in generate_from_templates(config):
|
||||
debug(ctx, f"current template: {name}")
|
||||
if any(fnmatch(name, t) for t in template):
|
||||
click.echo(content)
|
||||
|
||||
|
||||
def get_config(base_dn, phi_password, root_password):
|
||||
config = {"default_services": []}
|
||||
config["base_dn"] = base_dn
|
||||
config["phi_password"] = hash_pass(phi_password)
|
||||
config["root_password"] = hash_pass(root_password)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def add_default_services(config, services):
|
||||
for service in services:
|
||||
user, password = parse_service(service)
|
||||
config["default_services"].append(
|
||||
{"name": user, "password": hash_pass(password)})
|
||||
|
||||
|
||||
def parse_service(service):
|
||||
res = SERVICE_RE.search(service).groups()
|
||||
if len(res) != 2:
|
||||
raise ValueError(res)
|
||||
return res[0], res[1]
|
23
src/phi/cli/templates/00-base.ldif.j2
Normal file
23
src/phi/cli/templates/00-base.ldif.j2
Normal file
|
@ -0,0 +1,23 @@
|
|||
version: 1
|
||||
|
||||
dn: {{ base_dn }}
|
||||
objectClass: organization
|
||||
objectClass: dcObject
|
||||
dc: unit
|
||||
o: Unit
|
||||
|
||||
dn: ou=Hackers,{{ base_dn }}
|
||||
objectClass: organizationalUnit
|
||||
objectClass: top
|
||||
ou: Hackers
|
||||
|
||||
dn: ou=Robots,{{ base_dn }}
|
||||
objectClass: top
|
||||
objectClass: organizationalUnit
|
||||
ou: Robots
|
||||
|
||||
dn: ou=Roles,{{ base_dn }}
|
||||
objectClass: top
|
||||
objectClass: organizationalUnit
|
||||
ou: Roles
|
||||
|
17
src/phi/cli/templates/10-users.ldif.j2
Normal file
17
src/phi/cli/templates/10-users.ldif.j2
Normal file
|
@ -0,0 +1,17 @@
|
|||
version: 1
|
||||
|
||||
dn: uid=phi,ou=Robots,{{ base_dn }}
|
||||
objectClass: account
|
||||
objectClass: simpleSecurityObject
|
||||
objectClass: top
|
||||
uid: phi
|
||||
userPassword: {{ phi_password }}
|
||||
{%- for service in default_services %}
|
||||
|
||||
dn: uid={{ service.name }},ou=Robots,{{ base_dn }}
|
||||
objectClass: account
|
||||
objectClass: simpleSecurityObject
|
||||
objectClass: top
|
||||
uid={{ service.name }}
|
||||
userPassword={{ service.password }}
|
||||
{%- endfor %}
|
6
src/phi/cli/templates/20-roles.ldif.j2
Normal file
6
src/phi/cli/templates/20-roles.ldif.j2
Normal file
|
@ -0,0 +1,6 @@
|
|||
version: 1
|
||||
|
||||
dn: cn=Admins,ou=Roles,{{ base_dn }}
|
||||
cn: Admins
|
||||
objectClass: groupOfNames
|
||||
objectClass: top
|
14
src/phi/cli/templates/99-acl.ldif.j2
Normal file
14
src/phi/cli/templates/99-acl.ldif.j2
Normal file
|
@ -0,0 +1,14 @@
|
|||
access to dn.base=""
|
||||
by * read
|
||||
by group.exact="cn=Admins,{{ base_dn }}" manage
|
||||
|
||||
access to attrs=entry
|
||||
by * read
|
||||
|
||||
access to attrs=userPassword
|
||||
by self write
|
||||
by anonymous auth
|
||||
|
||||
access to dn.subtree="ou=Hackers,{{ base_dn }}"
|
||||
by self write
|
||||
by dn.subtree="ou=Services,{{ base_dn }}" read
|
0
src/phi/cli/templates/__init__.py
Normal file
0
src/phi/cli/templates/__init__.py
Normal file
87
src/phi/cli/templates/slapd.conf.j2
Normal file
87
src/phi/cli/templates/slapd.conf.j2
Normal file
|
@ -0,0 +1,87 @@
|
|||
#######################################################################
|
||||
# Modules
|
||||
#######################################################################
|
||||
|
||||
include /etc/openldap/schema/core.schema
|
||||
include /etc/openldap/schema/cosine.schema
|
||||
include /etc/openldap/schema/corba.schema
|
||||
include /etc/openldap/schema/inetorgperson.schema
|
||||
include /etc/openldap/schema/nis.schema
|
||||
include /etc/openldap/schema/collective.schema
|
||||
include /etc/openldap/schema/openldap.schema
|
||||
|
||||
modulepath /usr/lib/openldap
|
||||
moduleload back_mdb
|
||||
moduleload refint
|
||||
moduleload memberof
|
||||
|
||||
|
||||
#######################################################################
|
||||
# Core
|
||||
#######################################################################
|
||||
|
||||
pidfile /var/slapd/slapd.pid
|
||||
argsfile /var/slapd/slapd.args
|
||||
loglevel conns
|
||||
|
||||
serverID 0
|
||||
|
||||
|
||||
#######################################################################
|
||||
# Security
|
||||
#######################################################################
|
||||
|
||||
#TLSCACertificateFile /var/slapd/fullchain.pem
|
||||
TLSCertificateFile /var/slapd/cert.pem
|
||||
TLSCertificateKeyFile /var/slapd/key.pem
|
||||
TLSCipherSuite HIGH
|
||||
|
||||
# Sample security restrictions
|
||||
# Define global ACLs to disable default read access.
|
||||
# Require integrity protection (prevent hijacking)
|
||||
# Require 112-bit (3DES or better) encryption for updates
|
||||
# Require 63-bit encryption for simple bind
|
||||
security ssf=1 simple_bind=256 update_ssf=256
|
||||
|
||||
|
||||
#######################################################################
|
||||
# MDB database definitions
|
||||
#######################################################################
|
||||
|
||||
database mdb
|
||||
maxsize 1073741824
|
||||
suffix "{{ base_dn }}"
|
||||
|
||||
# Overlays to be loaded for the database.
|
||||
overlay memberof
|
||||
|
||||
# Cleartext passwords, especially for the rootdn, should
|
||||
# be avoid. See slappasswd(8) and slapd.conf(5) for details.
|
||||
# Use of strong authentication encouraged.
|
||||
rootdn "cn=root,{{ base_dn }}"
|
||||
rootpw {{ root_password }}
|
||||
|
||||
# The database directory MUST exist prior to running slapd AND
|
||||
# should only be accessible by the slapd and slap tools.
|
||||
# Mode 700 recommended.
|
||||
directory /var/slapd
|
||||
mode 0700
|
||||
|
||||
password-hash {CRYPT}
|
||||
password-crypt-salt-format "$6$%.16s"
|
||||
|
||||
# Indices to maintain
|
||||
index objectClass pres,eq
|
||||
index uid,cn,sn,mail eq,sub
|
||||
index memberof pres,eq
|
||||
|
||||
|
||||
#######################################################################
|
||||
# MemberOf configuration
|
||||
#######################################################################
|
||||
|
||||
memberof-group-oc groupOfNames
|
||||
memberof-memberof-ad memberOf
|
||||
memberof-member-ad member
|
||||
memberof-dangling error
|
||||
memberof-refint true
|
25
src/phi/cli/utils.py
Normal file
25
src/phi/cli/utils.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
import os.path
|
||||
|
||||
from jinja2 import Environment, ChoiceLoader, FileSystemLoader, PackageLoader
|
||||
|
||||
|
||||
def get_jinja_env():
|
||||
pkg_path_templates = os.path.realpath(os.path.join(__name__, "./templates"))
|
||||
loaders = [
|
||||
FileSystemLoader(pkg_path_templates),
|
||||
PackageLoader("phi.cli", package_path="templates"),
|
||||
]
|
||||
return Environment(loader=ChoiceLoader(loaders))
|
||||
|
||||
|
||||
def generate_from_templates(config):
|
||||
env = get_jinja_env()
|
||||
|
||||
for t in env.list_templates(extensions="j2"):
|
||||
tmpl = env.get_template(t)
|
||||
content = tmpl.render(**config)
|
||||
# This is loaded from a resource and won't be None
|
||||
name = tmpl.name.strip(".j2")
|
||||
# name = os.path.basename(tmpl.filename).strip(".j2")
|
||||
yield (name, content)
|
|
@ -1,31 +1,80 @@
|
|||
import os.path
|
||||
import pkg_resources
|
||||
import yaml
|
||||
|
||||
NAME = "phi"
|
||||
|
||||
NAME = 'phi'
|
||||
DEFAULT_CONFIG = {
|
||||
"core": {"listen": {"host": "localhost", "port": 8080}, "cookiestore_secret": None},
|
||||
"ldap": {
|
||||
"host": "localhost",
|
||||
"port": 389,
|
||||
"encryption": "TLSv1.2",
|
||||
"ciphers": "HIGH",
|
||||
"validate": True,
|
||||
"ca_certs": pkg_resources.resource_filename(NAME, "openldap/cert.pem"),
|
||||
"base_dn": None,
|
||||
"attribute_mail": "mail",
|
||||
},
|
||||
"logging": {
|
||||
"version": 1,
|
||||
"formatters": {"default": {"format": "[%(name)s %(levelname)s] %(message)s"}},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "default",
|
||||
"stream": "ext://sys.stdout",
|
||||
},
|
||||
"file": {
|
||||
"class": "logging.FileHandler",
|
||||
"formatter": "default",
|
||||
"filename": "phi.log",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"phi": {
|
||||
"level": "INFO",
|
||||
"handlers": ["console", "file"],
|
||||
},
|
||||
"aiohttp": {
|
||||
"level": "INFO",
|
||||
"handlers": ["console", "file"],
|
||||
},
|
||||
"bonsai": {
|
||||
"level": "WARNING",
|
||||
"handlers": ["console", "file"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
CONFIG_FILE = 'config.yml'
|
||||
CONFIG_PATHS = ['./',
|
||||
'~/.config/' + NAME + '/',
|
||||
'/usr/local/etc/' + NAME + '/',
|
||||
'/etc/' + NAME + '/']
|
||||
CONFIG_FILES = [os.path.join(p, CONFIG_FILE)
|
||||
for p in CONFIG_PATHS]
|
||||
DUMMY_CONFIG = {"core": {}, "ldap": {}, "logging": {}}
|
||||
|
||||
CONFIG_FILE = "config.yml"
|
||||
CONFIG_PATHS = [
|
||||
"./",
|
||||
"~/.config/" + NAME + "/",
|
||||
"/usr/local/etc/" + NAME + "/",
|
||||
"/etc/" + NAME + "/",
|
||||
]
|
||||
CONFIG_FILES = [os.path.join(p, CONFIG_FILE) for p in CONFIG_PATHS]
|
||||
|
||||
|
||||
def get_config(custom_config=None):
|
||||
def get_config(config_path=None):
|
||||
"""Return the path of the found configuration file and its content
|
||||
|
||||
:param config_path: optional path to a config file.
|
||||
|
||||
:returns: (path, config)
|
||||
:rtype: (str, dict)
|
||||
"""
|
||||
if custom_config:
|
||||
global CONFIG_FILES
|
||||
CONFIG_FILES = [custom_config]
|
||||
|
||||
if config_path:
|
||||
with open(config_path) as c:
|
||||
config = yaml.safe_load(c)
|
||||
return config_path, config
|
||||
for f in CONFIG_FILES:
|
||||
try:
|
||||
with open(f, 'r') as c:
|
||||
with open(f, "r") as c:
|
||||
config = yaml.safe_load(c)
|
||||
return (f, config)
|
||||
except FileNotFoundError:
|
||||
|
@ -34,11 +83,62 @@ def get_config(custom_config=None):
|
|||
# accessible or if the file is not present at all
|
||||
# in any of CONFIG_PATHS.
|
||||
pass
|
||||
else:
|
||||
if custom_config:
|
||||
raise FileNotFoundError('Config file {} not found.'
|
||||
.format(custom_config))
|
||||
return None, DUMMY_CONFIG
|
||||
|
||||
|
||||
def merge_config(cli_config, file_config):
|
||||
"""
|
||||
Merge the cli-provided and file-provided config.
|
||||
"""
|
||||
return recursive_merge(cli_config, file_config)
|
||||
|
||||
|
||||
def _init_with_shape_of(element):
|
||||
if isinstance(element, dict):
|
||||
return {}
|
||||
elif isinstance(element, list):
|
||||
return []
|
||||
return None
|
||||
|
||||
|
||||
def recursive_merge(main_config, aux_config):
|
||||
def _recursive_merge(main, aux, default, key, _ret_config):
|
||||
if isinstance(default, dict):
|
||||
_sub_conf = {}
|
||||
for k, v in default.items():
|
||||
_main = main[k] if k in main else _init_with_shape_of(v)
|
||||
_aux = aux[k] if k in aux else _init_with_shape_of(v)
|
||||
_recursive_merge(_main, _aux, v, k, _sub_conf)
|
||||
_ret_config[key] = _sub_conf
|
||||
elif isinstance(default, list):
|
||||
_main = main.copy()
|
||||
if aux is not None:
|
||||
_main.extend(aux)
|
||||
_ret_config[key] = list(set(_main))
|
||||
elif isinstance(default, bool):
|
||||
_ret_config[key] = default and aux and main
|
||||
else:
|
||||
raise FileNotFoundError("Could not find {} in any of {}."
|
||||
.format(CONFIG_FILE,
|
||||
', '.join(CONFIG_PATHS)))
|
||||
if main is not None:
|
||||
_ret_config[key] = main
|
||||
elif aux is not None:
|
||||
_ret_config[key] = aux
|
||||
else:
|
||||
_ret_config[key] = default
|
||||
|
||||
_config = {}
|
||||
_recursive_merge(main_config, aux_config, DEFAULT_CONFIG, "ROOT", _config)
|
||||
|
||||
return _config["ROOT"]
|
||||
|
||||
|
||||
def extract_secret(config):
|
||||
try:
|
||||
secret = config["core"]["cookiestore_secret"].encode("utf-8")
|
||||
if len(secret) != 32:
|
||||
raise ValueError(
|
||||
"The provided core.cookiestore_secret must be 32 bytes long"
|
||||
)
|
||||
|
||||
return secret
|
||||
except KeyError:
|
||||
raise RuntimeError("You must provide a core.cookiestore_secret")
|
||||
|
|
54
src/phi/exceptions.py
Normal file
54
src/phi/exceptions.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
|
||||
|
||||
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):
|
||||
self.dn = dn
|
||||
self.attr = attr
|
||||
|
||||
|
||||
class PhiUnauthorized(Exception):
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.user = user
|
||||
|
||||
|
||||
class PhiUnexpectedRuntimeValue(RuntimeWarning):
|
||||
def __init__(self, msg, result):
|
||||
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})"
|
|
@ -1,55 +0,0 @@
|
|||
from threading import Lock
|
||||
from ldap3.utils.log import set_library_log_detail_level, PROTOCOL
|
||||
|
||||
from phi.logging import get_logger
|
||||
from phi.ldap.connection import make_connection
|
||||
from phi.ldap.connection import open_connection, close_connection
|
||||
|
||||
log = get_logger(__name__)
|
||||
set_library_log_detail_level(PROTOCOL)
|
||||
|
||||
|
||||
class Client:
|
||||
def __init__(self,
|
||||
host=None, port=389,
|
||||
encryption=None, ciphers=None, validate=False, ca_certs=None,
|
||||
username=None, password=None,
|
||||
base_dn=None):
|
||||
log.info("Initializing LDAP Client.")
|
||||
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
self.encryption = encryption
|
||||
self.ciphers = ciphers
|
||||
self.validate = validate
|
||||
self.ca_certs = ca_certs
|
||||
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
self.base_dn = base_dn
|
||||
|
||||
self.connection_lock = Lock()
|
||||
self.connection = make_connection(host=self.host, port=self.port,
|
||||
encryption=self.encryption,
|
||||
ciphers=self.ciphers,
|
||||
validate=self.validate,
|
||||
ca_certs=self.ca_certs,
|
||||
username=self.username,
|
||||
password=self.password)
|
||||
|
||||
def open(self):
|
||||
self.connection_lock.acquire()
|
||||
if self.connection.closed is True:
|
||||
open_connection(self.connection)
|
||||
self.connection_lock.release()
|
||||
else:
|
||||
self.connection_lock.release()
|
||||
raise Exception("Trying to open a connection, "
|
||||
"but it is already open.")
|
||||
|
||||
def close(self):
|
||||
self.connection_lock.acquire()
|
||||
close_connection(self.connection)
|
||||
self.connection_lock.release()
|
|
@ -1,59 +0,0 @@
|
|||
from ssl import CERT_REQUIRED, PROTOCOL_TLSv1_2
|
||||
from ldap3 import Tls, Server, Connection, ASYNC
|
||||
|
||||
from phi.logging import get_logger
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
def make_connection(host=None, port=389,
|
||||
encryption=None, ciphers=None, validate=False,
|
||||
ca_certs=None, username=None, password=None):
|
||||
# TLSv1.2 is supported since Python 3.4
|
||||
if encryption is None:
|
||||
log.warning("The connection to the LDAP server will not be encrypted.")
|
||||
tls = None
|
||||
elif encryption == "TLSv1.2":
|
||||
log.info("The connection to the LDAP server will use TLSv1.2.")
|
||||
tls = Tls(version=PROTOCOL_TLSv1_2)
|
||||
else:
|
||||
raise NotImplementedError("Sorry, use TLSv1.2.")
|
||||
|
||||
if encryption is not None and ciphers is not None:
|
||||
log.info("The connection to the LDAP server will use the "
|
||||
"following ciphers: {}".format(ciphers))
|
||||
tls.ciphers = ciphers
|
||||
|
||||
if encryption is not None and validate is True:
|
||||
log.info("The certificate hostname will be checked to match the "
|
||||
"remote hostname.")
|
||||
tls.validate = CERT_REQUIRED
|
||||
|
||||
if encryption is not None and validate is True and ca_certs is not None:
|
||||
log.info("Using the following CA certificates: {}"
|
||||
.format(ca_certs))
|
||||
tls.ca_certs_file = ca_certs
|
||||
|
||||
server = Server(host=host, port=port, tls=tls)
|
||||
connection = Connection(server, user=username, password=password,
|
||||
client_strategy=ASYNC)
|
||||
|
||||
return connection
|
||||
|
||||
|
||||
def open_connection(connection):
|
||||
log.info("Opening connection to LDAP server.")
|
||||
connection.open()
|
||||
|
||||
if connection.server.tls is not None and connection.server.ssl is False:
|
||||
log.info("Issuing StartTLS command.")
|
||||
connection.start_tls()
|
||||
|
||||
log.info("Issuing BIND command.")
|
||||
connection.bind()
|
||||
|
||||
|
||||
def close_connection(connection):
|
||||
log.info("Closing connection to LDAP server.")
|
||||
log.info("Issuing UNBIND command.")
|
||||
connection.unbind()
|
|
@ -1,61 +0,0 @@
|
|||
from ldap3 import ALL_ATTRIBUTES, MODIFY_ADD
|
||||
from phi.ldap.utils import get_response, make_group_dict
|
||||
from phi.logging import get_logger
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
def get_group_by_cn(client, cn):
|
||||
log.info("Searching groups with common name: {}".format(cn))
|
||||
|
||||
dn = 'cn={},ou=Groups,{}'.format(cn, client.base_dn)
|
||||
log.debug("Search dn: {}".format(dn))
|
||||
|
||||
response_id = client.connection.search(
|
||||
dn, '(objectclass=groupOfNames)',
|
||||
search_scope='SUBTREE',
|
||||
attributes=[ALL_ATTRIBUTES]
|
||||
)
|
||||
|
||||
response = get_response(client, response_id)
|
||||
|
||||
if not response:
|
||||
return None
|
||||
|
||||
if len(response) > 1:
|
||||
log.error("Looking for exactly one result but server gave {}. "
|
||||
"Taking the first and ignoring the rest."
|
||||
.format(len(response)))
|
||||
|
||||
group = make_group_dict(client, response[0])
|
||||
return group
|
||||
|
||||
|
||||
def get_all_groups(client):
|
||||
log.info("Searching all the groups")
|
||||
dn = 'ou=Groups,{}'.format(client.base_dn)
|
||||
|
||||
log.debug("Search dn: {}".format(dn))
|
||||
|
||||
response_id = client.connection.search(
|
||||
dn, '(objectclass=groupOfNames)',
|
||||
search_scope='SUBTREE',
|
||||
attributes=[ALL_ATTRIBUTES]
|
||||
)
|
||||
|
||||
response = get_response(client, response_id)
|
||||
groups = [make_group_dict(client, entry) for entry in response]
|
||||
return groups
|
||||
|
||||
|
||||
def add_group_member(client, group, user):
|
||||
group_dn = group['dn']
|
||||
member_dn = user['dn']
|
||||
log.debug('Found adding {} to {}'.format(member_dn, group_dn))
|
||||
|
||||
response_id = client.connection.modify(
|
||||
group_dn,
|
||||
{'member': [(MODIFY_ADD, [member_dn])]}
|
||||
)
|
||||
|
||||
return get_response(client, response_id)
|
|
@ -1,77 +0,0 @@
|
|||
from ldap3 import ALL_ATTRIBUTES, HASHED_SALTED_SHA
|
||||
from ldap3.utils.hashed import hashed
|
||||
from phi.ldap.utils import get_response, make_user_dict, add_entry, delete_entry
|
||||
from phi.logging import get_logger
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
def get_user_by_uid(client, uid):
|
||||
log.info("Searching entry with identifier: {}".format(uid))
|
||||
|
||||
filter_ = "({}={})".format('uid', uid)
|
||||
log.debug("Search filter: {}".format(filter_))
|
||||
|
||||
response_id = client.connection.search(
|
||||
client.base_dn, filter_,
|
||||
search_scope='SUBTREE',
|
||||
attributes=[ALL_ATTRIBUTES]
|
||||
)
|
||||
|
||||
response = get_response(client, response_id)
|
||||
|
||||
if not response:
|
||||
return None
|
||||
|
||||
if len(response) > 1:
|
||||
log.error("Looking for exactly one result but server gave {}. "
|
||||
"Taking the first and ignoring the rest."
|
||||
.format(len(response)))
|
||||
|
||||
return make_user_dict(client, response[0])
|
||||
|
||||
|
||||
def get_all_users(client):
|
||||
log.info("Searching all the users")
|
||||
|
||||
dn = 'ou=Hackers,{}'.format(client.base_dn)
|
||||
log.debug("Search dn: {}".format(dn))
|
||||
|
||||
response_id = client.connection.search(
|
||||
dn, '(objectclass=person)',
|
||||
search_scope='SUBTREE',
|
||||
attributes=[ALL_ATTRIBUTES]
|
||||
)
|
||||
|
||||
response = get_response(client, response_id)
|
||||
|
||||
users = [make_user_dict(client, entry) for entry in response]
|
||||
return users
|
||||
|
||||
|
||||
def add_user(client, uid, cn, sn, mail, password):
|
||||
dn = 'uid={},ou=Hackers,{}'.format(uid, client.base_dn)
|
||||
hashed_password = hashed(HASHED_SALTED_SHA, password)
|
||||
|
||||
attributes={
|
||||
'objectClass': [
|
||||
'inetOrgPerson',
|
||||
'organizationalPerson',
|
||||
'person', 'top'
|
||||
],
|
||||
'cn': cn,
|
||||
'sn': sn,
|
||||
'mail': mail,
|
||||
'userPassword': hashed_password
|
||||
}
|
||||
|
||||
add_entry(client, dn, attributes)
|
||||
|
||||
|
||||
def delete_user(client, user):
|
||||
delete_entry(client, user['dn'])
|
||||
|
||||
|
||||
def delete_user_by_uid(client, uid):
|
||||
dn = 'uid={},ou=Hackers,{}'.format(uid, client.base_dn)
|
||||
delete_entry(client, dn)
|
|
@ -1,70 +0,0 @@
|
|||
import re
|
||||
from phi.logging import get_logger
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
def make_user_dict(client, entry):
|
||||
attributes = entry['attributes']
|
||||
|
||||
user = {}
|
||||
user['uid'] = attributes['uid'][0]
|
||||
user['dn'] = 'uid={},ou=Hackers,{}'.format(user['uid'], client.base_dn)
|
||||
user['cn'] = attributes['cn'][0]
|
||||
user['sn'] = attributes['sn'][0]
|
||||
user['mail'] = attributes['mail'][0]
|
||||
user['password'] = attributes['userPassword'][0]
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def get_uid_from_dn(client, dn):
|
||||
uid = re.search('uid=(.+?),ou=Hackers,{}'.format(client.base_dn),
|
||||
dn).group(1)
|
||||
return uid
|
||||
|
||||
|
||||
def make_group_dict(client, entry):
|
||||
attributes = entry['attributes']
|
||||
|
||||
cn = attributes['cn'][0]
|
||||
dn = 'cn={},ou=Groups,{}'.format(cn, client.base_dn)
|
||||
members = [get_uid_from_dn(client, u_dn)
|
||||
for u_dn in attributes['member']]
|
||||
|
||||
group = {}
|
||||
group['dn'] = dn
|
||||
group['cn'] = cn
|
||||
group['members'] = members
|
||||
|
||||
return group
|
||||
|
||||
|
||||
def get_response(client, response_id):
|
||||
response, result, request = client.connection.get_response(
|
||||
response_id, get_request=True
|
||||
)
|
||||
|
||||
log.debug("Request: {}".format(request))
|
||||
log.debug("Response: {}".format(response))
|
||||
log.debug("Result: {}".format(result))
|
||||
|
||||
if result['description'] is not 'success':
|
||||
raise Exception(result['description'])
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def add_entry(client, dn, attributes):
|
||||
log.info('Adding entry with distinguiscet name: {}'
|
||||
'and attributes {}'.format(dn, attributes))
|
||||
response_id = client.connection.add(dn, attributes=attributes)
|
||||
response = get_response(client, response_id)
|
||||
return response
|
||||
|
||||
|
||||
def delete_entry(client, dn):
|
||||
log.info('Deleting entry with distinguiscet name: {}')
|
||||
response_id = client.connection.delete(dn)
|
||||
response = get_response(client, response_id)
|
||||
return response
|
29
src/phi/security.py
Normal file
29
src/phi/security.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
from bonsai.ldapvaluelist import LDAPValueList
|
||||
from passlib.hash import (
|
||||
ldap_sha1,
|
||||
ldap_bcrypt,
|
||||
ldap_sha256_crypt,
|
||||
ldap_sha512_crypt,
|
||||
ldap_pbkdf2_sha256,
|
||||
ldap_pbkdf2_sha512,
|
||||
)
|
||||
|
||||
HASH_CALLABLE = {
|
||||
"sha1": ldap_sha1.hash,
|
||||
"bcrypt": ldap_bcrypt.hash,
|
||||
"sha256_crypt": ldap_sha256_crypt.hash,
|
||||
"sha512_crypt": ldap_sha512_crypt.hash,
|
||||
"pbkdf2_sha256": ldap_pbkdf2_sha256.hash,
|
||||
"pbkdf2_sha512": ldap_pbkdf2_sha512.hash,
|
||||
}
|
||||
|
||||
|
||||
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)
|
0
src/phi/web/__init__.py
Normal file
0
src/phi/web/__init__.py
Normal file
213
src/phi/web/app.py
Normal file
213
src/phi/web/app.py
Normal file
|
@ -0,0 +1,213 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
from aiohttp import web
|
||||
from aiohttp_session import setup
|
||||
from aiohttp_session.cookie_storage import EncryptedCookieStorage
|
||||
import click
|
||||
from pprint import pformat as pp
|
||||
import yaml
|
||||
|
||||
from phi.config import get_config, merge_config, extract_secret
|
||||
from phi.logging import setup_logging, get_logger
|
||||
from phi.api.app import api_app
|
||||
from phi.web.client_store import ClientStore
|
||||
from phi.web.login import login
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
LOGIN_ROUTE = "/login"
|
||||
COOKIE_NAME = "PHI_COOKIE"
|
||||
|
||||
|
||||
def setup_app(config):
|
||||
app = web.Application()
|
||||
setup(app, EncryptedCookieStorage(extract_secret(config), cookie_name=COOKIE_NAME))
|
||||
|
||||
app["config"] = config
|
||||
app["store"] = ClientStore(LOGIN_ROUTE)
|
||||
app["log"] = log
|
||||
|
||||
app.add_routes([web.post(LOGIN_ROUTE, login)])
|
||||
|
||||
api = api_app(app["store"])
|
||||
app.add_subapp("/api", api)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def run_app(app):
|
||||
web.run_app(
|
||||
app,
|
||||
host=app["config"]["core"]["listen"].get("host", "127.0.0.1"),
|
||||
port=app["config"]["core"]["listen"].get("port", "8080"),
|
||||
)
|
||||
|
||||
|
||||
@click.command(help="phid is the main application daemon.")
|
||||
@click.option(
|
||||
"--config",
|
||||
"config_path",
|
||||
type=click.Path(exists=True),
|
||||
help="Path to a valid config file.",
|
||||
)
|
||||
@click.option(
|
||||
"-H",
|
||||
"--host",
|
||||
type=click.STRING,
|
||||
default="localhost",
|
||||
help='Address to which the application bounds. Defaults to "localhost".',
|
||||
)
|
||||
@click.option(
|
||||
"-p",
|
||||
"--port",
|
||||
type=click.INT,
|
||||
default=8080,
|
||||
help="Port to which the application bounds. Defaults to 8080.",
|
||||
)
|
||||
@click.option(
|
||||
"--ldap-host",
|
||||
"ldap_host",
|
||||
type=click.STRING,
|
||||
default="localhost",
|
||||
help='Address of the LDAP server to connect to. Defaults to "localhost".',
|
||||
)
|
||||
@click.option(
|
||||
"--ldap-port",
|
||||
"ldap_port",
|
||||
type=click.INT,
|
||||
default=389,
|
||||
help="Port where is exposed the LDAP server to connect to. Defaults to 389.",
|
||||
)
|
||||
@click.option(
|
||||
"--ldap-crypt",
|
||||
"ldap_crypt",
|
||||
is_flag=True,
|
||||
default=True,
|
||||
help="Connect to the LDAP server using TLSv1.2. Defaults to True.",
|
||||
)
|
||||
@click.option(
|
||||
"--ldap-tls-do-not-validate",
|
||||
"ldap_tls_validate",
|
||||
is_flag=True,
|
||||
default=True,
|
||||
help="Toggle checking of TLS cert against the provided name. Defaults to True.",
|
||||
)
|
||||
@click.option(
|
||||
"--ldap-tls-ca",
|
||||
"ldap_tls_ca",
|
||||
type=click.Path(exists=True),
|
||||
help="Toggle checking of TLS cert against the provided name. Defaults to True.",
|
||||
)
|
||||
@click.option(
|
||||
"--ldap-base-dn", "ldap_base_dn", type=click.STRING, help="The LDAP base_dn to use."
|
||||
)
|
||||
@click.option(
|
||||
"--ldap-username",
|
||||
"ldap_username",
|
||||
type=click.STRING,
|
||||
help="The username to use to connect to the LDAP server.",
|
||||
)
|
||||
@click.option(
|
||||
"--ldap-password",
|
||||
"ldap_password",
|
||||
type=click.STRING,
|
||||
help="The password to use to connect to the LDAP server. "
|
||||
"THIS CAN BE READ BY OTHER PROCESSES. NEVER USE IN PRODUCTION!",
|
||||
)
|
||||
@click.option(
|
||||
"--log-conf",
|
||||
"log_conf",
|
||||
type=click.Path(exists=True),
|
||||
help="Path to a yaml configuration for the logger.",
|
||||
)
|
||||
@click.option(
|
||||
"--debug",
|
||||
"debug",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Set the log level to debug.",
|
||||
)
|
||||
def cli(
|
||||
host,
|
||||
port,
|
||||
config_path=None,
|
||||
ldap_host=None,
|
||||
ldap_port=None,
|
||||
ldap_crypt=True,
|
||||
ldap_tls_validate=True,
|
||||
ldap_tls_ca=None,
|
||||
ldap_base_dn=None,
|
||||
ldap_username=None,
|
||||
ldap_password=None,
|
||||
log_conf=None,
|
||||
debug=False,
|
||||
):
|
||||
cli_config = prepare_config_from_cli(
|
||||
host,
|
||||
port,
|
||||
ldap_host,
|
||||
ldap_port,
|
||||
ldap_crypt,
|
||||
ldap_tls_validate,
|
||||
ldap_tls_ca,
|
||||
ldap_base_dn,
|
||||
ldap_username,
|
||||
ldap_password,
|
||||
log_conf,
|
||||
debug,
|
||||
)
|
||||
config_file, file_config = get_config(config_path)
|
||||
log.debug(f"FILE: {pp(file_config)}")
|
||||
config = merge_config(cli_config, file_config)
|
||||
if debug:
|
||||
set_to_debug(config)
|
||||
# Beware that everything happened until now
|
||||
# could not possibly get logged.
|
||||
setup_logging(config.get("logging", {}))
|
||||
if config_file:
|
||||
log.debug("Config file found at: %s", config_file)
|
||||
log.debug("{}".format(pp(file_config)))
|
||||
log.debug("CLI config:\n{}".format(pp(cli_config)))
|
||||
log.info("Starting app with config:\n{}".format(pp(config)))
|
||||
|
||||
app = setup_app(config)
|
||||
run_app(app)
|
||||
|
||||
|
||||
def prepare_config_from_cli(
|
||||
host,
|
||||
port,
|
||||
ldap_host=None,
|
||||
ldap_port=None,
|
||||
ldap_crypt=True,
|
||||
ldap_tls_validate=True,
|
||||
ldap_tls_ca=None,
|
||||
ldap_base_dn=None,
|
||||
ldap_username=None,
|
||||
ldap_password=None,
|
||||
log_conf=None,
|
||||
debug=False,
|
||||
):
|
||||
_core = {"listen": {"host": host, "port": port}}
|
||||
_ldap = {
|
||||
"host": ldap_host,
|
||||
"port": ldap_port,
|
||||
"encryption": "TLSv1.2" if ldap_crypt else None,
|
||||
"validate": ldap_tls_validate,
|
||||
"ca_certs": ldap_tls_ca,
|
||||
"username": ldap_username,
|
||||
"password": ldap_password,
|
||||
"base_dn": ldap_base_dn,
|
||||
}
|
||||
_logging = {}
|
||||
if log_conf:
|
||||
with open(log_conf) as log_conf_fd:
|
||||
_logging = yaml.safe_load(log_conf_fd)
|
||||
|
||||
return {"core": _core, "ldap": _ldap, "logging": _logging}
|
||||
|
||||
|
||||
def set_to_debug(conf):
|
||||
for logger, log_conf in conf["logging"]["loggers"].items():
|
||||
log_conf["level"] = "DEBUG"
|
||||
conf["logging"]["loggers"][logger] = log_conf
|
23
src/phi/web/auth_middleware.py
Normal file
23
src/phi/web/auth_middleware.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
from aiohttp.web import middleware, HTTPFound
|
||||
|
||||
from phi.web import app
|
||||
from phi.async_ldap.model import Hackers, Robots, Roles
|
||||
|
||||
|
||||
@middleware
|
||||
async def authenticated(request, handler):
|
||||
try:
|
||||
store = request.app["store"]
|
||||
except KeyError:
|
||||
raise HTTPFound(app.LOGIN_ROUTE)
|
||||
|
||||
client = await store.get_client(request)
|
||||
|
||||
request.app["ldap_client"] = client
|
||||
request.app["users"] = Hackers(client)
|
||||
request.app["services"] = Robots(client)
|
||||
request.app["groups"] = Roles(client)
|
||||
resp = await handler(request)
|
||||
|
||||
return resp
|
35
src/phi/web/client_store.py
Normal file
35
src/phi/web/client_store.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
import secrets
|
||||
|
||||
from aiohttp.web import HTTPFound
|
||||
from aiohttp_session import get_session
|
||||
|
||||
|
||||
class ClientStore(dict):
|
||||
"""
|
||||
This class is responsible to hold the clients used by the active connections.
|
||||
"""
|
||||
|
||||
def __init__(self, login_route):
|
||||
self.login_route = login_route
|
||||
self.store = dict()
|
||||
|
||||
async def get_client(self, request):
|
||||
session = await get_session(request)
|
||||
client_id = session.get("client_id")
|
||||
if client_id is None:
|
||||
raise HTTPFound(self.login_route)
|
||||
|
||||
client = self.store.get(client_id)
|
||||
if client is None:
|
||||
raise HTTPFound(self.login_route)
|
||||
|
||||
return client
|
||||
|
||||
async def set_client(self, request, client):
|
||||
session = await get_session(request)
|
||||
|
||||
client_id = secrets.token_hex(16)
|
||||
|
||||
self.store[client_id] = client
|
||||
session["client_id"] = client_id
|
38
src/phi/web/login.py
Normal file
38
src/phi/web/login.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
from aiohttp.web import HTTPBadRequest, HTTPOk
|
||||
|
||||
from phi.async_ldap.client import AsyncClient
|
||||
|
||||
|
||||
async def login(request):
|
||||
log = request.app["log"]
|
||||
log.debug("login")
|
||||
|
||||
store = request.app["store"]
|
||||
config = request.app["config"]
|
||||
|
||||
body = await request.json()
|
||||
|
||||
tag = body.get("tag", "uid")
|
||||
|
||||
ou = body.get("ou")
|
||||
if ou is not None:
|
||||
config["ldap"]["ou"] = ou
|
||||
|
||||
try:
|
||||
user = body["user"]
|
||||
password = body["password"]
|
||||
except KeyError as e:
|
||||
text = f"Missing key: {e}"
|
||||
log.warn(text)
|
||||
raise HTTPBadRequest(text=text)
|
||||
|
||||
client = AsyncClient(
|
||||
attribute_id=tag, username=user, password=password, **config.get("ldap")
|
||||
)
|
||||
|
||||
log.debug(f"Client: {client}")
|
||||
|
||||
await store.set_client(request, client)
|
||||
|
||||
raise HTTPOk
|
130
src/phicli
130
src/phicli
|
@ -1,130 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
from pprint import pformat as pp
|
||||
from getpass import getpass
|
||||
|
||||
from phi.config import get_config
|
||||
from phi.logging import setup_logging, get_logger
|
||||
from phi import cli
|
||||
import phi.ldap.client
|
||||
from phi.ldap.user import get_user_by_uid, add_user, delete_user
|
||||
from phi.ldap.group import get_group_by_cn, get_all_groups, add_group_member
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
@cli.register('dispaly user fields', ['user identifier'])
|
||||
def showuser(uid):
|
||||
user = get_user_by_uid(client, uid)
|
||||
if user is None:
|
||||
print('User {} not found'.format(uid))
|
||||
return
|
||||
|
||||
print(pp(user))
|
||||
|
||||
|
||||
@cli.register('add a new user', ['user identifier'])
|
||||
def adduser(uid):
|
||||
def ask(prompt, default):
|
||||
full_prompt = '{} [{}] '.format(prompt, default)
|
||||
return input(full_prompt) or default
|
||||
|
||||
user = get_user_by_uid(client, uid)
|
||||
if user is not None:
|
||||
print("User {} already existing".format(uid))
|
||||
return
|
||||
|
||||
cn = ask('Common name:', uid)
|
||||
sn = ask('Last name:', uid)
|
||||
mail = ask('Mail:', '{}@localhost'.format(uid))
|
||||
|
||||
password = getpass()
|
||||
pass_check = getpass('Retype password: ')
|
||||
if password != pass_check:
|
||||
print('Password not matching')
|
||||
return
|
||||
|
||||
add_user(client, uid, cn, sn, mail, password)
|
||||
|
||||
# Check
|
||||
user = get_user_by_uid(client, uid)
|
||||
print()
|
||||
print(pp(user))
|
||||
|
||||
|
||||
@cli.register('delete an user', ['user identifier'])
|
||||
def deluser(uid):
|
||||
check = input('Are you sure? [y/N] ') or 'N'
|
||||
if check.lower() != 'y':
|
||||
print('Ok then')
|
||||
return
|
||||
|
||||
user = get_user_by_uid(client, uid)
|
||||
if user is not None:
|
||||
delete_user(client, user)
|
||||
print('Done')
|
||||
else:
|
||||
print('User {} not found'.format(uid))
|
||||
|
||||
|
||||
@cli.register('show a group', ['group common name'])
|
||||
def showgroup(cn):
|
||||
group = get_group_by_cn(client, cn)
|
||||
if group is None:
|
||||
print('Group {} not found'.format(gcn))
|
||||
return
|
||||
|
||||
print(pp(group))
|
||||
|
||||
|
||||
@cli.register('list all groups')
|
||||
def listgroups():
|
||||
groups = get_all_groups(client)
|
||||
|
||||
for group in groups:
|
||||
print(group['cn'])
|
||||
|
||||
|
||||
@cli.register('add an user to a group',
|
||||
['user identifier', 'group common name'])
|
||||
def addtogroup(uid, gcn):
|
||||
user = get_user_by_uid(client, uid)
|
||||
group = get_group_by_cn(client, gcn)
|
||||
|
||||
if user is None:
|
||||
print('User {} not found'.format(uid))
|
||||
return
|
||||
|
||||
if group is None:
|
||||
print('Group {} not found'.format(gcn))
|
||||
return
|
||||
|
||||
if uid in group['members']:
|
||||
print('User {} is already in group {}'.format(uid, gcn))
|
||||
return
|
||||
|
||||
add_group_member(client, group, user)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli.add_arg('--config', 'config.yml', 'custom configuration file')
|
||||
args = cli.get_args()
|
||||
|
||||
config_file = args['config']
|
||||
|
||||
config_file, config = get_config(config_file)
|
||||
setup_logging(config.get('logging', {}))
|
||||
log.info("Using configuration at '{}':\n{}"
|
||||
.format(config_file, pp(config)))
|
||||
|
||||
# TODO: check fields in config
|
||||
client = phi.ldap.client.Client(**config['ldap'])
|
||||
|
||||
log.info('Opening LDAP client')
|
||||
client.open()
|
||||
|
||||
log.info('Arguments: {}'.format(pp(args)))
|
||||
|
||||
cli.run(args)
|
||||
|
||||
log.info('Closing LDAP client')
|
||||
client.close()
|
22
src/phid
22
src/phid
|
@ -1,22 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
from pprint import pformat as pp
|
||||
|
||||
from phi.config import get_config
|
||||
from phi.logging import setup_logging, get_logger
|
||||
from phi.app import setup_app, run_app
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
config_file, config = get_config()
|
||||
|
||||
# Beware that everything happened until now
|
||||
# could not possibly get logged.
|
||||
setup_logging(config.get('logging', {}))
|
||||
|
||||
log.info("Found configuration at '{}':\n{}"
|
||||
.format(config_file, pp(config)))
|
||||
|
||||
app = setup_app(config)
|
||||
run_app(app)
|
159
test/aux_async_model.py
Normal file
159
test/aux_async_model.py
Normal file
|
@ -0,0 +1,159 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import asyncio
|
||||
from pprint import pprint as pp
|
||||
|
||||
from phi.async_ldap.model import (
|
||||
Hackers,
|
||||
User,
|
||||
Robots,
|
||||
Service,
|
||||
Group,
|
||||
Roles,
|
||||
)
|
||||
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):
|
||||
return [el async for el in build_heritage(h, cls)]
|
||||
|
||||
|
||||
cl = AsyncClient(
|
||||
"ldap://localhost",
|
||||
port=389,
|
||||
encryption=True,
|
||||
# validate=True,
|
||||
ca_cert="/home/leo/Documents/coding/phi/openldap/cert.pem",
|
||||
username="root",
|
||||
password="root",
|
||||
base_dn="dc=unit,dc=macaomilano,dc=org",
|
||||
attribute_id="cn",
|
||||
)
|
||||
|
||||
|
||||
async def get_all_children():
|
||||
h = Hackers(cl)
|
||||
r = Robots(cl)
|
||||
g = Roles(cl)
|
||||
|
||||
hackers = await dlv(h, User)
|
||||
robots = await dlv(r, Service)
|
||||
groups = await dlv(g, Group)
|
||||
|
||||
return (hackers, robots, groups)
|
||||
|
||||
|
||||
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):
|
||||
return await obj.describe()
|
||||
|
||||
|
||||
async def _await(awaitable):
|
||||
return await awaitable
|
||||
|
||||
|
||||
def sync_await(awaitable):
|
||||
return asyncio.run(_await(awaitable))
|
||||
|
||||
|
||||
h = Hackers(cl)
|
||||
r = Robots(cl)
|
||||
c = Roles(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("Roles 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("Roles members:", get_members(c)))
|
||||
asyncio.run(
|
||||
add_new(Group, "naughty", member=[User(cl, "conte_mascetti"), User(cl, "necchi")])
|
||||
)
|
||||
asyncio.run(print_async("Roles members:", get_members(c)))
|
||||
asyncio.run(safe_delete(Group(cl, "naughty")))
|
||||
asyncio.run(print_async("Roles members:", get_members(c)))
|
||||
asyncio.run(
|
||||
add_new(Group, "naughty", member=[User(cl, "conte_mascetti"), User(cl, "necchi")])
|
||||
)
|
||||
asyncio.run(print_async("Roles 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()])
|
|
@ -1,6 +1,6 @@
|
|||
import pytest
|
||||
import phi.ldap.client
|
||||
# import phi.api.app
|
||||
import phi.api.app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -9,27 +9,26 @@ def ldap_client():
|
|||
host='localhost', port=389,
|
||||
encryption='TLSv1.2', ciphers='HIGH',
|
||||
validate=False,
|
||||
# username='uid=phi,ou=Services,dc=unit,dc=macaomilano,dc=org',
|
||||
# password='phi',
|
||||
username='cn=root,dc=unit,dc=macaomilano,dc=org',
|
||||
password='root',
|
||||
base_dn='dc=unit,dc=macaomilano,dc=org')
|
||||
username='uid=phi,ou=Services,dc=unit,dc=macaomilano,dc=org',
|
||||
password='phi',
|
||||
base_dn='dc=unit,dc=macaomilano,dc=org',
|
||||
attribute_id='uid', attribute_mail='mail')
|
||||
client.open()
|
||||
yield client
|
||||
client.close()
|
||||
|
||||
|
||||
# @pytest.fixture
|
||||
# def api_app():
|
||||
# return phi.api.app.api_app({
|
||||
# 'ldap': {
|
||||
# 'host': 'localhost',
|
||||
# 'port': 389,
|
||||
# 'encryption': 'TLSv1.2',
|
||||
# 'ciphers': 'HIGH',
|
||||
# 'validate': 'False',
|
||||
# 'username': 'uid=phi,ou=Services,dc=unit,dc=macaomilano,dc=org',
|
||||
# 'password': 'phi',
|
||||
# 'base_dn': 'dc=unit,dc=macaomilano,dc=org',
|
||||
# 'attribute_id': 'uid',
|
||||
# 'attribute_mail': 'mail'}})
|
||||
@pytest.fixture
|
||||
def api_app():
|
||||
return phi.api.app.api_app({
|
||||
'ldap': {
|
||||
'host': 'localhost',
|
||||
'port': 389,
|
||||
'encryption': 'TLSv1.2',
|
||||
'ciphers': 'HIGH',
|
||||
'validate': 'False',
|
||||
'username': 'uid=phi,ou=Services,dc=unit,dc=macaomilano,dc=org',
|
||||
'password': 'phi',
|
||||
'base_dn': 'dc=unit,dc=macaomilano,dc=org',
|
||||
'attribute_id': 'uid',
|
||||
'attribute_mail': 'mail'}})
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
# async def test_user_request_not_valid(test_client, api_app):
|
||||
# client = await test_client(api_app)
|
||||
async def test_user_request_not_valid(test_client, api_app):
|
||||
client = await test_client(api_app)
|
||||
|
||||
# resp = await client.get('/user')
|
||||
# assert resp.status == 422
|
||||
# resp = None
|
||||
resp = await client.get('/user')
|
||||
assert resp.status == 422
|
||||
resp = None
|
||||
|
||||
# resp = await client.get('/user/')
|
||||
# assert resp.status == 422
|
||||
resp = await client.get('/user/')
|
||||
assert resp.status == 422
|
||||
|
||||
|
||||
# async def test_user_not_found(test_client, api_app):
|
||||
# client = await test_client(api_app)
|
||||
# resp = await client.get('/user/nonexistent')
|
||||
# assert resp.status == 404
|
||||
async def test_user_not_found(test_client, api_app):
|
||||
client = await test_client(api_app)
|
||||
resp = await client.get('/user/nonexistent')
|
||||
assert resp.status == 404
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
from phi.ldap.user import get_user_by_uid, get_all_users, \
|
||||
add_user, delete_user_by_uid, delete_user
|
||||
|
||||
from phi.ldap.group import add_group_member, get_group_by_cn, get_all_groups
|
||||
from phi.ldap.user import get_user_by_uid
|
||||
|
||||
|
||||
def test_connection(ldap_client):
|
||||
|
@ -10,90 +7,6 @@ def test_connection(ldap_client):
|
|||
|
||||
|
||||
def test_get_user_by_id(ldap_client):
|
||||
user = get_user_by_uid(ldap_client, 'conte_mascetti')
|
||||
assert user['uid'] == 'conte_mascetti'
|
||||
assert user['mail'] == 'rmascetti@autistici.org'
|
||||
|
||||
|
||||
def test_get_all_users(ldap_client):
|
||||
users = get_all_users(ldap_client)
|
||||
# print(users)
|
||||
assert 'conte_mascetti' in [u['uid'] for u in users]
|
||||
|
||||
|
||||
def test_add_delete_user(ldap_client):
|
||||
uid = 'rosa_rossi'
|
||||
cn = 'Rosa'
|
||||
sn = 'Rossi'
|
||||
mail = 'foo@autistici.org'
|
||||
password = 'changeme'
|
||||
|
||||
add_user(ldap_client, uid, cn, sn, mail, password)
|
||||
|
||||
user = get_user_by_uid(ldap_client, uid)
|
||||
assert user['uid'] == uid
|
||||
assert user['mail'] == mail
|
||||
|
||||
delete_user(ldap_client, user)
|
||||
# print(user)
|
||||
|
||||
user = get_user_by_uid(ldap_client, uid)
|
||||
assert user is None
|
||||
|
||||
|
||||
def test_failing_add_user(ldap_client):
|
||||
uid = 'conte_mascetti'
|
||||
|
||||
try:
|
||||
add_user(ldap_client, uid, 'name', 'surname', 'mail', 'pass')
|
||||
except: # User alrady existing
|
||||
pass
|
||||
else:
|
||||
assert False
|
||||
|
||||
def test_failing_delete_user(ldap_client):
|
||||
uid = 'rosa_rossi'
|
||||
|
||||
try:
|
||||
delete_user_by_uid(ldap_client, uid)
|
||||
except: # User already not existing
|
||||
pass
|
||||
else:
|
||||
assert False
|
||||
|
||||
|
||||
def test_get_all_groups(ldap_client):
|
||||
groups = get_all_groups(ldap_client)
|
||||
|
||||
cns = [g['cn'] for g in groups]
|
||||
assert 'WikiUsers' in cns
|
||||
|
||||
|
||||
def test_add_to_group(ldap_client):
|
||||
client = ldap_client
|
||||
|
||||
group_cn = 'WikiUsers'
|
||||
member_uid = 'rosa_rossi'
|
||||
add_user(client, member_uid, 'name', 'surname', 'mail', 'pass')
|
||||
|
||||
user = get_user_by_uid(client, member_uid)
|
||||
# print(user)
|
||||
|
||||
group = get_group_by_cn(client, group_cn)
|
||||
group_members = group['members']
|
||||
|
||||
assert len(group_members) == 1
|
||||
# print(group_members)
|
||||
|
||||
add_group_member(client, group, user)
|
||||
|
||||
group = get_group_by_cn(client, group_cn)
|
||||
group_members = group['members']
|
||||
|
||||
assert len(group_members) == 2
|
||||
assert user['uid'] in group_members
|
||||
|
||||
# print(group_members)
|
||||
# print(user)
|
||||
|
||||
delete_user(client, user)
|
||||
entry = get_user_by_uid(ldap_client, 'conte_mascetti')
|
||||
assert entry['uid'] == 'conte_mascetti'
|
||||
assert entry['mail'] == 'rmascetti@autistici.org'
|
||||
|
|
Loading…
Reference in New Issue
Block a user