Compare commits

...

64 Commits

Author SHA1 Message Date
c5a4b86349
Tons of new tests and new_model refinement 2020-12-27 19:48:57 +01:00
b0e5d00994
Update dev-related files 2020-12-27 19:48:25 +01:00
401f01f110
Minor fix 2020-12-27 19:48:02 +01:00
30c46c059a
Rename test module 2020-12-27 19:47:28 +01:00
b400127cc6
Fix bugs (thanks to integration tests) 2020-11-30 19:16:36 +01:00
6bd1beba9e
Add some integration tests 2020-11-30 19:16:10 +01:00
e202d54a7d
Add pytest-integration 2020-11-27 19:42:00 +01:00
67c7830ba4
Add and improve new_model unit tests 2020-11-27 19:41:45 +01:00
95bc52ebd8
Add docs and clarify 2020-11-27 19:41:09 +01:00
ac0ab02d6e
Massive code update 2020-11-11 22:25:02 +01:00
d024232d66
Add logs target in openldap Makefile 2020-11-11 22:24:11 +01:00
166b542846
Set 88 chars per line 2020-11-11 22:23:30 +01:00
d87c89a075
Update Pipfile 2020-11-11 22:23:10 +01:00
b7316ff513
Refactor helper script 2020-08-31 11:02:03 +02:00
67c83975d1
Switch to mixins to build async_ldap model classes 2020-08-31 11:01:46 +02:00
205c87dc49
Update setup.py and Pipfile 2020-08-31 10:11:20 +02:00
c05b023bdb
Use async model in api 2020-08-29 20:15:02 +02:00
1b97d6d7ca
Refactor describe func in model 2020-08-29 20:14:20 +02:00
b979002f78
Black'd 2020-08-29 20:13:14 +02:00
e41e03f464
Move singleton logic in function 2020-08-29 20:10:56 +02:00
d9a6db63d7
Add description 2020-08-29 20:07:56 +02:00
6fea75022f
Refactor config logic for bools
If a config paramenter is a boolean, the resulting configuration is the
logic and of all those given (including defaults). BEWARE in future
config implementations!
2020-08-29 20:06:08 +02:00
12f37b3a55
Move custom exceptions in own module 2020-08-29 20:05:15 +02:00
a0cf7e9603
Silence mypy 2020-08-29 20:03:59 +02:00
8779db9ca0
Stub methods in async client 2020-08-29 19:42:24 +02:00
e454fbd84a
Add sync method and method stubs to Service 2020-08-29 19:41:34 +02:00
66885641c4
Add test and stub test 2020-08-29 19:40:06 +02:00
a22f459915
Clean harder 2020-08-23 21:32:20 +02:00
08c45b54f2 Do not verify cert in dev container 2020-08-23 21:31:23 +02:00
2bc7f0b75f
Improve error verification in async_model tests. 2019-07-19 14:29:46 +02:00
d9e8eb23e3
Tests for Service create method. 2019-07-18 18:15:43 +02:00
ce4c085e3f
User modify_password and verify_password. Failing Service tests. 2019-07-13 16:13:50 +02:00
7634f0f530
Better logging style and improved tests. 2019-07-13 11:40:35 +02:00
79f682cbb7
Password now hashed in app. 2019-07-13 11:39:06 +02:00
2b46eb0353
Renamed exceptions. 2019-07-13 11:35:03 +02:00
fed48022af
Improve tests and added tests for sync, modify and remove. 2019-07-06 21:27:00 +02:00
3454c194b1
Test for create_new_. 2019-07-06 21:26:21 +02:00
f05fe8a0d5
Improve User remove. 2019-07-06 21:25:23 +02:00
ec472218c4
Manage append and replace User modify. 2019-07-06 21:25:01 +02:00
79d7dcc653
Improve User sync. 2019-07-06 21:16:06 +02:00
fe3450b886
Tests for name property. 2019-07-01 22:48:04 +02:00
f558492975
Improved singletonic models. 2019-07-01 22:37:56 +02:00
706f109faf
Create, sync, modify and remove User. 2019-06-30 21:26:27 +02:00
0f7882a387
Search user by attr and by uid. 2019-06-30 21:24:55 +02:00
1be1aac9d0
Hacker, Robots, Congregations now singletons. 2019-06-30 21:23:11 +02:00
7e6b757e3a
Style. 2019-06-30 21:19:04 +02:00
ed8af40392
Tests on async part moved. 2019-06-30 21:16:48 +02:00
2cf07d6732
Moved ldap async modules to dedicated subpkg. 2019-05-04 17:59:21 +02:00
8dce6566ee
Tests for full coverage of async_model. 2019-05-01 15:34:52 +02:00
b466bf8ed2
Leafs are singletons, bound to the client object. 2019-05-01 15:34:46 +02:00
d0cba75ee0
Entry and OU are async iterators that do not deplete. 2019-05-01 15:34:41 +02:00
fd729170d3
Entry and OU shall not be singletons. 2019-05-01 15:34:36 +02:00
c4046a83ff
Adding auxiliary test module. 2019-05-01 15:34:31 +02:00
4e2cadaa92
Extending model. 2019-05-01 15:34:26 +02:00
b1837f80e4
Adding AsyncClient. 2019-05-01 15:34:21 +02:00
27fc927254
Make the init.ldif more sound to the real use case. 2019-04-28 12:31:52 +02:00
a434ff9b4c
Adding bonsai==1.1.0 2019-04-28 12:31:19 +02:00
422d238fc1
Ignore cpy* venv. 2019-04-28 12:30:17 +02:00
b0f312284d
Change cli bool flag and fix ldap connection. 2019-04-28 12:26:20 +02:00
543416368b
Updating Pipfile and lock. 2019-04-19 16:58:44 +02:00
bdd4a39531
Updating test dependencies. 2019-04-19 16:58:28 +02:00
e67b97b214
Added async_model and tests. 2019-04-19 16:57:54 +02:00
c8eb5c2dd4
Typo. 2019-04-15 20:12:51 +02:00
80fb51f7de
Refactor entrypoint to use click. 2019-04-15 18:27:22 +02:00
31 changed files with 4044 additions and 166 deletions

3
.gitignore vendored
View File

@ -90,6 +90,9 @@ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
# Personal local venv standard ("*" matches the version number)
/cpy*
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject
.spyproject .spyproject

22
Pipfile
View File

@ -4,9 +4,25 @@ url = "https://pypi.org/simple"
verify_ssl = true verify_ssl = true
[dev-packages] [dev-packages]
ipython = "*"
pytest = "*"
ipdb = "*"
async-generator = "*"
pytest-cov = "*"
pytest-asyncio = "*"
pytest-aiohttp = "*"
mock = "*"
yarl = {editable = true, path = "."}
pytest-integration = "*"
[packages] [packages]
phi = {editable = true,path = "."} phi = {editable = true,path = "."}
aiohttp = "2.3.8"
[requires] click = "7.0"
python_version = "3.7" pyYAML = "*"
ldap3 = "*"
bonsai = "==1.2.0"
passlib = "==1.7.1"
bcrypt = "==3.1.7"
multidict = "*"
iniconfig = "*"

903
Pipfile.lock generated
View File

@ -1,12 +1,10 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "0fe9733eafff036659c506639ac835b824b653cd2f121c82f2726ba2ea6bdb06" "sha256": "38260864a710721e0938c9510ecf6eaf63d7cf1f31f445ce28be2cabe93a5888"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {},
"python_version": "3.7"
},
"sources": [ "sources": [
{ {
"name": "pypi", "name": "pypi",
@ -35,6 +33,7 @@
"sha256:c7b8a47315e8c0b79a008e33eca4299baa002efd4b53ace068f0894133a8933e", "sha256:c7b8a47315e8c0b79a008e33eca4299baa002efd4b53ace068f0894133a8933e",
"sha256:e81997b2fb4b7f19a80257aa2bb6e35e521d62dcf595599bf34886b115607bad" "sha256:e81997b2fb4b7f19a80257aa2bb6e35e521d62dcf595599bf34886b115607bad"
], ],
"index": "pypi",
"version": "==2.3.8" "version": "==2.3.8"
}, },
"async-timeout": { "async-timeout": {
@ -42,62 +41,184 @@
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
], ],
"markers": "python_full_version >= '3.5.3'",
"version": "==3.0.1" "version": "==3.0.1"
}, },
"bcrypt": {
"hashes": [
"sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89",
"sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42",
"sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294",
"sha256:436a487dec749bca7e6e72498a75a5fa2433bda13bac91d023e18df9089ae0b8",
"sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161",
"sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752",
"sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31",
"sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5",
"sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c",
"sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0",
"sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de",
"sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e",
"sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052",
"sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09",
"sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105",
"sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133",
"sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1",
"sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7",
"sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc"
],
"index": "pypi",
"version": "==3.1.7"
},
"bonsai": {
"hashes": [
"sha256:1f8c8bb3ae9cd514a046aa6c7193ee3b67993a0f27b28a9fe4b9088fa3782e23",
"sha256:220002bd80722ee45eaa802c87b411160b8ae5bd792d6e7aaeecbeb92b56ebb3",
"sha256:2df864cdda584d0902f4ddb3184bb67cbb6b27a1572b8eeb42d0ab89c229bb3c",
"sha256:42477259176ffcc4ecc3e2195fbaaee29ad5d73d17c2b1bda4d91b26bcb17075",
"sha256:44de82623b765f2bb967a91c3d3a48ca578c4090682a508b9533e144575a2f2c",
"sha256:45bb556e33ff57466e7e70a137afc984231b79edb7df2d4a65210d1c4f53bd0a",
"sha256:482c5ba1db3dc87daa6b8fa792269060db20115a37bcd84412b75c7c2b780132",
"sha256:83dde90bbe3f5957c6f987c0aceffd606ce14f494bfb56be2ddb633ae311ebdb",
"sha256:86dad31ad4260d7812832b59a75872602960739ed77961dcd89d7112b5278b67",
"sha256:91585832da3e465b46d3690091872c94239bb12a2db72c59265c4333bb272de0",
"sha256:915ed6326f5c0baf5942534d874fd9f8063e689ec681af24815f523d7434f093",
"sha256:a2bcbb02b5483a32739541f72f750a17bc28467eca1a286604b9b80bf8309225",
"sha256:e9c116ed3e553fb46ecd1cb3c42799e395d517ad044a1988614556510c1ff229",
"sha256:fd9a3d4c7ff668cdd04b8e68d67e186e7124e2262da149c3dbc78d1aa0a16b04"
],
"index": "pypi",
"version": "==1.2.0"
},
"cffi": {
"hashes": [
"sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e",
"sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d",
"sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a",
"sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec",
"sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362",
"sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668",
"sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c",
"sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b",
"sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06",
"sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698",
"sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2",
"sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c",
"sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7",
"sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009",
"sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03",
"sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b",
"sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909",
"sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53",
"sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35",
"sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26",
"sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b",
"sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01",
"sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb",
"sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293",
"sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd",
"sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d",
"sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3",
"sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d",
"sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e",
"sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca",
"sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d",
"sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775",
"sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375",
"sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b",
"sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b",
"sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f"
],
"version": "==1.14.4"
},
"chardet": { "chardet": {
"hashes": [ "hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
], ],
"version": "==3.0.4" "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.0.0"
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"index": "pypi",
"version": "==7.0"
}, },
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
], ],
"version": "==2.8" "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.10"
},
"iniconfig": {
"editable": true,
"path": "."
}, },
"ldap3": { "ldap3": {
"hashes": [ "hashes": [
"sha256:3f67c83185b1f0df8fdf6b52fa42c55bc9e9b7120c8b7fec60f0d6003c536d18", "sha256:10bdd23b612e942ce90ea4dbc744dfd88735949833e46c5467a2dcf68e60f469",
"sha256:dd9be8ea27773c4ffc18ede0b95c3ca1eb12513a184590b9f8ae423db3f71eb9" "sha256:37d633e20fa360c302b1263c96fe932d40622d0119f1bddcb829b03462eeeeb7",
"sha256:7c3738570766f5e5e74a56fade15470f339d5c436d821cf476ef27da0a4de8b0",
"sha256:8f59a7b5399555b22db06f153daa76c77ded2dd84bc0f0ffe5b0b33901b6eac4",
"sha256:bed71c6ce2f70a00a330eed0c8370664c065239d45bcbe1b82517b6f6eed7f25"
], ],
"version": "==2.5.2" "index": "pypi",
"version": "==2.8.1"
}, },
"multidict": { "multidict": {
"hashes": [ "hashes": [
"sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f", "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a",
"sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3", "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93",
"sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef", "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632",
"sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b", "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656",
"sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73", "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79",
"sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc", "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7",
"sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3", "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d",
"sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd", "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5",
"sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351", "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224",
"sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941", "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26",
"sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d", "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea",
"sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1", "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348",
"sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b", "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6",
"sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a", "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76",
"sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3", "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1",
"sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7", "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f",
"sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0", "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952",
"sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0", "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a",
"sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014", "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37",
"sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5", "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9",
"sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036", "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359",
"sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d", "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8",
"sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a", "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da",
"sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce", "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3",
"sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1", "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d",
"sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a", "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf",
"sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9", "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841",
"sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7", "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d",
"sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b" "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93",
"sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f",
"sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647",
"sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635",
"sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456",
"sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda",
"sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5",
"sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281",
"sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"
], ],
"version": "==4.5.2" "index": "pypi",
"version": "==5.1.0"
},
"passlib": {
"hashes": [
"sha256:3d948f64138c25633613f303bcc471126eae67c04d5e3f6b7b8ce6242f8653e0",
"sha256:43526aea08fa32c6b6dbbbe9963c4c767285b78147b7437597f992812f69d280"
],
"index": "pypi",
"version": "==1.7.1"
}, },
"phi": { "phi": {
"editable": true, "editable": true,
@ -105,43 +226,683 @@
}, },
"pyasn1": { "pyasn1": {
"hashes": [ "hashes": [
"sha256:da2420fe13a9452d8ae97a0e478adde1dee153b11ba832a95b223a2ba01c10f7", "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
"sha256:da6b43a8c9ae93bc80e2739efb38cc776ba74a886e3e9318d65fe81a8b8a2c6e" "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
"sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
], ],
"version": "==0.4.5" "version": "==0.4.8"
},
"pycparser": {
"hashes": [
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.20"
},
"pytest-runner": {
"hashes": [
"sha256:5534b08b133ef9a5e2c22c7886a8f8508c95bb0b0bdc6cc13214f269c3c70d51",
"sha256:96c7e73ead7b93e388c5d614770d2bae6526efd997757d3543fe17b557a0942b"
],
"markers": "python_version >= '2.7'",
"version": "==5.2"
}, },
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
"sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
"sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
"sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
"sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e",
"sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
"sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
"sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
"sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
"sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
"sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a",
"sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
"sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
"sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
], ],
"version": "==3.13" "index": "pypi",
"version": "==5.3.1"
},
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0"
}, },
"yarl": { "yarl": {
"hashes": [ "hashes": [
"sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9", "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e",
"sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f", "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434",
"sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb", "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366",
"sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320", "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3",
"sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842", "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec",
"sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0", "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959",
"sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829", "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e",
"sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310", "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c",
"sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4", "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6",
"sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8", "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a",
"sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1" "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6",
"sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424",
"sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e",
"sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f",
"sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50",
"sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2",
"sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc",
"sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4",
"sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970",
"sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10",
"sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0",
"sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406",
"sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896",
"sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643",
"sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721",
"sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478",
"sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724",
"sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e",
"sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8",
"sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96",
"sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25",
"sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76",
"sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2",
"sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2",
"sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c",
"sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a",
"sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"
], ],
"version": "==1.3.0" "markers": "python_version >= '3.6'",
"version": "==1.6.3"
} }
}, },
"develop": {} "develop": {
"aiohttp": {
"hashes": [
"sha256:02f77d7d5467d71144a07c5e817a81ef5ccb005cbbba61c2f207b60db320e716",
"sha256:335c1ac277784eb6ba3d3b4e6901b06df3c5c93926026c594c4fdb61e270542d",
"sha256:37b62fa71369f2d6c9828d427b9a22829d603943a915fab216a99fc98447342b",
"sha256:48a798891ec157049f6b02e8ed1909e09c35aa03a29a90006215b8fb936185c4",
"sha256:536e9b6d1f8c1ebc44af530879c41fa0204369b5d2257bd1995534faacefad92",
"sha256:56fd240c9eb3bc09c081ca2e5b677be433ac84657b13ac29bc8deb16ba0b4f0b",
"sha256:6a12fa57d0608da29060c6fac531d049317422eecb23f509f8fae5b9a24c5b79",
"sha256:709ef5e6172b58cadb6575509baf7dacd97504911b54a654ab74f728f764249d",
"sha256:862c68d1237b811915a2614e996a3d60ed8d167e1e66e0b6c031a4aace5cf67f",
"sha256:8b2c9b1f43e12fe8837455ce4caf3a8621c3ab474219f11bd0270ca1c0735fc7",
"sha256:9ca64837c9a6d66543c1b49e6f04132ef9f2683377fe28f2497b7152e9d3c312",
"sha256:afec8fd2a464f8837cf019a26c52271d65f72512ae8ef2c05217086230fa50f1",
"sha256:b67b4238498225db921911f873e4b6959d51de55d7b71ced8e902aed9d6ff2aa",
"sha256:b7b5a4e34d3a4d9ecce4aa9e172a0fd13c8f5acee46afeb5944c4489f71701c1",
"sha256:c7b8a47315e8c0b79a008e33eca4299baa002efd4b53ace068f0894133a8933e",
"sha256:e81997b2fb4b7f19a80257aa2bb6e35e521d62dcf595599bf34886b115607bad"
],
"index": "pypi",
"version": "==2.3.8"
},
"async-generator": {
"hashes": [
"sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b",
"sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"
],
"index": "pypi",
"version": "==1.10"
},
"async-timeout": {
"hashes": [
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
],
"markers": "python_full_version >= '3.5.3'",
"version": "==3.0.1"
},
"attrs": {
"hashes": [
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.3.0"
},
"backcall": {
"hashes": [
"sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e",
"sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"
],
"version": "==0.2.0"
},
"bcrypt": {
"hashes": [
"sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89",
"sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42",
"sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294",
"sha256:436a487dec749bca7e6e72498a75a5fa2433bda13bac91d023e18df9089ae0b8",
"sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161",
"sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752",
"sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31",
"sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5",
"sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c",
"sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0",
"sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de",
"sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e",
"sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052",
"sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09",
"sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105",
"sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133",
"sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1",
"sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7",
"sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc"
],
"index": "pypi",
"version": "==3.1.7"
},
"bonsai": {
"hashes": [
"sha256:1f8c8bb3ae9cd514a046aa6c7193ee3b67993a0f27b28a9fe4b9088fa3782e23",
"sha256:220002bd80722ee45eaa802c87b411160b8ae5bd792d6e7aaeecbeb92b56ebb3",
"sha256:2df864cdda584d0902f4ddb3184bb67cbb6b27a1572b8eeb42d0ab89c229bb3c",
"sha256:42477259176ffcc4ecc3e2195fbaaee29ad5d73d17c2b1bda4d91b26bcb17075",
"sha256:44de82623b765f2bb967a91c3d3a48ca578c4090682a508b9533e144575a2f2c",
"sha256:45bb556e33ff57466e7e70a137afc984231b79edb7df2d4a65210d1c4f53bd0a",
"sha256:482c5ba1db3dc87daa6b8fa792269060db20115a37bcd84412b75c7c2b780132",
"sha256:83dde90bbe3f5957c6f987c0aceffd606ce14f494bfb56be2ddb633ae311ebdb",
"sha256:86dad31ad4260d7812832b59a75872602960739ed77961dcd89d7112b5278b67",
"sha256:91585832da3e465b46d3690091872c94239bb12a2db72c59265c4333bb272de0",
"sha256:915ed6326f5c0baf5942534d874fd9f8063e689ec681af24815f523d7434f093",
"sha256:a2bcbb02b5483a32739541f72f750a17bc28467eca1a286604b9b80bf8309225",
"sha256:e9c116ed3e553fb46ecd1cb3c42799e395d517ad044a1988614556510c1ff229",
"sha256:fd9a3d4c7ff668cdd04b8e68d67e186e7124e2262da149c3dbc78d1aa0a16b04"
],
"index": "pypi",
"version": "==1.2.0"
},
"cffi": {
"hashes": [
"sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e",
"sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d",
"sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a",
"sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec",
"sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362",
"sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668",
"sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c",
"sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b",
"sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06",
"sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698",
"sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2",
"sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c",
"sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7",
"sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009",
"sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03",
"sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b",
"sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909",
"sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53",
"sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35",
"sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26",
"sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b",
"sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01",
"sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb",
"sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293",
"sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd",
"sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d",
"sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3",
"sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d",
"sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e",
"sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca",
"sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d",
"sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775",
"sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375",
"sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b",
"sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b",
"sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f"
],
"version": "==1.14.4"
},
"chardet": {
"hashes": [
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.0.0"
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"index": "pypi",
"version": "==7.0"
},
"coverage": {
"hashes": [
"sha256:08b3ba72bd981531fd557f67beee376d6700fba183b167857038997ba30dd297",
"sha256:2757fa64e11ec12220968f65d086b7a29b6583d16e9a544c889b22ba98555ef1",
"sha256:3102bb2c206700a7d28181dbe04d66b30780cde1d1c02c5f3c165cf3d2489497",
"sha256:3498b27d8236057def41de3585f317abae235dd3a11d33e01736ffedb2ef8606",
"sha256:378ac77af41350a8c6b8801a66021b52da8a05fd77e578b7380e876c0ce4f528",
"sha256:38f16b1317b8dd82df67ed5daa5f5e7c959e46579840d77a67a4ceb9cef0a50b",
"sha256:3911c2ef96e5ddc748a3c8b4702c61986628bb719b8378bf1e4a6184bbd48fe4",
"sha256:3a3c3f8863255f3c31db3889f8055989527173ef6192a283eb6f4db3c579d830",
"sha256:3b14b1da110ea50c8bcbadc3b82c3933974dbeea1832e814aab93ca1163cd4c1",
"sha256:535dc1e6e68fad5355f9984d5637c33badbdc987b0c0d303ee95a6c979c9516f",
"sha256:6f61319e33222591f885c598e3e24f6a4be3533c1d70c19e0dc59e83a71ce27d",
"sha256:723d22d324e7997a651478e9c5a3120a0ecbc9a7e94071f7e1954562a8806cf3",
"sha256:76b2775dda7e78680d688daabcb485dc87cf5e3184a0b3e012e1d40e38527cc8",
"sha256:782a5c7df9f91979a7a21792e09b34a658058896628217ae6362088b123c8500",
"sha256:7e4d159021c2029b958b2363abec4a11db0ce8cd43abb0d9ce44284cb97217e7",
"sha256:8dacc4073c359f40fcf73aede8428c35f84639baad7e1b46fce5ab7a8a7be4bb",
"sha256:8f33d1156241c43755137288dea619105477961cfa7e47f48dbf96bc2c30720b",
"sha256:8ffd4b204d7de77b5dd558cdff986a8274796a1e57813ed005b33fd97e29f059",
"sha256:93a280c9eb736a0dcca19296f3c30c720cb41a71b1f9e617f341f0a8e791a69b",
"sha256:9a4f66259bdd6964d8cf26142733c81fb562252db74ea367d9beb4f815478e72",
"sha256:9a9d4ff06804920388aab69c5ea8a77525cf165356db70131616acd269e19b36",
"sha256:a2070c5affdb3a5e751f24208c5c4f3d5f008fa04d28731416e023c93b275277",
"sha256:a4857f7e2bc6921dbd487c5c88b84f5633de3e7d416c4dc0bb70256775551a6c",
"sha256:a607ae05b6c96057ba86c811d9c43423f35e03874ffb03fbdcd45e0637e8b631",
"sha256:a66ca3bdf21c653e47f726ca57f46ba7fc1f260ad99ba783acc3e58e3ebdb9ff",
"sha256:ab110c48bc3d97b4d19af41865e14531f300b482da21783fdaacd159251890e8",
"sha256:b239711e774c8eb910e9b1ac719f02f5ae4bf35fa0420f438cdc3a7e4e7dd6ec",
"sha256:be0416074d7f253865bb67630cf7210cbc14eb05f4099cc0f82430135aaa7a3b",
"sha256:c46643970dff9f5c976c6512fd35768c4a3819f01f61169d8cdac3f9290903b7",
"sha256:c5ec71fd4a43b6d84ddb88c1df94572479d9a26ef3f150cef3dacefecf888105",
"sha256:c6e5174f8ca585755988bc278c8bb5d02d9dc2e971591ef4a1baabdf2d99589b",
"sha256:c89b558f8a9a5a6f2cfc923c304d49f0ce629c3bd85cb442ca258ec20366394c",
"sha256:cc44e3545d908ecf3e5773266c487ad1877be718d9dc65fc7eb6e7d14960985b",
"sha256:cc6f8246e74dd210d7e2b56c76ceaba1cc52b025cd75dbe96eb48791e0250e98",
"sha256:cd556c79ad665faeae28020a0ab3bda6cd47d94bec48e36970719b0b86e4dcf4",
"sha256:ce6f3a147b4b1a8b09aae48517ae91139b1b010c5f36423fa2b866a8b23df879",
"sha256:ceb499d2b3d1d7b7ba23abe8bf26df5f06ba8c71127f188333dddcf356b4b63f",
"sha256:cef06fb382557f66d81d804230c11ab292d94b840b3cb7bf4450778377b592f4",
"sha256:e448f56cfeae7b1b3b5bcd99bb377cde7c4eb1970a525c770720a352bc4c8044",
"sha256:e52d3d95df81c8f6b2a1685aabffadf2d2d9ad97203a40f8d61e51b70f191e4e",
"sha256:ee2f1d1c223c3d2c24e3afbb2dd38be3f03b1a8d6a83ee3d9eb8c36a52bee899",
"sha256:f2c6888eada180814b8583c3e793f3f343a692fc802546eed45f40a001b1169f",
"sha256:f51dbba78d68a44e99d484ca8c8f604f17e957c1ca09c3ebc2c7e3bbd9ba0448",
"sha256:f54de00baf200b4539a5a092a759f000b5f45fd226d6d25a76b0dff71177a714",
"sha256:fa10fee7e32213f5c7b0d6428ea92e3a3fdd6d725590238a3f92c0de1c78b9d2",
"sha256:fabeeb121735d47d8eab8671b6b031ce08514c86b7ad8f7d5490a7b6dcd6267d",
"sha256:fac3c432851038b3e6afe086f777732bcf7f6ebbfd90951fa04ee53db6d0bcdd",
"sha256:fda29412a66099af6d6de0baa6bd7c52674de177ec2ad2630ca264142d69c6c7",
"sha256:ff1330e8bc996570221b450e2d539134baa9465f5cb98aff0e0f73f34172e0ae"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==5.3.1"
},
"decorator": {
"hashes": [
"sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760",
"sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"
],
"version": "==4.4.2"
},
"idna": {
"hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.10"
},
"iniconfig": {
"editable": true,
"path": "."
},
"ipdb": {
"hashes": [
"sha256:c85398b5fb82f82399fc38c44fe3532c0dde1754abee727d8f5cfcc74547b334"
],
"index": "pypi",
"version": "==0.13.4"
},
"ipython": {
"hashes": [
"sha256:c987e8178ced651532b3b1ff9965925bfd445c279239697052561a9ab806d28f",
"sha256:cbb2ef3d5961d44e6a963b9817d4ea4e1fa2eb589c371a470fed14d8d40cbd6a"
],
"index": "pypi",
"version": "==7.19.0"
},
"ipython-genutils": {
"hashes": [
"sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8",
"sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"
],
"version": "==0.2.0"
},
"jedi": {
"hashes": [
"sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93",
"sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"
],
"markers": "python_version >= '3.6'",
"version": "==0.18.0"
},
"ldap3": {
"hashes": [
"sha256:10bdd23b612e942ce90ea4dbc744dfd88735949833e46c5467a2dcf68e60f469",
"sha256:37d633e20fa360c302b1263c96fe932d40622d0119f1bddcb829b03462eeeeb7",
"sha256:7c3738570766f5e5e74a56fade15470f339d5c436d821cf476ef27da0a4de8b0",
"sha256:8f59a7b5399555b22db06f153daa76c77ded2dd84bc0f0ffe5b0b33901b6eac4",
"sha256:bed71c6ce2f70a00a330eed0c8370664c065239d45bcbe1b82517b6f6eed7f25"
],
"index": "pypi",
"version": "==2.8.1"
},
"mock": {
"hashes": [
"sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62",
"sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc"
],
"index": "pypi",
"version": "==4.0.3"
},
"multidict": {
"hashes": [
"sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a",
"sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93",
"sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632",
"sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656",
"sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79",
"sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7",
"sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d",
"sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5",
"sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224",
"sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26",
"sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea",
"sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348",
"sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6",
"sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76",
"sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1",
"sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f",
"sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952",
"sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a",
"sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37",
"sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9",
"sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359",
"sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8",
"sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da",
"sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3",
"sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d",
"sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf",
"sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841",
"sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d",
"sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93",
"sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f",
"sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647",
"sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635",
"sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456",
"sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda",
"sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5",
"sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281",
"sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"
],
"index": "pypi",
"version": "==5.1.0"
},
"packaging": {
"hashes": [
"sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858",
"sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.8"
},
"parso": {
"hashes": [
"sha256:15b00182f472319383252c18d5913b69269590616c947747bc50bf4ac768f410",
"sha256:8519430ad07087d4c997fda3a7918f7cfa27cb58972a8c89c2a0295a1c940e9e"
],
"markers": "python_version >= '3.6'",
"version": "==0.8.1"
},
"passlib": {
"hashes": [
"sha256:3d948f64138c25633613f303bcc471126eae67c04d5e3f6b7b8ce6242f8653e0",
"sha256:43526aea08fa32c6b6dbbbe9963c4c767285b78147b7437597f992812f69d280"
],
"index": "pypi",
"version": "==1.7.1"
},
"pexpect": {
"hashes": [
"sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937",
"sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"
],
"markers": "sys_platform != 'win32'",
"version": "==4.8.0"
},
"pickleshare": {
"hashes": [
"sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca",
"sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"
],
"version": "==0.7.5"
},
"pluggy": {
"hashes": [
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.13.1"
},
"prompt-toolkit": {
"hashes": [
"sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c",
"sha256:7debb9a521e0b1ee7d2fe96ee4bd60ef03c6492784de0547337ca4433e46aa63"
],
"markers": "python_full_version >= '3.6.1'",
"version": "==3.0.8"
},
"ptyprocess": {
"hashes": [
"sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0",
"sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"
],
"version": "==0.6.0"
},
"py": {
"hashes": [
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.10.0"
},
"pyasn1": {
"hashes": [
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
"sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
],
"version": "==0.4.8"
},
"pycparser": {
"hashes": [
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.20"
},
"pygments": {
"hashes": [
"sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716",
"sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08"
],
"markers": "python_version >= '3.5'",
"version": "==2.7.3"
},
"pyparsing": {
"hashes": [
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.7"
},
"pytest": {
"hashes": [
"sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8",
"sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"
],
"index": "pypi",
"version": "==6.2.1"
},
"pytest-aiohttp": {
"hashes": [
"sha256:0b9b660b146a65e1313e2083d0d2e1f63047797354af9a28d6b7c9f0726fa33d",
"sha256:c929854339637977375838703b62fef63528598bc0a9d451639eba95f4aaa44f"
],
"index": "pypi",
"version": "==0.3.0"
},
"pytest-asyncio": {
"hashes": [
"sha256:2eae1e34f6c68fc0a9dc12d4bea190483843ff4708d24277c41568d6b6044f1d",
"sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700"
],
"index": "pypi",
"version": "==0.14.0"
},
"pytest-cov": {
"hashes": [
"sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191",
"sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"
],
"index": "pypi",
"version": "==2.10.1"
},
"pytest-integration": {
"hashes": [
"sha256:560b18c003cf6a3d6672878e826a823ea5f8d1d289dbe97546495040b2f0bd3d",
"sha256:7630b2bb1a8d518168bae44d827c20c4f0c1bbc5a1d3e1014dc5624ccadcdbd1"
],
"index": "pypi",
"version": "==0.2.2"
},
"pytest-runner": {
"hashes": [
"sha256:5534b08b133ef9a5e2c22c7886a8f8508c95bb0b0bdc6cc13214f269c3c70d51",
"sha256:96c7e73ead7b93e388c5d614770d2bae6526efd997757d3543fe17b557a0942b"
],
"markers": "python_version >= '2.7'",
"version": "==5.2"
},
"pyyaml": {
"hashes": [
"sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
"sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
"sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
"sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e",
"sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
"sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
"sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
"sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
"sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
"sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a",
"sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
"sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
"sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
],
"index": "pypi",
"version": "==5.3.1"
},
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0"
},
"toml": {
"hashes": [
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.2"
},
"traitlets": {
"hashes": [
"sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396",
"sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"
],
"markers": "python_version >= '3.7'",
"version": "==5.0.5"
},
"wcwidth": {
"hashes": [
"sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784",
"sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"
],
"version": "==0.2.5"
},
"yarl": {
"hashes": [
"sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e",
"sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434",
"sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366",
"sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3",
"sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec",
"sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959",
"sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e",
"sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c",
"sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6",
"sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a",
"sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6",
"sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424",
"sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e",
"sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f",
"sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50",
"sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2",
"sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc",
"sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4",
"sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970",
"sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10",
"sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0",
"sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406",
"sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896",
"sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643",
"sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721",
"sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478",
"sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724",
"sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e",
"sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8",
"sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96",
"sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25",
"sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76",
"sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2",
"sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2",
"sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c",
"sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a",
"sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"
],
"markers": "python_version >= '3.6'",
"version": "==1.6.3"
}
}
} }

View 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 == ""

View File

@ -0,0 +1,775 @@
# -*- encoding: utf-8 -*-
from argparse import Namespace
import asyncio
from async_generator import asynccontextmanager
from bonsai import NoSuchObjectError
import mock
import pytest
from phi.async_ldap.model import (
get_class,
recall,
call_if_callable,
inheritance,
iter_children,
Entry,
build_heritage,
Hackers,
Robots,
Congregations,
Service,
create_new_,
User,
PhiEntryExists,
PhiEntryDoesNotExist,
PhiAttributeMissing,
PhiUnauthorized,
Group,
)
from phi.security import hash_pass
BASE_DN = "dc=test,dc=abbiamoundominio,dc=org"
USER_LIST = [{"uid": ["conte_mascetti"]}, {"uid": ["perozzi"]}, {"uid": ["necchi"]}]
SERVICE_LIST = [{"uid": ["phi"]}, {"uid": ["irc"]}]
GROUP_LIST = [{"cn": ["amici_miei"]}, {"cn": ["antani"]}]
EXISTING_USER = [
{
"dn": f"uid=existing_user,ou=Hackers,{BASE_DN}>",
"objectClass": ["inetOrgPerson", "organizationalPerson", "person", "top"],
"cn": ["exists"],
"sn": ["Existing User"],
"mail": ["existing@mail.org"],
"uid": ["existing_user"],
"userPassword": ["{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g="],
}
]
MISSING_AUTH_USER = [
{
"dn": f"uid=existing_user,ou=Hackers,{BASE_DN}>",
"objectClass": ["inetOrgPerson", "organizationalPerson", "person", "top"],
"cn": ["exists"],
"sn": ["Existing User"],
"mail": ["existing@mail.org"],
"uid": ["existing_user"],
}
]
EXISTING_SERVICE = [
{
"dn": f"uid=existing_service,ou=Services,{BASE_DN}>",
"objectClass": ["account", "simpleSecurityObject", "top"],
"uid": ["existing_service"],
"userPassword": ["{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g="],
}
]
class MockClient:
def __init__(self, base_dn, *args):
self._base_dn = base_dn
self.args = args
self.called_with_args = {}
self.username = f"uid=mock_connection,ou=Services,{base_dn}"
@property
def base_dn(self):
return self._base_dn
@asynccontextmanager
async def connect(self, *args, **kwargs):
conn = Namespace()
async def _search(*args, **kwargs):
self.called_with_args["search"] = {"args": args, "kwargs": kwargs}
if "Services" in self.args:
return SERVICE_LIST
elif "Groups" in self.args:
return GROUP_LIST
if "Users" in self.args or f"None=Entry,{BASE_DN}" in args:
return USER_LIST
elif f"uid=existing_user,ou=Hackers,{BASE_DN}" in args:
return EXISTING_USER
elif f"uid=existing_service,ou=Services,{BASE_DN}" in args:
return EXISTING_SERVICE
elif f"uid=not_existing,ou=Hackers,{BASE_DN}" in args:
return []
elif f"uid=not_existing,ou=Services,{BASE_DN}" in args:
return []
elif f"uid=missing_auth_user,ou=Hackers,{BASE_DN}" in args:
return MISSING_AUTH_USER
conn.search = _search
async def _add(*args, **kwargs):
self.called_with_args["add"] = {"args": args, "kwargs": kwargs}
return
conn.add = _add
async def _modify(*args, **kwargs):
self.called_with_args["modify"] = {"args": args, "kwargs": kwargs}
return
conn.modify = _modify
yield conn
@pytest.fixture
def client_fixture():
return MockClient(BASE_DN)
@pytest.fixture
def client_fixture_multi():
res = Namespace()
res.users = MockClient(BASE_DN, "Users")
res.services = MockClient(BASE_DN, "Services")
res.groups = MockClient(BASE_DN, "Groups")
return res
@pytest.fixture
def lineage_fixture():
class Grand:
@classmethod
def name(cls):
return cls.__name__
class Ma(Grand):
pass
class Child(Ma):
pass
grand = Grand()
ma = Ma()
child = Child()
return Grand, Ma, Child, grand, ma, child
def test_get_class():
c = MockClient(BASE_DN)
assert get_class(c) is MockClient
def test_recall(lineage_fixture):
Grand, Ma, Child, grand, ma, child = lineage_fixture
assert object is recall(grand)
assert recall(grand) is recall(Grand)
assert Grand is recall(ma)
assert recall(ma) is recall(Ma)
assert Ma is recall(child)
assert recall(child) is recall(Child)
def test_call_if_callable():
class Dummy:
classattr = "classattr"
@property
def prop(self):
return "prop"
def func(self):
return "func"
@classmethod
def clsmth(cls):
return "clsmth"
d = Dummy()
assert "classattr" == call_if_callable(d, "classattr")
assert "prop" == call_if_callable(d, "prop")
assert "func" == call_if_callable(d, "func")
assert "clsmth" == call_if_callable(d, "clsmth")
def test_inheritance(lineage_fixture):
Grand, Ma, Child, _, _, _ = lineage_fixture
assert inheritance(Grand, "name") == "Grand"
assert inheritance(Ma, "name") == "Ma,Grand"
assert inheritance(Child, "name") == "Child,Ma,Grand"
assert inheritance(Child, "name", Grand) == "Child,Ma"
assert inheritance(Child, "name", Ma) == "Child"
@pytest.mark.asyncio
async def test_iter_children():
LIST = [1, 2, 3, 4]
async def _async_gen():
for i in LIST:
yield i
ALIST = _async_gen()
assert LIST == await iter_children(ALIST)
def test_Entry(client_fixture):
e = Entry(client_fixture)
assert e.base_dn == BASE_DN
assert e.client is not None
def test_Entry_name(client_fixture):
e = Entry(client_fixture)
assert e.name() == "Entry"
def test_Entry_qualified_name(client_fixture):
e = Entry(client_fixture)
assert e.qualified_name() == "None=Entry"
def test_Entry_dn(client_fixture):
e = Entry(client_fixture)
assert e.dn == "None=Entry,{}".format(BASE_DN)
@pytest.mark.asyncio
async def test_Entry_describe(client_fixture):
e = Entry(client_fixture)
assert USER_LIST == await e.describe()
@pytest.mark.asyncio
async def test_build_heritage(client_fixture):
class MockAIterable(object):
client = None
def __init__(self, elements):
self.elements = elements
async def get_children(self):
for el in self.elements:
yield el
class Dummy(object):
def __init__(self, *args, **kwargs):
self.id = sum([id(el) for el in args]) + sum(
[id(v) for _, v in kwargs.items()]
)
def __repr__(self):
return f"<Dummy({self.id})>"
def __eq__(self, other):
return self.id == other.id
m = MockAIterable(USER_LIST)
res = [el async for el in build_heritage(m, Dummy, attribute_id="uid")]
exp_res = [Dummy(el["uid"][0], None) for el in USER_LIST]
assert res == exp_res
def test_Hackers(client_fixture_multi):
h = Hackers(client_fixture_multi.users)
assert h.kind == "ou"
assert h.dn == "ou={},{}".format(h.name(), BASE_DN)
assert repr(h) == f"<Hackers {h.kind}=Hackers,{BASE_DN}>"
def test_Hackers_singleton(client_fixture):
other_client = MockClient(BASE_DN)
h1 = Hackers(client_fixture)
h2 = Hackers(other_client)
h3 = Hackers(client_fixture)
assert client_fixture is not other_client
assert h1 is h3
assert h2 is not h1
@pytest.mark.asyncio
async def test_Entry_get_children(client_fixture_multi):
h = Hackers(client_fixture_multi.users)
assert USER_LIST == [el async for el in h.get_children()]
@pytest.mark.asyncio
async def test_Hackers_anext(client_fixture_multi):
h = Hackers(client_fixture_multi.users)
exp_res = [User(el["uid"][0], client_fixture_multi.users) for el in USER_LIST]
assert exp_res == [el async for el in h]
def test_Hackers_children(client_fixture_multi):
h = Hackers(client_fixture_multi.users)
assert USER_LIST == h.children
@pytest.mark.asyncio
async def test_Hackers_get_by_attr(client_fixture):
h = Hackers(client_fixture)
res = await h.get_by_attr("uid", "existing_user")
assert len(res) == 1
assert res[0] is User("existing_user", client_fixture)
assert client_fixture.called_with_args["search"]["args"] == (
f"uid=existing_user,ou=Hackers,{BASE_DN}",
0,
)
@pytest.mark.asyncio
async def test_Hackers_get_by_attr_empty(client_fixture):
h = Hackers(client_fixture)
res = await h.get_by_attr("uid", "not_existing")
assert res is None
@pytest.mark.asyncio
async def test_Hackers_get_by_uid(client_fixture):
h = Hackers(client_fixture)
res = await h.get_by_uid("existing_user")
assert res is User("existing_user", client_fixture)
def test_Robots(client_fixture_multi):
r = Robots(client_fixture_multi.services)
assert r.kind == "ou"
assert r.dn == "ou={},{}".format(r.name(), BASE_DN)
assert repr(r) == f"<Services {r.kind}=Services,{BASE_DN}>"
def test_Robots_singleton(client_fixture):
other_client = MockClient(BASE_DN)
r1 = Robots(client_fixture)
r2 = Robots(other_client)
r3 = Robots(client_fixture)
assert client_fixture is not other_client
assert r1 is r3
assert r2 is not r1
@pytest.mark.asyncio
async def test_Robots_anext(client_fixture_multi):
r = Robots(client_fixture_multi.services)
exp_res = [
Service(el["uid"][0], client_fixture_multi.services) for el in SERVICE_LIST
]
assert exp_res == [el async for el in r]
def test_Robots_children(client_fixture_multi):
r = Robots(client_fixture_multi.services)
assert SERVICE_LIST == r.children
def test_Congregations(client_fixture_multi):
g = Congregations(client_fixture_multi.groups)
assert g.kind == "ou"
assert g.dn == "ou={},{}".format(g.name(), BASE_DN)
assert repr(g) == f"<Groups {g.kind}=Groups,{BASE_DN}>"
def test_Congregations_singleton(client_fixture):
other_client = MockClient(BASE_DN)
g1 = Congregations(client_fixture)
g2 = Congregations(other_client)
g3 = Congregations(client_fixture)
assert client_fixture is not other_client
assert g1 is g3
assert g2 is not g1
@pytest.mark.asyncio
async def test_Congregations_anext(client_fixture_multi):
c = Congregations(client_fixture_multi.groups)
exp_res = [Group(el["cn"][0], client_fixture_multi.groups) for el in GROUP_LIST]
assert exp_res == [el async for el in c]
def test_Congregations_children(client_fixture_multi):
c = Congregations(client_fixture_multi.groups)
assert GROUP_LIST == c.children
@pytest.mark.asyncio
async def test_create_new_(client_fixture):
self = Namespace()
self.dn = f"uid=test,ou=Tests,{BASE_DN}"
self.client = client_fixture
self.object_class = ["a", "b", "c"]
ENTRY_DICT = {"attr1": "val1", "attr2": "val2", "attr3": "val3", "attr4": "val4"}
res = await create_new_(self, **ENTRY_DICT)
assert res is not None
assert client_fixture.called_with_args["add"]["args"][0] == res
assert res["objectClass"] == self.object_class
for k, v in ENTRY_DICT.items():
assert v == res[k][0]
def test_User(client_fixture):
c = User("conte_mascetti", client_fixture)
assert c.kind == "uid"
assert c.name == "conte_mascetti"
assert c.dn == "uid=conte_mascetti,ou=Hackers,{}".format(BASE_DN)
assert repr(c) == f"<User({c.name}) {c.dn}>"
def test_User_unsettable_name(client_fixture):
c = User("conte_mascetti", client_fixture)
with pytest.raises(RuntimeError) as e:
c.name = "totò"
assert "Name property is not modifiable." in str(e.value)
def test_User_singleton(client_fixture):
other_client = MockClient(BASE_DN)
c1 = User("conte_mascetti", client_fixture)
c2 = User("perozzi", client_fixture)
c3 = User("conte_mascetti", client_fixture)
c4 = User("conte_mascetti", other_client)
assert client_fixture is not other_client
assert c1 is c3
assert c2 is not c1
assert c4 is not c1
@pytest.mark.asyncio
async def test_User_create_existing(client_fixture):
c = User("existing_user", client_fixture)
with pytest.raises(PhiEntryExists) as e:
await c.create(
"existing@mail.org", password="password", sn="exists", cn="Existing User"
)
assert c.dn in str(e.value)
@pytest.mark.asyncio
async def test_User_create_not_existing(client_fixture):
c = User("not_existing", client_fixture)
await c.create("not@existing.org", "password")
assert client_fixture.called_with_args["search"]["args"] == (c.dn, 0)
assert c._entry["mail"][0] == "not@existing.org"
@pytest.mark.asyncio
async def test_User_sync_existing(client_fixture, caplog):
c = User("existing_user", client_fixture)
await c.sync()
assert f"User({c.name}): synced" in caplog.text
assert client_fixture.called_with_args["search"]["args"] == (c.dn, 0)
for k, v in EXISTING_USER[0].items():
if k != "dn":
assert c._entry[k] == v
@pytest.mark.asyncio
async def test_User_sync_not_existing(client_fixture, caplog):
c = User("not_existing", client_fixture)
with pytest.raises(PhiEntryDoesNotExist) as e:
await c.sync()
assert c.dn in str(e.value)
assert client_fixture.called_with_args["search"]["args"] == (c.dn, 0)
@pytest.mark.asyncio
async def test_User_modify_existing(client_fixture, caplog):
c = User("existing_user", client_fixture)
c._entry = mock.MagicMock()
async def _modify():
return
c._entry.modify = _modify
await c.sync()
await c.modify("mail", "other@existing.org")
assert f"User({c.name}): modified (mail)" in caplog.text
c._entry.__setitem__.assert_called_with("mail", "other@existing.org")
c._entry.__delitem__.assert_called_with("mail")
@pytest.mark.asyncio
async def test_User_modify_existing_append(client_fixture, caplog):
c = User("existing_user", client_fixture)
c._entry = mock.MagicMock()
async def _modify():
return
c._entry.modify = _modify
await c.sync()
await c.modify("mail", "other@existing.org", append=True)
assert f"User({c.name}): modified (mail)" in caplog.text
c._entry.__setitem__.assert_called_with("mail", "other@existing.org")
c._entry.__delitem__.assert_not_called()
@pytest.mark.asyncio
async def test_User_modify_not_existing(client_fixture):
c = User("existing_user", client_fixture)
c._entry = mock.MagicMock()
attr = {"__delitem__.side_effect": KeyError}
c._entry.configure_mock(**attr)
async def _modify():
return
c._entry.modify = _modify
await c.sync()
with pytest.raises(PhiAttributeMissing) as e:
await c.modify("snafu", "modified")
assert c.dn in str(e.value)
assert "snafu" in str(e.value)
@pytest.mark.asyncio
async def test_User_remove_existing(client_fixture, caplog):
c = User("existing_user", client_fixture)
c._entry = mock.MagicMock()
def _delete():
coro = mock.Mock(name="coroutine")
fn = mock.MagicMock(side_effect=asyncio.coroutine(coro))
return fn
delete = _delete()
c._entry.delete = delete
await c.sync()
await c.remove()
assert f"User({c.name}): removed" in caplog.text
delete.assert_called_once()
@pytest.mark.asyncio
async def test_User_remove_not_existing(client_fixture):
c = User("not_existing", client_fixture)
c._entry = mock.MagicMock()
def _delete():
fn = mock.MagicMock(side_effect=NoSuchObjectError)
return fn
delete = _delete()
c._entry.delete = delete
with pytest.raises(PhiEntryDoesNotExist) as e:
await c.remove()
assert c.dn in str(e.value)
@pytest.mark.asyncio
async def test_User_modify_password(client_fixture, caplog):
c = User("existing_user", client_fixture)
def _modify():
coro = mock.Mock(name="coroutine")
fn = mock.MagicMock(side_effect=asyncio.coroutine(coro))
return fn
modify = _modify()
c.modify = modify
await c.modify_password("new-password")
modify.assert_called_with("userPassword", hash_pass("new-password"))
assert "User(existing_user): password modified" in caplog.text
@pytest.mark.asyncio
async def test_User_modify_password_raises(client_fixture):
c = User("existing_user", client_fixture)
def _modify():
coro = mock.Mock(
name="coroutine", side_effect=PhiAttributeMissing("TESTDN", "TESTATTR")
)
fn = mock.MagicMock(side_effect=asyncio.coroutine(coro))
return fn
modify = _modify()
c.modify = modify
c.connection = mock.MagicMock()
c.connection.username = "existing_user"
with pytest.raises(PhiUnauthorized) as e:
await c.modify_password("randombytes")
modify.assert_called_with("userPassword", hash_pass("randombytes"))
assert f"uid=mock_connection,ou=Services,{BASE_DN}" in str(e.value.user)
@pytest.mark.asyncio
async def test_User_verify_password(client_fixture):
c = User("existing_user", client_fixture)
res_true = await c.verify_password("password")
res_false = await c.verify_password("wrong")
assert res_true
assert not res_false
@pytest.mark.asyncio
async def test_User_verify_password_raises_not_existing(client_fixture):
c = User("not_existing", client_fixture)
with pytest.raises(PhiEntryDoesNotExist) as e:
await c.verify_password("randombytes")
assert f"uid=not_existing,ou=Hackers,{BASE_DN}" in str(e.value)
@pytest.mark.asyncio
async def test_User_verify_password_raises_unauthorized(client_fixture):
c = User("missing_auth_user", client_fixture)
with pytest.raises(PhiUnauthorized) as e:
await c.verify_password("password")
assert f"uid=mock_connection,ou=Services,{BASE_DN}" in str(e.value.user)
def test_Service(client_fixture):
c = Service("phi", client_fixture)
assert c.kind == "uid"
assert c.name == "phi"
assert c.dn == "uid=phi,ou=Services,{}".format(BASE_DN)
assert repr(c) == f"<Service({c.name}) {c.dn}>"
def test_Service_unsettable_name(client_fixture):
c = Service("phi", client_fixture)
with pytest.raises(RuntimeError) as e:
c.name = "theta"
assert "Name property is not modifiable." in str(e.value)
def test_Service_singleton(client_fixture):
other_client = MockClient(BASE_DN)
c1 = Service("phi", client_fixture)
c2 = Service("irc", client_fixture)
c3 = Service("phi", client_fixture)
c4 = Service("phi", other_client)
assert client_fixture is not other_client
assert c1 is c3
assert c2 is not c1
assert c4 is not c1
@pytest.mark.asyncio
async def test_Service_create_existing(client_fixture):
s = Service("existing_service", client_fixture)
with pytest.raises(PhiEntryExists) as e:
await s.create("password")
assert s.dn in str(e.value)
@pytest.mark.asyncio
async def test_Service_create_not_existing(client_fixture):
s = Service("not_existing", client_fixture)
await s.create("password")
assert client_fixture.called_with_args["search"]["args"] == (s.dn, 0)
@pytest.mark.asyncio
async def test_Service_sync_existing(client_fixture):
s = Service("existing_service", client_fixture)
await s.sync()
assert client_fixture.called_with_args["search"]["args"] == (s.dn, 0)
@pytest.mark.asyncio
async def test_Service_sync_non_existing(client_fixture):
pass
def test_Group(client_fixture):
c = Group("amici_miei", client_fixture)
assert c.kind == "cn"
assert c.name == "amici_miei"
assert c.dn == "cn=amici_miei,ou=Groups,{}".format(BASE_DN)
assert repr(c) == f"<Group({c.name}) {c.dn}>"
def test_Group_unsettable_name(client_fixture):
c = Group("amici_miei", client_fixture)
with pytest.raises(RuntimeError) as e:
c.name = "nemici"
assert "Name property is not modifiable." in str(e.value)
def test_Group_singleton(client_fixture):
other_client = MockClient(BASE_DN)
c1 = Group("amici_miei", client_fixture)
c2 = Group("antani", client_fixture)
c3 = Group("amici_miei", client_fixture)
c4 = Group("amici_miei", other_client)
assert client_fixture is not other_client
assert c1 is c3
assert c2 is not c1
assert c4 is not c1

View 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.new_model import get_dn, User
from phi.async_ldap.mixins import Member
from phi.exceptions import PhiCannotExecute
BASE_DN = "dc=test,dc=domain,dc=tld"
class MockClient(object):
def __init__(self, *args, **kwargs):
self.return_value = kwargs.get("return_value")
self.connect_called = False
self.conn = mock.MagicMock()
self.search_event = asyncio.Event()
self.add_event = asyncio.Event()
self.delete_event = asyncio.Event()
async def connect_called_with_search(self):
return await self.search_event.wait()
async def connect_called_with_add(self):
return await self.add_event.wait()
async def connect_called_with_delete(self):
return await self.delete_event.wait()
@property
def base_dn(self):
return BASE_DN
@asynccontextmanager
async def connect(self, *args, **kwargs):
self.connect_called = True
async def _search(*a, **kw):
self.search_event.set()
return self.return_value
async def _add(*a, **kw):
self.add_event.set()
return self.return_value
async def _modify(*a, **kw):
return self.return_value
async def _delete(*a, **kw):
self.delete_event.set()
return self.return_value
self.conn.search = mock.MagicMock(side_effect=_search)
self.conn.add = mock.MagicMock(side_effect=_add)
self.conn.modify = mock.MagicMock(side_effect=_modify)
self.conn.delete = mock.MagicMock(side_effect=_delete)
yield self.conn
cl = mock.MagicMock()
cl.base_dn = BASE_DN
@pytest.mark.parametrize(
"input_obj, expected_result",
[
(User(cl, "test_user"), f"uid=test_user,ou=Hackers,{BASE_DN}"),
(
LDAPEntry(f"uid=test_user,ou=Hackers,{BASE_DN}"),
f"uid=test_user,ou=Hackers,{BASE_DN}",
),
(f"uid=test_user,ou=Hackers,{BASE_DN}", f"uid=test_user,ou=Hackers,{BASE_DN}"),
],
)
def test_get_dn(input_obj, expected_result):
assert get_dn(input_obj) == expected_result
def test_get_dn_raises():
with pytest.raises(ValueError) as e:
_ = get_dn(object)
assert "Unacceptable input:" in str(e.value)
def test_repr():
_cl = MockClient(return_value=None)
u = User(_cl, "test_user")
assert repr(u) == f"<User uid=test_user,ou=Hackers,{BASE_DN}>"
def test_str():
_cl = MockClient(return_value=None)
u = User(_cl, "test_user")
assert str(u) == f"<User uid=test_user,ou=Hackers,{BASE_DN}>"
@pytest.mark.parametrize(
"input_obj",
[
User(cl, "test_user"),
LDAPEntry(f"uid=test_user,ou=Hackers,{BASE_DN}"),
f"uid=test_user,ou=Hackers,{BASE_DN}",
],
)
def test_eq(input_obj):
u = User(cl, "test_user")
assert u == input_obj
@pytest.mark.asyncio
async def test_User_add():
_cl = MockClient(return_value=None)
u = User(_cl, "test_user")
assert u.dn == f"uid=test_user,ou=Hackers,{BASE_DN}"
_ = await u.save()
assert _cl.connect_called
assert await _cl.connect_called_with_add()
@pytest.mark.asyncio
async def test_User_modify():
"""
This test does not use the MockClient check facilities because
of implementation details of the Entry class.
"""
_cl = MockClient(
return_value=[
LDAPEntry(f"uid=test_user,ou=Hackers,{BASE_DN}"),
]
)
u = User(_cl, "test_user")
# This is the asyncio equivalent of a semaphore
modified = asyncio.Event()
async def _mock_modify():
modified.set()
u._entry = mock.MagicMock()
u._entry.modify = mock.MagicMock(side_effect=_mock_modify)
u["cn"] = "random_cn"
_ = await u.modify()
assert _cl.connect_called
assert await _cl.connect_called_with_search()
# The `wait()` call here is needed to wait for `_mock_modify`
# to end its async execution
assert await modified.wait()
@pytest.mark.asyncio
async def test_User_delete():
_cl = MockClient(return_value=None)
u = User(_cl, "test_user")
_ = await u.delete()
assert _cl.connect_called
assert await _cl.connect_called_with_delete()
@pytest.mark.asyncio
async def test_User_get_invalid_attr_raises():
_cl = MockClient(return_value=None)
u = User(_cl, "test_user")
with pytest.raises(PhiCannotExecute) as ex:
_ = u["iDoNotExist"]
assert "iDoNotExist" in str(ex.value)
assert "is not an allowed ldap attribute" in str(ex.value)
@pytest.mark.asyncio
async def test_User_set_invalid_attr_raises():
_cl = MockClient(return_value=None)
u = User(_cl, "test_user")
with pytest.raises(PhiCannotExecute) as ex:
u["iDoNotExist"] = "hello"
assert "iDoNotExist" in str(ex.value)
assert "is not an allowed ldap attribute" in str(ex.value)

View File

@ -11,7 +11,7 @@ ldap:
encryption: TLSv1.2 # Can either be None or TLSv1.2. Default: None encryption: TLSv1.2 # Can either be None or TLSv1.2. Default: None
ciphers: "HIGH" 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 ca_certs: openldap/cert.pem
username: uid=phi,ou=Services,dc=unit,dc=macaomilano,dc=org username: uid=phi,ou=Services,dc=unit,dc=macaomilano,dc=org
@ -19,7 +19,6 @@ ldap:
base_dn: dc=unit,dc=macaomilano,dc=org base_dn: dc=unit,dc=macaomilano,dc=org
attribute_id: uid attribute_id: uid
attribute_mail: mail
logging: logging:

View File

@ -0,0 +1,362 @@
# -*- coding: utf-8 -*-
import asyncio
from async_generator import asynccontextmanager
import pytest
from phi.async_ldap.new_model import (
Hackers,
User,
Robots,
Service,
Group,
Congregations,
)
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 = Congregations(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 == {
"ou": "Hackers",
"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 = Congregations(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": "Congregations",
"cn": "achaeans",
"dn": f"cn=achaeans,ou=Congregations,{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

View File

@ -1,5 +1,7 @@
FROM alpine:3.7 FROM alpine:3.7
ENV LDAPTLS_REQCERT=never
RUN apk add --no-cache \ RUN apk add --no-cache \
openldap \ openldap \
openldap-back-mdb \ openldap-back-mdb \

View File

@ -16,6 +16,8 @@ gen-cert:
.PHONY: clean .PHONY: clean
clean: clean:
docker rm $(CONTAINER) || true
docker rmi unit/slapd
rm -f key.pem cert.pem rm -f key.pem cert.pem
.PHONY: run .PHONY: run
@ -35,6 +37,10 @@ prepare:
run-bg: run-bg:
make prepare make prepare
.PHONY: logs
logs:
docker logs -f phi_slapd
.PHONY: stop .PHONY: stop
stop: is-running stop: is-running
docker stop $(CONTAINER) docker stop $(CONTAINER)

View File

@ -11,30 +11,93 @@ objectClass: organizationalUnit
objectClass: top objectClass: top
ou: Hackers ou: Hackers
dn: ou=Services,dc=unit,dc=macaomilano,dc=org dn: ou=Robots,dc=unit,dc=macaomilano,dc=org
objectClass: top objectClass: top
objectClass: organizationalUnit objectClass: organizationalUnit
ou: Services ou: Robots
dn: ou=Groups,dc=unit,dc=macaomilano,dc=org dn: ou=Congregations,dc=unit,dc=macaomilano,dc=org
objectClass: top objectClass: top
objectClass: organizationalUnit objectClass: organizationalUnit
ou: Groups ou: Congregations
dn: uid=phi,ou=Services,dc=unit,dc=macaomilano,dc=org dn: uid=phi,ou=Robots,dc=unit,dc=macaomilano,dc=org
objectClass: account objectClass: account
objectClass: simpleSecurityObject objectClass: simpleSecurityObject
objectClass: top objectClass: top
uid: phi uid: phi
userPassword: {SHA}REu9CtcqSaA1c5J+sEYlTgg0H+M= 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 dn: uid=conte_mascetti,ou=Hackers,dc=unit,dc=macaomilano,dc=org
objectClass: inetOrgPerson objectClass: inetOrgPerson
objectClass: organizationalPerson objectClass: organizationalPerson
objectClass: person objectClass: person
objectClass: top objectClass: top
memberOf: cn=Admins,ou=Congregations,dc=unit,dc=macaomilano,dc=org
memberOf: cn=GitUsers,ou=Congregations,dc=unit,dc=macaomilano,dc=org
memberOf: cn=IRCUsers,ou=Congregations,dc=unit,dc=macaomilano,dc=org
cn: Raffaello cn: Raffaello
sn: Mascetti sn: Mascetti
mail: rmascetti@autistici.org mail: rmascetti@autistici.org
uid: conte_mascetti uid: conte_mascetti
userPassword: {SHA}oLY7P6V+DWaMJhix7vbMYGIfA+E= userPassword: {SHA}oLY7P6V+DWaMJhix7vbMYGIfA+E=
dn: uid=necchi,ou=Hackers,dc=unit,dc=macaomilano,dc=org
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
memberOf: cn=GitUsers,ou=Congregations,dc=unit,dc=macaomilano,dc=org
memberOf: cn=IRCUsers,ou=Congregations,dc=unit,dc=macaomilano,dc=org
cn: Guido
sn: Necchi
mail: gnecchi@autistici.org
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=Congregations,dc=unit,dc=macaomilano,dc=org
cn: Giorgio
sn: Perozzi
mail: gperozzi@autistici.org
uid: perozzi
userPassword: {SHA}0+CRQKqsTj1I82PHxvZ4ebbddXQ=
dn: cn=Admins,ou=Congregations,dc=unit,dc=macaomilano,dc=org
member: uid=conte_mascetti,ou=Hackers,dc=unit,dc=macaomilano,dc=org
cn: Admins
objectClass: groupOfNames
objectClass: top
dn: cn=GitUsers,ou=Congregations,dc=unit,dc=macaomilano,dc=org
member: uid=conte_mascetti,ou=Hackers,dc=unit,dc=macaomilano,dc=org
member: uid=necchi,ou=Hackers,dc=unit,dc=macaomilano,dc=org
member: uid=perozzi,ou=Hackers,dc=unit,dc=macaomilano,dc=org
cn: GitUsers
objectClass: groupOfNames
objectClass: top
dn: cn=IRCUsers,ou=Congregations,dc=unit,dc=macaomilano,dc=org
member: uid=conte_mascetti,ou=Hackers,dc=unit,dc=macaomilano,dc=org
member: uid=necchi,ou=Hackers,dc=unit,dc=macaomilano,dc=org
cn: IRCUsers
objectClass: groupOfNames
objectClass: top

3
pyproject.toml Normal file
View File

@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta:__legacy__"

View File

@ -1,7 +1,11 @@
[aliases] [aliases]
test=pytest test=pytest
[pycodestyle]
max-line-length=88
[flake8] [flake8]
max-line-length=88
exclude = exclude =
.git, .git,
__pycache__, __pycache__,

View File

@ -1,22 +1,25 @@
from setuptools import setup from setuptools import setup, find_packages
setup( setup(
name='phi', name="phi",
version='0.0.1', version="0.0.1",
description="Post-Human Interface",
description='Post-Human Interface',
# license='', # license='',
url='https://git.abbiamoundominio.org/unit/phi', url="https://git.abbiamoundominio.org/unit/phi",
author="unit",
author='unit', author_email="unit@paranoici.org",
author_email='unit@paranoici.org', package_dir={"": "src"},
packages=find_packages("src"),
package_dir={'': 'src'}, entry_points={"console_scripts": ["phid=phi.app:cli"]},
packages=['phi', 'phi.api', 'phi.ldap'], setup_requires=["pytest-runner"],
scripts=['src/phid'], install_requires=[
"aiohttp==2.3.8",
setup_requires=['pytest-runner'], "click==7.0",
install_requires=['aiohttp==2.3.8', 'pyYAML', 'ldap3'], "pyYAML",
tests_require=['pytest', 'pytest-aiohttp'] "ldap3",
"bonsai==1.2.0",
"passlib==1.7.1",
"bcrypt==3.1.7",
],
tests_require=["pytest", "pytest-aiohttp", "pytest-asyncio", "async-generator"],
) )

View File

@ -1,18 +1,23 @@
# -*- encoding: utf-8 -*-
import logging
from aiohttp import web from aiohttp import web
from phi.logging import get_logger from phi.logging import get_logger
from phi.ldap.client import Client from phi.async_ldap.client import AsyncClient
from phi.async_ldap.model import Hackers, Robots, Congregations
from phi.api.routes import api_routes from phi.api.routes import api_routes
log = get_logger(__name__) log = get_logger(__name__)
alog = logging.getLogger("asyncio")
def api_startup(app): def api_startup(app):
app['ldap_client'].open() app["ldap_client"].open()
def api_shutdown(app): def api_shutdown(app):
app['ldap_client'].close() app["ldap_client"].close()
def api_app(config): def api_app(config):
@ -20,8 +25,12 @@ def api_app(config):
app = web.Application() app = web.Application()
ldap_client = Client(**config.get('ldap', {})) ldap_client = AsyncClient(**config.get("ldap", {}))
app['ldap_client'] = ldap_client app["ldap_client"] = ldap_client
app["users"] = Hackers(ldap_client)
app["services"] = Robots(ldap_client)
app["groups"] = Congregations(ldap_client)
app["alog"] = alog
app.on_startup.append(api_startup) app.on_startup.append(api_startup)
app.on_shutdown.append(api_shutdown) app.on_shutdown.append(api_shutdown)

View File

@ -2,23 +2,26 @@ from aiohttp.web import json_response, View
from aiohttp.web import HTTPNotFound, HTTPUnprocessableEntity from aiohttp.web import HTTPNotFound, HTTPUnprocessableEntity
from phi.logging import get_logger 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 Hackers
log = get_logger(__name__) log = get_logger(__name__)
class User(View): class UserView(View):
async def get(self): async def get(self):
uid = self.request.match_info.get('uid', None) uid = self.request.match_info.get("uid", None)
if uid is None: if uid is None:
return HTTPUnprocessableEntity() return HTTPUnprocessableEntity()
client = self.request.app['ldap_client'] user = await self.request.app["users"].get_by_uid(uid)
user = get_user_by_uid(client, uid) self.request.app["alog"].info("Found user %s", user)
if not user: if not user:
return HTTPNotFound() return HTTPNotFound()
return json_response(serialize(user)) result = await user.describe()
self.request.app["alog"].debug("Returning result %s", result)
return json_response(result)

View File

@ -1,10 +1,9 @@
from aiohttp.web import route from aiohttp.web import route
from phi.api.rest import User from phi.api.rest import UserView
api_routes = [ api_routes = [
route('*', '/user', User), route("*", "/user", UserView),
route('*', '/user/', User), route("*", "/user/", UserView),
route('*', '/user/{uid}', User) route("*", "/user/{uid}", UserView),
] ]

View File

@ -1,6 +1,8 @@
from datetime import datetime from datetime import datetime
def serialize(d): def serialize(obj):
return {k: (v.isoformat() if isinstance(v, datetime) else v) return {
for k, v in d.items()} k: (v.isoformat() if isinstance(v, datetime) else v)
for k, v in dict(obj).items()
}

View File

@ -1,22 +1,212 @@
# -*- encoding: utf-8 -*-
import pkg_resources
from asyncio import get_event_loop from asyncio import get_event_loop
from aiohttp import web from aiohttp import web
import click
from pprint import pformat as pp
import yaml
from phi.config import get_config, merge_config
from phi.logging import setup_logging, get_logger
from phi.api.app import api_app from phi.api.app import api_app
log = get_logger(__name__)
def setup_app(config): def setup_app(config):
loop = get_event_loop() loop = get_event_loop()
app = web.Application(loop=loop) app = web.Application(loop=loop)
app['config'] = config app["config"] = config
api = api_app(config) api = api_app(config)
app.add_subapp('/api', api) app.add_subapp("/api", api)
return app return app
def run_app(app): def run_app(app):
web.run_app(app, web.run_app(
host=app['config']['core']['listen'].get('host', '127.0.0.1'), app,
port=app['config']['core']['listen'].get('port', '8080')) host=app["config"]["core"]["listen"].get("host", "127.0.0.1"),
port=app["config"]["core"]["listen"].get("port", "8080"),
)
def _validate_port(ctx, param, value):
"""
Callback to validate provided port.
"""
if 0 < value or value > 65535:
raise click.BadParameter(
"Provided value is not in the range 0-65535: {} [type: {}]".format(
value, type(value)
)
)
return value
@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)
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

View File

View File

@ -0,0 +1,138 @@
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 open(self):
pass
def close(self):
pass

View File

@ -0,0 +1,340 @@
# -*- encoding: utf-8 -*-
from bonsai import LDAPEntry, LDAPModOp, NoSuchObjectError # type: ignore
from bonsai.ldapvaluelist import LDAPValueList
import bonsai.errors
from phi.exceptions import (
PhiAttributeMissing,
PhiEntryDoesNotExist,
PhiEntryExists,
PhiUnauthorized,
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 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((k, de_listify(_internal[k])) for k in self.ldap_attributes)
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

440
src/phi/async_ldap/model.py Normal file
View File

@ -0,0 +1,440 @@
# -*- encoding: utf-8 -*-
import asyncio
import logging
import typing as T
from bonsai import LDAPEntry, LDAPModOp, NoSuchObjectError # type: ignore
from phi.exceptions import (
PhiAttributeMissing,
PhiEntryDoesNotExist,
PhiEntryExists,
PhiUnauthorized,
PhiUnexpectedRuntimeValue,
)
from phi.logging import get_logger
from phi.security import hash_pass
log = get_logger(__name__)
alog = logging.getLogger("asyncio")
alog.setLevel(logging.DEBUG)
def get_class(obj):
"""
Return the input if input is a class, else tryes to get the class from the
`__class__` method.
"""
if type(obj) is type:
return obj
return obj.__class__
def recall(cls):
"""
Prints the name of the parent class.
"""
_cls = get_class(cls)
return _cls.__bases__[0]
def call_if_callable(cls, attr):
"""
Tell between methods and properties. Call if method.
"""
_attr = getattr(cls, attr)
if "__call__" in dir(_attr):
return _attr()
return _attr
def inheritance(obj, attr, root_cls=object):
"""
Concatenates the value obtained from invoking the method attr
on the class and on any parent class until root_cls.
"""
res = call_if_callable(obj, attr)
base = get_class(obj)
while base is not root_cls:
if base is not get_class(obj):
res += ",{}".format(call_if_callable(base, attr))
base = recall(base)
return res.strip(",")
def singletonize(cls, name):
"""
Helper function to be plugged in `__new__` method to make the class a singleton.
"""
if name not in cls._instances:
cls._instances[name] = object.__new__(cls)
return cls._instances[name]
async def iter_children(children):
return [child async for child in children]
class Entry(object):
"""
LDAP Entry. Interface to LDAP.
"""
kind: T.Union[None, str] = None
_name: T.Union[None, str] = None
@classmethod
def name(cls):
if "_name" in dir(cls) and cls._name is not None:
return cls._name
return cls.__name__
@classmethod
def qualified_name(cls):
return "{}={}".format(cls.kind, cls.name())
def __init__(self, client):
self.client = client
self.base_dn = client.base_dn
def __repr__(self):
return f"<{call_if_callable(self, 'name')} {self.dn}>"
def __dict__(self):
return self._dict
def __aiter__(self):
return self
async def get_children(self):
async with self.client.connect(is_async=True) as conn:
for el in await conn.search(self.dn, 2):
yield el
@property
def children(self):
"""
Synchronous property to enumerate the children.
"""
return asyncio.run(iter_children(self.get_children()))
@property
def dn(self):
return "{},{}".format(inheritance(self, "qualified_name", Entry), self.base_dn)
@property
def attributes(self):
raise NotImplemented()
async def describe(self):
async with self.client.connect(is_async=True) as conn:
# This returns a list of dicts. It should always contain only one item:
# the one we are interested.
res = await conn.search(self.dn, 0)
if len(res) == 0:
return
elif len(res) > 1:
raise PhiUnexpectedRuntimeValue(
"return value should be no more than one", res
)
res = res[0]
res.update({"dn": self.dn, "name": self.name})
return res
async def build_heritage(obj, child_class, attribute_id="uid"):
"""
Given the object and the child class, yields the
instances of the children.
"""
async for child in obj.get_children():
if attribute_id in child:
_name = child[attribute_id][0]
yield child_class(_name, obj.client)
class Hackers(Entry):
"""
This class is where Users belong.
"""
kind = "ou"
_name = "Hackers"
_instances: T.Dict[str, Entry] = dict()
def __new__(cls, client, *args, **kwargs):
return singletonize(cls, f"{cls._name}-{id(client)}")
def __init__(self, client, *args, **kwargs):
super().__init__(client)
self._hackers = build_heritage(self, User)
async def __anext__(self):
try:
return await self._hackers.__anext__()
except StopAsyncIteration:
self._hackers = build_heritage(self, User)
raise
async def get_by_attr(self, attr, value):
async with self.client.connect(is_async=True) as conn:
res = await conn.search("{}={},{}".format(attr, value, self.dn), 0)
if len(res) == 0:
return None
return [User(r["uid"][0], self.client) for r in res]
async def get_by_uid(self, uid):
res = await self.get_by_attr("uid", uid)
return res[0]
@property
def attributes(self):
return {"kind": self.kind}
class Robots(Entry):
"""
This class is where Services belong.
"""
kind = "ou"
_name = "Services"
_instances: T.Dict[str, Entry] = dict()
def __new__(cls, client, *args, **kwargs):
return singletonize(cls, f"{cls._name}-{id(client)}")
def __init__(self, client, *args, **kwargs):
super().__init__(client)
self._robots = build_heritage(self, Service)
async def __anext__(self):
try:
return await self._robots.__anext__()
except StopAsyncIteration:
self._robots = build_heritage(self, Service)
raise
class Congregations(Entry):
"""
This class is where Groups belong.
"""
kind = "ou"
_name = "Groups"
_instances: T.Dict[str, Entry] = dict()
def __new__(cls, client, *args, **kwargs):
return singletonize(cls, f"{cls._name}-{id(client)}")
def __init__(self, client, *args, **kwargs):
super().__init__(client)
self._groups = build_heritage(self, Group, attribute_id="cn")
async def __anext__(self):
try:
return await self._groups.__anext__()
except StopAsyncIteration:
self._groups = build_heritage(self, Group, attribute_id="cn")
raise
async def create_new_(self, **kwargs):
entry = LDAPEntry(self.dn)
entry["objectClass"] = self.object_class
for k, w in kwargs.items():
entry[k] = w
async with self.client.connect(is_async=True) as conn:
await conn.add(entry)
return entry
class User(Hackers):
"""
This class models a user. Users may have attributes
and belong one or more Group(s).
"""
kind = "uid"
_instances: T.Dict[str, Entry] = dict()
object_class = ["inetOrgPerson", "organizationalPerson", "person", "top"]
def __new__(cls, client, name, *args, **kwargs):
return singletonize(cls, f"{name}-{id(client)}")
def __init__(self, client, name, *args, **kwargs):
super().__init__(client, *args, **kwargs)
self._name = name
self._entry = LDAPEntry(self.dn)
def __repr__(self):
return f"<{get_class(self).__name__}({self.name}) {self.dn}>"
def qualified_name(self):
return "{}={}".format(self.kind, self.name)
@property
def name(self):
return self._name
@name.setter
def name(self, name):
raise RuntimeError("Name property is not modifiable.")
async def create(self, mail, password=None, sn=None, cn=None):
async with self.client.connect(is_async=True) as conn:
res = await conn.search(self.dn, 0)
if len(res) > 0:
raise PhiEntryExists(self.dn)
_sn = sn if sn is not None else self.name
_cn = cn if cn is not None else self.name
hashed = hash_pass(password)
self._entry = await create_new_(
self, uid=self.name, mail=mail, sn=_sn, cn=_cn, userPassword=hashed
)
alog.info("User(%s): created", self.name)
async def sync(self):
async with self.client.connect(is_async=True) as conn:
res = await conn.search(self.dn, 0)
if len(res) == 0:
raise PhiEntryDoesNotExist(self.dn)
for k, v in res[0].items():
if not k == "dn":
self._entry[k] = v
alog.info("User(%s): synced", self.name)
async def modify(self, key, value, append=False):
async with self.client.connect(is_async=True) as conn:
self._entry.connection = conn
try:
if not append:
del self._entry[key]
self._entry[key] = value
except KeyError:
raise PhiAttributeMissing(self.dn, key)
await self._entry.modify()
alog.info("User(%s): modified (%s)", self.name, key)
async def remove(self):
async with self.client.connect(is_async=True) as conn:
self._entry.connection = conn
try:
await self._entry.delete()
alog.info("User(%s): removed", self.name)
except NoSuchObjectError:
raise PhiEntryDoesNotExist(self.dn)
async def modify_password(self, new_pass, old_pass=None):
try:
await self.modify("userPassword", hash_pass(new_pass))
except PhiAttributeMissing:
raise PhiUnauthorized(user=self.client.username,)
alog.info("User(%s): password modified", self.name)
async def verify_password(self, given_pass):
async with self.client.connect(is_async=True) as conn:
res = await conn.search(self.dn, 0)
if len(res) == 0:
raise PhiEntryDoesNotExist(self.dn)
try:
match_pass = res[0]["userPassword"][0] == hash_pass(given_pass)
except KeyError:
raise PhiUnauthorized(user=self.client.username,)
return match_pass
class Service(Robots):
"""
This class models a system user (i.e. users that are ancillary to
services on a machine). System user may have attributes
and belong to one or more Group(s).
"""
kind = "uid"
_instances: T.Dict[str, Entry] = dict()
object_class = ["account", "top", "simpleSecurityObject"]
def __new__(cls, client, name, *args, **kwargs):
return singletonize(cls, f"{name}-{id(client)}")
def __init__(self, client, name, *args, **kwargs):
super().__init__(client, *args, **kwargs)
self._name = name
self._entry = LDAPEntry(self.dn)
def __repr__(self):
return f"<{get_class(self).__name__}({self.name}) {self.dn}>"
def qualified_name(self):
return "{}={}".format(self.kind, self.name)
@property
def name(self):
return self._name
@name.setter
def name(self, name):
raise RuntimeError("Name property is not modifiable.")
async def create(self, password):
async with self.client.connect(is_async=True) as conn:
res = await conn.search(self.dn, 0)
if len(res) > 0:
raise PhiEntryExists(self.dn)
self._entry = await create_new_(self, uid=self.name, userPassword=password)
alog.info("Service(%s): created", self.name)
async def sync(self):
async with self.client.connect(is_async=True) as conn:
res = await conn.search(self.dn, 0)
if len(res) == 0:
raise PhiEntryDoesNotExist(self.dn)
for k, v in res[0].items():
if not k == "dn":
self._entry[k] = v
alog.info("Service(%s): synced", self.name)
async def remove(self):
pass
async def modify_password(self, new_pass, old_pass=None):
pass
async def verify_password(self, given_pass):
pass
class Group(Congregations):
"""
This class models a group. Groups may have attributes
and may have Users and Services belonging to them.
"""
kind = "cn"
_instances: T.Dict[str, Entry] = dict()
object_class = ["groupOfNames", "top"]
def __new__(cls, client, name, *args, **kwargs):
return singletonize(cls, f"{name}-{id(client)}")
def __init__(self, client, name, *args, **kwargs):
super().__init__(client, *args, **kwargs)
self._name = name
self._entry = LDAPEntry(self.dn)
def __repr__(self):
return f"<{get_class(self).__name__}({self.name}) {self.dn}>"
def qualified_name(self):
return "{}={}".format(self.kind, self.name)
@property
def name(self):
return self._name
@name.setter
def name(self, name):
raise RuntimeError("Name property is not modifiable.")

View File

@ -0,0 +1,137 @@
# -*- encoding: utf-8 -*-
from bonsai import LDAPEntry
from multidict import MultiDict
from phi.async_ldap import mixins
def parse_dn(dn):
return MultiDict(e.split("=") for e in dn.split(","))
def get_dn(obj):
if isinstance(obj, mixins.Entry):
return obj.dn
elif isinstance(obj, LDAPEntry):
return obj["dn"]
elif isinstance(obj, str):
return obj
else:
raise ValueError(f"Unacceptable input: {obj}")
class User(mixins.Member, mixins.Entry, mixins.Singleton):
object_class = [
"inetOrgPerson",
"simpleSecurityObject",
"organizationalPerson",
"person",
"top",
]
_instances = dict() # type: ignore
id_tag = "uid"
ou = "Hackers"
ldap_attributes = ["uid", "ou", "cn", "sn", "mail", "userPassword"]
async def iter_groups(self): # To be monkeypatched later
pass # pragma: no cover
async def groups(self):
return [g async for g in self.iter_groups()]
async def delete(self):
async for group in self.iter_groups():
await group.remove_member(self)
await super().delete()
class Hackers(mixins.OrganizationalUnit, mixins.Entry, mixins.Singleton):
_instances = dict() # type: ignore
ou = "Hackers"
child_class = User
class Service(mixins.Member, mixins.Entry, mixins.Singleton):
object_class = ["simpleSecurityObject", "account", "top"]
_instances = dict() # type: ignore
id_tag = "uid"
ou = "Robots"
ldap_attributes = ["uid", "ou", "userPassword"]
async def iter_groups(self): # To be monkeypatched later
pass # pragma: no cover
async def groups(self):
return [g async for g in self.iter_groups()]
async def delete(self):
async for group in self.iter_groups():
await group.remove_member(self)
await super().delete()
class Robots(mixins.OrganizationalUnit, mixins.Entry, mixins.Singleton):
_instances = dict() # type: ignore
ou = "Robots"
child_class = Service
class Group(mixins.Member, mixins.Entry, mixins.Singleton):
object_class = ["groupOfNames", "top"]
_instances = dict() # type: ignore
id_tag = "cn"
ou = "Congregations"
ldap_attributes = ["cn", "ou", "member"]
memeber_classes = {"Hackers": User, "Robots": Service}
empty = False
async def add_member(self, member):
member_dn = get_dn(member)
self._entry["member"].append(member_dn)
await self.modify()
async def remove_member(self, member):
new_group_members = [get_dn(m) async for m in self.get_members() if member != m]
if len(new_group_members) == 0:
await self.delete()
self.empty = True
else:
self._entry["member"] = new_group_members
await self.modify()
async def get_members(self):
await self.sync()
for member in self._entry.get("member", []):
dn = parse_dn(member)
yield self.memeber_classes.get(dn["ou"])(self.client, dn["uid"])
class Congregations(mixins.OrganizationalUnit, mixins.Entry, mixins.Singleton):
_instances = dict() # type: ignore
ou = "Congregations"
child_class = Group
# We define this async method here **after** `User`, `Service` and `Group` have been
# defined, in order to avoid definition loops that would prevent the code from running.
# Indeed, this function explicitely uses `Group` but is needed as a `User` and `Service`
# method. In turn, `Group` definition relies on both `User` and `Service` being yet
# defined.
async def iter_groups(self):
async with self.client.connect(is_async=True) as conn:
res = await conn.search(f"{self.dn}", 2, attrlist=["memberOf"])
if not res or len(res) == 0:
return
elif len(res) == 1:
for group in res[0].get("memberOf", []):
yield Group(self.client, parse_dn(get_dn(group))["cn"])
else:
raise PhiUnexpectedRuntimeValue(
"return value should be no more than one", res
)
# Monkeypatch
User.iter_groups = iter_groups # type: ignore
Service.iter_groups = iter_groups # type: ignore

View File

@ -1,27 +1,74 @@
import os.path import os.path
import pkg_resources
import yaml import yaml
NAME = "phi"
NAME = 'phi' DEFAULT_CONFIG = {
"core": {"listen": {"host": "localhost", "port": 8080}},
"ldap": {
"host": "localhost",
"port": 389,
"encryption": "TLSv1.2",
"ciphers": "HIGH",
"validate": True,
"ca_certs": pkg_resources.resource_filename(NAME, "openldap/cert.pem"),
"username": None,
"password": None,
"base_dn": None,
"attribute_id": "uid",
"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"],},
"ldap3": {"level": "WARNING", "handlers": ["console", "file"],},
},
},
}
CONFIG_FILE = 'config.yml' DUMMY_CONFIG = {"core": {}, "ldap": {}, "logging": {}}
CONFIG_PATHS = ['./',
'~/.config/' + NAME + '/', CONFIG_FILE = "config.yml"
'/usr/local/etc/' + NAME + '/', CONFIG_PATHS = [
'/etc/' + NAME + '/'] "./",
CONFIG_FILES = [os.path.join(p, CONFIG_FILE) "~/.config/" + NAME + "/",
for p in CONFIG_PATHS] "/usr/local/etc/" + NAME + "/",
"/etc/" + NAME + "/",
]
CONFIG_FILES = [os.path.join(p, CONFIG_FILE) for p in CONFIG_PATHS]
def get_config(): def get_config(config_path=None):
"""Return the path of the found configuration file and its content """Return the path of the found configuration file and its content
:param config_path: optional path to a config file.
:returns: (path, config) :returns: (path, config)
:rtype: (str, dict) :rtype: (str, dict)
""" """
if config_path:
with open(config_path) as c:
config = yaml.safe_load(c)
return config_path, config
for f in CONFIG_FILES: for f in CONFIG_FILES:
try: try:
with open(f, 'r') as c: with open(f, "r") as c:
config = yaml.safe_load(c) config = yaml.safe_load(c)
return (f, config) return (f, config)
except FileNotFoundError: except FileNotFoundError:
@ -30,6 +77,49 @@ def get_config():
# accessible or if the file is not present at all # accessible or if the file is not present at all
# in any of CONFIG_PATHS. # in any of CONFIG_PATHS.
pass pass
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: else:
raise FileNotFoundError("Could not find {} in any of {}." if main is not None:
.format(CONFIG_FILE, ', '.join(CONFIG_PATHS))) _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"]

54
src/phi/exceptions.py Normal file
View 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})"

View File

@ -29,7 +29,7 @@ def get_entry_by_uid(client, uid):
return None return None
if response[1:]: if response[1:]:
log.erorr("Looking for exactly one result but server gave {}. " log.error("Looking for exactly one result but server gave {}. "
"Taking the first and ignoring the rest." "Taking the first and ignoring the rest."
.format(len(response))) .format(len(response)))

View File

@ -4,10 +4,10 @@ from phi.ldap.utils import flatten_attributes
def user_attributes_mapping(client): def user_attributes_mapping(client):
return { return {
client.attribute_id: 'uid', client.attribute_id: "uid",
client.attribute_mail: 'mail', client.attribute_mail: "mail",
'createTimestamp': 'created_at', "createTimestamp": "created_at",
'modifyTimestamp': 'modified_at' "modifyTimestamp": "modified_at",
} }
@ -19,8 +19,8 @@ def get_user_by_uid(client, uid):
mapping = user_attributes_mapping(client) mapping = user_attributes_mapping(client)
user = {mapping[k]: v user = {
for k, v in entry['attributes'].items() mapping[k]: v for k, v in entry["attributes"].items() if k in mapping.keys()
if k in mapping.keys()} }
return flatten_attributes(user) return flatten_attributes(user)

29
src/phi/security.py Normal file
View 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)

View File

@ -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
View File

@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
import asyncio
from pprint import pprint as pp
from phi.async_ldap.new_model import (
Hackers,
User,
Robots,
Service,
Group,
Congregations,
)
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 = Congregations(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 = Congregations(cl)
# asyncio.run(print_async("hackers:", describe(h)))
# asyncio.run(print_async("conte_mascetti:", describe(User(cl, "conte_mascetti"))))
# asyncio.run(print_async("robots:", describe(r)))
# asyncio.run(print_async("phi:", describe(Service(cl, "phi"))))
# asyncio.run(print_async("congregations:", describe(c)))
# asyncio.run(print_async("GitUsers:", describe(Group(cl, "GitUsers"))))
# asyncio.run(print_async("Hackers members:", get_members(h)))
# asyncio.run(print_async("Robots members:", get_members(r)))
# asyncio.run(print_async("Congregations members:", get_members(c)))
#
async def add_new(obj, name, **kw):
try:
_new = obj(cl, name, **kw)
await _new.save()
except e.PhiEntryExists as err:
print(f"Failed add: {repr(err)}")
async def safe_search(group, name):
try:
res = await group.search(name)
print("Search result:", res)
return res
except e.PhiEntryDoesNotExist as err:
print(f"Failed search: {repr(err)}")
async def safe_delete(obj, cascade=None):
try:
if cascade:
obj.delete_cascade = cascade
await obj.delete()
except Exception as err:
print(f"Failed delete: {repr(err)}")
async def add_member(group, member):
await group.add_member(member)
async def remove_member(group, member):
await group.remove_member(member)
# asyncio.run(safe_search(h, "pippo"))
# asyncio.run(
# add_new(User, "pippo", cn="Pippo (Goofy)", sn="Pippo", mail="pippo@unit.info")
# )
# asyncio.run(safe_search(h, "pippo"))
# asyncio.run(
# add_new(User, "pippo", cn="Pippo (Goofy)", sn="Pippo", mail="pippo@unit.net")
# )
# asyncio.run(safe_delete(asyncio.run(safe_search(h, "pippo"))))
# asyncio.run(print_async("Hackers members:", get_members(h)))
# asyncio.run(safe_delete(h))
# asyncio.run(print_async("Hackers members:", get_members(h)))
# asyncio.run(safe_delete(h, True))
# asyncio.run(print_async("Hackers members:", get_members(h)))
# asyncio.run(safe_search(r, "phi"))
# asyncio.run(print_async("Robots members:", get_members(r)))
# asyncio.run(add_new(Service, "db", userPassword="lolpassword"))
# asyncio.run(print_async("Robots members:", get_members(r)))
asyncio.run(safe_search(c, "GitUsers"))
asyncio.run(print_async("Congregations members:", get_members(c)))
asyncio.run(
add_new(Group, "naughty", member=[User(cl, "conte_mascetti"), User(cl, "necchi")])
)
asyncio.run(print_async("Congregations members:", get_members(c)))
asyncio.run(safe_delete(Group(cl, "naughty")))
asyncio.run(print_async("Congregations members:", get_members(c)))
asyncio.run(
add_new(Group, "naughty", member=[User(cl, "conte_mascetti"), User(cl, "necchi")])
)
asyncio.run(print_async("Congregations members:", get_members(c)))
print("==> HERE <==")
naughty = sync_await(Group(cl, "naughty").sync())
print("NAUGHTY =>>", [m for m in naughty.get_members()])
asyncio.run(add_member(naughty, User(cl, "perozzi")))
print("NAUGHTY =>>", [m for m in naughty.get_members()])
asyncio.run(remove_member(naughty, User(cl, "conte_mascetti")))
print("NAUGHTY =>>", [m for m in naughty.get_members()])