Compare commits
64 Commits
Author | SHA1 | Date | |
---|---|---|---|
c5a4b86349 | |||
b0e5d00994 | |||
401f01f110 | |||
30c46c059a | |||
b400127cc6 | |||
6bd1beba9e | |||
e202d54a7d | |||
67c7830ba4 | |||
95bc52ebd8 | |||
ac0ab02d6e | |||
d024232d66 | |||
166b542846 | |||
d87c89a075 | |||
b7316ff513 | |||
67c83975d1 | |||
205c87dc49 | |||
c05b023bdb | |||
1b97d6d7ca | |||
b979002f78 | |||
e41e03f464 | |||
d9a6db63d7 | |||
6fea75022f | |||
12f37b3a55 | |||
a0cf7e9603 | |||
8779db9ca0 | |||
e454fbd84a | |||
66885641c4 | |||
a22f459915 | |||
08c45b54f2 | |||
2bc7f0b75f | |||
d9e8eb23e3 | |||
ce4c085e3f | |||
7634f0f530 | |||
79f682cbb7 | |||
2b46eb0353 | |||
fed48022af | |||
3454c194b1 | |||
f05fe8a0d5 | |||
ec472218c4 | |||
79d7dcc653 | |||
fe3450b886 | |||
f558492975 | |||
706f109faf | |||
0f7882a387 | |||
1be1aac9d0 | |||
7e6b757e3a | |||
ed8af40392 | |||
2cf07d6732 | |||
8dce6566ee | |||
b466bf8ed2 | |||
d0cba75ee0 | |||
fd729170d3 | |||
c4046a83ff | |||
4e2cadaa92 | |||
b1837f80e4 | |||
27fc927254 | |||
a434ff9b4c | |||
422d238fc1 | |||
b0f312284d | |||
543416368b | |||
bdd4a39531 | |||
e67b97b214 | |||
c8eb5c2dd4 | |||
80fb51f7de |
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -90,6 +90,9 @@ ENV/
|
|||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Personal local venv standard ("*" matches the version number)
|
||||
/cpy*
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
|
22
Pipfile
22
Pipfile
|
@ -4,9 +4,25 @@ url = "https://pypi.org/simple"
|
|||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
ipython = "*"
|
||||
pytest = "*"
|
||||
ipdb = "*"
|
||||
async-generator = "*"
|
||||
pytest-cov = "*"
|
||||
pytest-asyncio = "*"
|
||||
pytest-aiohttp = "*"
|
||||
mock = "*"
|
||||
yarl = {editable = true, path = "."}
|
||||
pytest-integration = "*"
|
||||
|
||||
[packages]
|
||||
phi = {editable = true,path = "."}
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
aiohttp = "2.3.8"
|
||||
click = "7.0"
|
||||
pyYAML = "*"
|
||||
ldap3 = "*"
|
||||
bonsai = "==1.2.0"
|
||||
passlib = "==1.7.1"
|
||||
bcrypt = "==3.1.7"
|
||||
multidict = "*"
|
||||
iniconfig = "*"
|
||||
|
|
903
Pipfile.lock
generated
903
Pipfile.lock
generated
|
@ -1,12 +1,10 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "0fe9733eafff036659c506639ac835b824b653cd2f121c82f2726ba2ea6bdb06"
|
||||
"sha256": "38260864a710721e0938c9510ecf6eaf63d7cf1f31f445ce28be2cabe93a5888"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.7"
|
||||
},
|
||||
"requires": {},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
|
@ -35,6 +33,7 @@
|
|||
"sha256:c7b8a47315e8c0b79a008e33eca4299baa002efd4b53ace068f0894133a8933e",
|
||||
"sha256:e81997b2fb4b7f19a80257aa2bb6e35e521d62dcf595599bf34886b115607bad"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.3.8"
|
||||
},
|
||||
"async-timeout": {
|
||||
|
@ -42,62 +41,184 @@
|
|||
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
||||
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
|
||||
],
|
||||
"markers": "python_full_version >= '3.5.3'",
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:3f67c83185b1f0df8fdf6b52fa42c55bc9e9b7120c8b7fec60f0d6003c536d18",
|
||||
"sha256:dd9be8ea27773c4ffc18ede0b95c3ca1eb12513a184590b9f8ae423db3f71eb9"
|
||||
"sha256:10bdd23b612e942ce90ea4dbc744dfd88735949833e46c5467a2dcf68e60f469",
|
||||
"sha256:37d633e20fa360c302b1263c96fe932d40622d0119f1bddcb829b03462eeeeb7",
|
||||
"sha256:7c3738570766f5e5e74a56fade15470f339d5c436d821cf476ef27da0a4de8b0",
|
||||
"sha256:8f59a7b5399555b22db06f153daa76c77ded2dd84bc0f0ffe5b0b33901b6eac4",
|
||||
"sha256:bed71c6ce2f70a00a330eed0c8370664c065239d45bcbe1b82517b6f6eed7f25"
|
||||
],
|
||||
"version": "==2.5.2"
|
||||
"index": "pypi",
|
||||
"version": "==2.8.1"
|
||||
},
|
||||
"multidict": {
|
||||
"hashes": [
|
||||
"sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f",
|
||||
"sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3",
|
||||
"sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef",
|
||||
"sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b",
|
||||
"sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73",
|
||||
"sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc",
|
||||
"sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3",
|
||||
"sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd",
|
||||
"sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351",
|
||||
"sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941",
|
||||
"sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d",
|
||||
"sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1",
|
||||
"sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b",
|
||||
"sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a",
|
||||
"sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3",
|
||||
"sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7",
|
||||
"sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0",
|
||||
"sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0",
|
||||
"sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014",
|
||||
"sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5",
|
||||
"sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036",
|
||||
"sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d",
|
||||
"sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a",
|
||||
"sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce",
|
||||
"sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1",
|
||||
"sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a",
|
||||
"sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9",
|
||||
"sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7",
|
||||
"sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b"
|
||||
"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"
|
||||
],
|
||||
"version": "==4.5.2"
|
||||
"index": "pypi",
|
||||
"version": "==5.1.0"
|
||||
},
|
||||
"passlib": {
|
||||
"hashes": [
|
||||
"sha256:3d948f64138c25633613f303bcc471126eae67c04d5e3f6b7b8ce6242f8653e0",
|
||||
"sha256:43526aea08fa32c6b6dbbbe9963c4c767285b78147b7437597f992812f69d280"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.7.1"
|
||||
},
|
||||
"phi": {
|
||||
"editable": true,
|
||||
|
@ -105,43 +226,683 @@
|
|||
},
|
||||
"pyasn1": {
|
||||
"hashes": [
|
||||
"sha256:da2420fe13a9452d8ae97a0e478adde1dee153b11ba832a95b223a2ba01c10f7",
|
||||
"sha256:da6b43a8c9ae93bc80e2739efb38cc776ba74a886e3e9318d65fe81a8b8a2c6e"
|
||||
"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.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": {
|
||||
"hashes": [
|
||||
"sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b",
|
||||
"sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf",
|
||||
"sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a",
|
||||
"sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3",
|
||||
"sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1",
|
||||
"sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1",
|
||||
"sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613",
|
||||
"sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04",
|
||||
"sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f",
|
||||
"sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537",
|
||||
"sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"
|
||||
"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"
|
||||
],
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9",
|
||||
"sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f",
|
||||
"sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb",
|
||||
"sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320",
|
||||
"sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842",
|
||||
"sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0",
|
||||
"sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829",
|
||||
"sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310",
|
||||
"sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4",
|
||||
"sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8",
|
||||
"sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1"
|
||||
"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"
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
47
README.md
47
README.md
|
@ -9,50 +9,3 @@ APIs for the Unit hacklab.
|
|||
Requirements:
|
||||
|
||||
* Python >= 3.5
|
||||
|
||||
|
||||
Create a virtual environment and activate it (optional):
|
||||
```
|
||||
virtualenv --python=/usr/bin/python3 env
|
||||
source env/bin/activate
|
||||
```
|
||||
|
||||
Run the setup:
|
||||
```
|
||||
python setup.py install
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
In the ldap section of `config.yml` change host, port and password according to
|
||||
your setup.
|
||||
|
||||
|
||||
## Command Line
|
||||
|
||||
```
|
||||
usage: phicli [-h] [--config config.yml]
|
||||
{showuser,adduser,deluser,showgroup,listgroups,addtogroup} ...
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--config config.yml custom configuration file
|
||||
|
||||
actions:
|
||||
showuser dispaly user fields
|
||||
adduser add a new user
|
||||
deluser delete an user
|
||||
showgroup show a group
|
||||
listgroups list all groups
|
||||
addtogroup add an user to a group
|
||||
```
|
||||
|
||||
```
|
||||
phicli showuser [-h] user_id
|
||||
phicli adduser [-h] user_id
|
||||
phicli deluser [-h] user_id
|
||||
|
||||
phicli showgroup [-h] common_name
|
||||
phicli listgroups [-h]
|
||||
phicli addtogroup [-h] user_id group_common_name
|
||||
```
|
||||
|
|
117
async_tests/test_async_ldap_client.py
Normal file
117
async_tests/test_async_ldap_client.py
Normal file
|
@ -0,0 +1,117 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from contextlib import contextmanager
|
||||
import logging
|
||||
|
||||
import mock
|
||||
import pytest
|
||||
|
||||
from phi.async_ldap.client import (
|
||||
parse_host,
|
||||
checked_port,
|
||||
compose_dn_username,
|
||||
AsyncClient,
|
||||
)
|
||||
|
||||
BASE_DN = "dc=unit,dc=macaomilano,dc=org"
|
||||
|
||||
|
||||
@contextmanager
|
||||
def does_not_raise():
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_url, exp_proto, exp_addr, exp_port",
|
||||
[
|
||||
("1.3.1.2", "ldap", "1.3.1.2", 389),
|
||||
("ldap://localhost:1312", "ldap", "localhost", 1312),
|
||||
("localhost:1312", "ldap", "localhost", 1312),
|
||||
("localhost", "ldap", "localhost", 389),
|
||||
("ldap://localhost", "ldap", "localhost", 389),
|
||||
("ldaps://localhost", "ldaps", "localhost", 636),
|
||||
("ldaps://localhost:1312", "ldaps", "localhost", 1312),
|
||||
],
|
||||
)
|
||||
def test_parse_host(test_url, exp_proto, exp_addr, exp_port):
|
||||
proto, addr, port = parse_host(test_url)
|
||||
|
||||
assert proto == exp_proto
|
||||
assert addr == exp_addr
|
||||
assert port == exp_port
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"manual, auto, exp_port", [(None, 389, 389), (1312, 389, 1312), (1312, 1312, 1312)]
|
||||
)
|
||||
def test_checked_port(manual, auto, exp_port, caplog):
|
||||
port = checked_port(manual, auto)
|
||||
if manual and manual != auto:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
"The former prevails" in caplog.text
|
||||
|
||||
assert port == exp_port
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"username, base_dn, ou, attribute_id, exp_dn",
|
||||
[
|
||||
(
|
||||
f"uid=conte_mascetti,{BASE_DN}",
|
||||
BASE_DN,
|
||||
None,
|
||||
"uid",
|
||||
f"uid=conte_mascetti,{BASE_DN}",
|
||||
),
|
||||
("root", BASE_DN, None, "cn", f"cn=root,{BASE_DN}"),
|
||||
("necchi", BASE_DN, "Hackers", "uid", f"uid=necchi,ou=Hackers,{BASE_DN}"),
|
||||
("perozzi", BASE_DN, "Phrackers", "cn", f"cn=perozzi,ou=Phrackers,{BASE_DN}"),
|
||||
],
|
||||
)
|
||||
def test_compose_dn_username(username, base_dn, ou, attribute_id, exp_dn):
|
||||
dn = compose_dn_username(username, base_dn, ou, attribute_id)
|
||||
|
||||
assert dn == exp_dn
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url, encryption, validate, ca_cert, expectation",
|
||||
[
|
||||
("localhost", None, False, None, does_not_raise()),
|
||||
("localhost", True, False, None, does_not_raise()),
|
||||
("localhost", False, True, None, does_not_raise()),
|
||||
("localhost", True, True, "path/to/cert.pem", does_not_raise()),
|
||||
("ldaps://localhost", False, False, None, pytest.raises(ValueError)),
|
||||
],
|
||||
)
|
||||
def test_AsyncClient_init(url, encryption, validate, ca_cert, expectation):
|
||||
with expectation as exp:
|
||||
cl = AsyncClient(
|
||||
host=url,
|
||||
port=389,
|
||||
encryption=encryption,
|
||||
ciphers=None,
|
||||
validate=validate,
|
||||
ca_cert=ca_cert,
|
||||
username="conte_mascetti",
|
||||
password="pass",
|
||||
base_dn=BASE_DN,
|
||||
ou="Hackers",
|
||||
)
|
||||
|
||||
if exp is not None:
|
||||
assert "Incompatible provided protocol" in str(exp.value)
|
||||
return
|
||||
|
||||
assert cl.base_dn == BASE_DN
|
||||
assert url in cl.full_uri
|
||||
assert "389" in cl.full_uri
|
||||
assert cl._tls if encryption else not cl._tls
|
||||
if validate:
|
||||
assert cl.cert_policy == -1
|
||||
else:
|
||||
assert cl.cert_policy == 0
|
||||
if ca_cert:
|
||||
assert cl.ca_cert == ca_cert
|
||||
else:
|
||||
assert cl.ca_cert == ""
|
775
async_tests/test_async_ldap_model.py
Normal file
775
async_tests/test_async_ldap_model.py
Normal 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
|
196
async_tests/test_async_ldap_new_model.py
Normal file
196
async_tests/test_async_ldap_new_model.py
Normal file
|
@ -0,0 +1,196 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
import asyncio
|
||||
|
||||
from async_generator import asynccontextmanager
|
||||
from bonsai import LDAPEntry
|
||||
import mock
|
||||
import pytest
|
||||
|
||||
from phi.async_ldap.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)
|
13
config.yml
13
config.yml
|
@ -11,15 +11,14 @@ ldap:
|
|||
|
||||
encryption: TLSv1.2 # Can either be None or TLSv1.2. Default: None
|
||||
ciphers: "HIGH"
|
||||
validate: True # Can either be True or False. Default: False
|
||||
validate: False # Can either be True or False. Default: False
|
||||
ca_certs: openldap/cert.pem
|
||||
|
||||
# username: uid=phi,ou=Services,dc=unit,dc=macaomilano,dc=org
|
||||
# password: phi
|
||||
username: cn=root,dc=unit,dc=macaomilano,dc=org
|
||||
password: root
|
||||
username: uid=phi,ou=Services,dc=unit,dc=macaomilano,dc=org
|
||||
password: phi
|
||||
|
||||
base_dn: dc=unit,dc=macaomilano,dc=org
|
||||
attribute_id: uid
|
||||
|
||||
|
||||
logging:
|
||||
|
@ -40,10 +39,10 @@ logging:
|
|||
|
||||
loggers:
|
||||
phi:
|
||||
level: WARNING
|
||||
level: DEBUG
|
||||
handlers: [console, file]
|
||||
aiohttp:
|
||||
level: WARNING
|
||||
level: DEBUG
|
||||
handlers: [console, file]
|
||||
ldap3:
|
||||
level: WARNING
|
||||
|
|
362
integration_tests/test_model.py
Normal file
362
integration_tests/test_model.py
Normal 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
|
|
@ -1,5 +1,7 @@
|
|||
FROM alpine:3.7
|
||||
|
||||
ENV LDAPTLS_REQCERT=never
|
||||
|
||||
RUN apk add --no-cache \
|
||||
openldap \
|
||||
openldap-back-mdb \
|
||||
|
|
|
@ -16,6 +16,8 @@ gen-cert:
|
|||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
docker rm $(CONTAINER) || true
|
||||
docker rmi unit/slapd
|
||||
rm -f key.pem cert.pem
|
||||
|
||||
.PHONY: run
|
||||
|
@ -35,6 +37,10 @@ prepare:
|
|||
run-bg:
|
||||
make prepare
|
||||
|
||||
.PHONY: logs
|
||||
logs:
|
||||
docker logs -f phi_slapd
|
||||
|
||||
.PHONY: stop
|
||||
stop: is-running
|
||||
docker stop $(CONTAINER)
|
||||
|
|
|
@ -11,36 +11,93 @@ objectClass: organizationalUnit
|
|||
objectClass: top
|
||||
ou: Hackers
|
||||
|
||||
dn: ou=Services,dc=unit,dc=macaomilano,dc=org
|
||||
dn: ou=Robots,dc=unit,dc=macaomilano,dc=org
|
||||
objectClass: top
|
||||
objectClass: organizationalUnit
|
||||
ou: Services
|
||||
ou: Robots
|
||||
|
||||
dn: ou=Groups,dc=unit,dc=macaomilano,dc=org
|
||||
dn: ou=Congregations,dc=unit,dc=macaomilano,dc=org
|
||||
objectClass: top
|
||||
objectClass: organizationalUnit
|
||||
ou: Groups
|
||||
ou: Congregations
|
||||
|
||||
dn: uid=phi,ou=Services,dc=unit,dc=macaomilano,dc=org
|
||||
dn: uid=phi,ou=Robots,dc=unit,dc=macaomilano,dc=org
|
||||
objectClass: account
|
||||
objectClass: simpleSecurityObject
|
||||
objectClass: top
|
||||
uid: phi
|
||||
userPassword: {SHA}REu9CtcqSaA1c5J+sEYlTgg0H+M=
|
||||
|
||||
dn: uid=irc,ou=Robots,dc=unit,dc=macaomilano,dc=org
|
||||
objectClass: account
|
||||
objectClass: simpleSecurityObject
|
||||
objectClass: top
|
||||
uid: irc
|
||||
userPassword: {SHA}0WvxFW9MSsesf55SOh4vnuwdkgY=
|
||||
|
||||
dn: uid=git,ou=Robots,dc=unit,dc=macaomilano,dc=org
|
||||
objectClass: account
|
||||
objectClass: simpleSecurityObject
|
||||
objectClass: top
|
||||
uid: git
|
||||
userPassword: {SHA}RvGgvVWSovkkTKMhsSmQKga1PgM=
|
||||
|
||||
dn: uid=conte_mascetti,ou=Hackers,dc=unit,dc=macaomilano,dc=org
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: organizationalPerson
|
||||
objectClass: person
|
||||
objectClass: top
|
||||
memberOf: cn=Admins,ou=Congregations,dc=unit,dc=macaomilano,dc=org
|
||||
memberOf: cn=GitUsers,ou=Congregations,dc=unit,dc=macaomilano,dc=org
|
||||
memberOf: cn=IRCUsers,ou=Congregations,dc=unit,dc=macaomilano,dc=org
|
||||
cn: Raffaello
|
||||
sn: Mascetti
|
||||
mail: rmascetti@autistici.org
|
||||
uid: conte_mascetti
|
||||
userPassword: {SHA}oLY7P6V+DWaMJhix7vbMYGIfA+E=
|
||||
|
||||
dn: cn=WikiUsers,ou=Groups,dc=unit,dc=macaomilano,dc=org
|
||||
dn: uid=necchi,ou=Hackers,dc=unit,dc=macaomilano,dc=org
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: organizationalPerson
|
||||
objectClass: person
|
||||
objectClass: top
|
||||
memberOf: cn=GitUsers,ou=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
|
||||
cn: WikiUsers
|
||||
|
||||
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
3
pyproject.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta:__legacy__"
|
|
@ -1,7 +1,11 @@
|
|||
[aliases]
|
||||
test=pytest
|
||||
|
||||
[pycodestyle]
|
||||
max-line-length=88
|
||||
|
||||
[flake8]
|
||||
max-line-length=88
|
||||
exclude =
|
||||
.git,
|
||||
__pycache__,
|
||||
|
|
39
setup.py
39
setup.py
|
@ -1,22 +1,25 @@
|
|||
from setuptools import setup
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name='phi',
|
||||
version='0.0.1',
|
||||
|
||||
description='Post-Human Interface',
|
||||
name="phi",
|
||||
version="0.0.1",
|
||||
description="Post-Human Interface",
|
||||
# license='',
|
||||
url='https://git.abbiamoundominio.org/unit/phi',
|
||||
|
||||
author='unit',
|
||||
author_email='unit@paranoici.org',
|
||||
|
||||
package_dir={'': 'src'},
|
||||
packages=['phi', 'phi.api', 'phi.ldap'],
|
||||
scripts=['src/phid', 'src/phicli'],
|
||||
|
||||
setup_requires=['pytest-runner'],
|
||||
install_requires=['pyYAML', 'ldap3'],
|
||||
tests_require=['pytest']
|
||||
url="https://git.abbiamoundominio.org/unit/phi",
|
||||
author="unit",
|
||||
author_email="unit@paranoici.org",
|
||||
package_dir={"": "src"},
|
||||
packages=find_packages("src"),
|
||||
entry_points={"console_scripts": ["phid=phi.app:cli"]},
|
||||
setup_requires=["pytest-runner"],
|
||||
install_requires=[
|
||||
"aiohttp==2.3.8",
|
||||
"click==7.0",
|
||||
"pyYAML",
|
||||
"ldap3",
|
||||
"bonsai==1.2.0",
|
||||
"passlib==1.7.1",
|
||||
"bcrypt==3.1.7",
|
||||
],
|
||||
tests_require=["pytest", "pytest-aiohttp", "pytest-asyncio", "async-generator"],
|
||||
)
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
import logging
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
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
|
||||
|
||||
log = get_logger(__name__)
|
||||
alog = logging.getLogger("asyncio")
|
||||
|
||||
|
||||
def api_startup(app):
|
||||
app['ldap_client'].open()
|
||||
app["ldap_client"].open()
|
||||
|
||||
|
||||
def api_shutdown(app):
|
||||
app['ldap_client'].close()
|
||||
app["ldap_client"].close()
|
||||
|
||||
|
||||
def api_app(config):
|
||||
|
@ -20,8 +25,12 @@ def api_app(config):
|
|||
|
||||
app = web.Application()
|
||||
|
||||
ldap_client = Client(**config.get('ldap', {}))
|
||||
app['ldap_client'] = ldap_client
|
||||
ldap_client = AsyncClient(**config.get("ldap", {}))
|
||||
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_shutdown.append(api_shutdown)
|
||||
|
|
|
@ -2,23 +2,26 @@ from aiohttp.web import json_response, View
|
|||
from aiohttp.web import HTTPNotFound, HTTPUnprocessableEntity
|
||||
|
||||
from phi.logging import get_logger
|
||||
from phi.ldap.user import get_user_by_uid
|
||||
from phi.api.utils import serialize
|
||||
from phi.async_ldap.model import Hackers
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
class User(View):
|
||||
class UserView(View):
|
||||
async def get(self):
|
||||
uid = self.request.match_info.get('uid', None)
|
||||
uid = self.request.match_info.get("uid", None)
|
||||
|
||||
if uid is None:
|
||||
return HTTPUnprocessableEntity()
|
||||
|
||||
client = self.request.app['ldap_client']
|
||||
user = get_user_by_uid(client, uid)
|
||||
user = await self.request.app["users"].get_by_uid(uid)
|
||||
self.request.app["alog"].info("Found user %s", user)
|
||||
|
||||
if not user:
|
||||
return HTTPNotFound()
|
||||
|
||||
return json_response(serialize(user))
|
||||
result = await user.describe()
|
||||
self.request.app["alog"].debug("Returning result %s", result)
|
||||
|
||||
return json_response(result)
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
from aiohttp.web import route
|
||||
|
||||
from phi.api.rest import User
|
||||
|
||||
from phi.api.rest import UserView
|
||||
|
||||
api_routes = [
|
||||
route('*', '/user', User),
|
||||
route('*', '/user/', User),
|
||||
route('*', '/user/{uid}', User)
|
||||
route("*", "/user", UserView),
|
||||
route("*", "/user/", UserView),
|
||||
route("*", "/user/{uid}", UserView),
|
||||
]
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from datetime import datetime
|
||||
|
||||
|
||||
def serialize(d):
|
||||
return {k: (v.isoformat() if isinstance(v, datetime) else v)
|
||||
for k, v in d.items()}
|
||||
def serialize(obj):
|
||||
return {
|
||||
k: (v.isoformat() if isinstance(v, datetime) else v)
|
||||
for k, v in dict(obj).items()
|
||||
}
|
||||
|
|
200
src/phi/app.py
200
src/phi/app.py
|
@ -1,22 +1,212 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
import pkg_resources
|
||||
|
||||
from asyncio import get_event_loop
|
||||
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
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
def setup_app(config):
|
||||
loop = get_event_loop()
|
||||
|
||||
app = web.Application(loop=loop)
|
||||
app['config'] = config
|
||||
app["config"] = config
|
||||
|
||||
api = api_app(config)
|
||||
app.add_subapp('/api', api)
|
||||
app.add_subapp("/api", api)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def run_app(app):
|
||||
web.run_app(app,
|
||||
host=app['config']['core']['listen'].get('host', '127.0.0.1'),
|
||||
port=app['config']['core']['listen'].get('port', '8080'))
|
||||
web.run_app(
|
||||
app,
|
||||
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
|
||||
|
|
0
src/phi/async_ldap/__init__.py
Normal file
0
src/phi/async_ldap/__init__.py
Normal file
138
src/phi/async_ldap/client.py
Normal file
138
src/phi/async_ldap/client.py
Normal 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
|
340
src/phi/async_ldap/mixins.py
Normal file
340
src/phi/async_ldap/mixins.py
Normal 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
440
src/phi/async_ldap/model.py
Normal 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.")
|
137
src/phi/async_ldap/new_model.py
Normal file
137
src/phi/async_ldap/new_model.py
Normal 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
|
|
@ -1,47 +0,0 @@
|
|||
import sys
|
||||
import argparse
|
||||
import inspect
|
||||
from phi.logging import setup_logging, get_logger
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
subparses = parser.add_subparsers(title='actions', dest='action')
|
||||
|
||||
cli_callbacks = {}
|
||||
|
||||
|
||||
def register(action_info='', param_infos=[]):
|
||||
def decorator(action):
|
||||
# Get function name and arguments
|
||||
action_name = action.__name__
|
||||
param_names = inspect.getfullargspec(action)[0]
|
||||
|
||||
# Create subparser for specific action
|
||||
subparser = subparses.add_parser(action_name, help=action_info)
|
||||
|
||||
for i, name in enumerate(param_names):
|
||||
info = param_infos[i] if i<len(param_infos) else ''
|
||||
subparser.add_argument(dest=name, help=info)
|
||||
|
||||
# Register action
|
||||
cli_callbacks[action_name] = action, param_names
|
||||
return action
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def run(args):
|
||||
for action_name, (action, param_names) in cli_callbacks.items():
|
||||
if args['action'] == action_name:
|
||||
action(**{pname: args[pname] for pname in param_names})
|
||||
|
||||
|
||||
def add_arg(name, example, info):
|
||||
parser.add_argument(name, metavar=example, help=info)
|
||||
|
||||
|
||||
def get_args():
|
||||
namespace = parser.parse_args(sys.argv[1:])
|
||||
args = namespace.__dict__
|
||||
return args
|
|
@ -1,31 +1,74 @@
|
|||
import os.path
|
||||
import pkg_resources
|
||||
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'
|
||||
CONFIG_PATHS = ['./',
|
||||
'~/.config/' + NAME + '/',
|
||||
'/usr/local/etc/' + NAME + '/',
|
||||
'/etc/' + NAME + '/']
|
||||
CONFIG_FILES = [os.path.join(p, CONFIG_FILE)
|
||||
for p in CONFIG_PATHS]
|
||||
DUMMY_CONFIG = {"core": {}, "ldap": {}, "logging": {}}
|
||||
|
||||
CONFIG_FILE = "config.yml"
|
||||
CONFIG_PATHS = [
|
||||
"./",
|
||||
"~/.config/" + NAME + "/",
|
||||
"/usr/local/etc/" + NAME + "/",
|
||||
"/etc/" + NAME + "/",
|
||||
]
|
||||
CONFIG_FILES = [os.path.join(p, CONFIG_FILE) for p in CONFIG_PATHS]
|
||||
|
||||
|
||||
def get_config(custom_config=None):
|
||||
def get_config(config_path=None):
|
||||
"""Return the path of the found configuration file and its content
|
||||
|
||||
:param config_path: optional path to a config file.
|
||||
|
||||
:returns: (path, config)
|
||||
:rtype: (str, dict)
|
||||
"""
|
||||
if custom_config:
|
||||
global CONFIG_FILES
|
||||
CONFIG_FILES = [custom_config]
|
||||
|
||||
if config_path:
|
||||
with open(config_path) as c:
|
||||
config = yaml.safe_load(c)
|
||||
return config_path, config
|
||||
for f in CONFIG_FILES:
|
||||
try:
|
||||
with open(f, 'r') as c:
|
||||
with open(f, "r") as c:
|
||||
config = yaml.safe_load(c)
|
||||
return (f, config)
|
||||
except FileNotFoundError:
|
||||
|
@ -34,11 +77,49 @@ def get_config(custom_config=None):
|
|||
# accessible or if the file is not present at all
|
||||
# in any of CONFIG_PATHS.
|
||||
pass
|
||||
else:
|
||||
if custom_config:
|
||||
raise FileNotFoundError('Config file {} not found.'
|
||||
.format(custom_config))
|
||||
return None, DUMMY_CONFIG
|
||||
|
||||
|
||||
def merge_config(cli_config, file_config):
|
||||
"""
|
||||
Merge the cli-provided and file-provided config.
|
||||
"""
|
||||
return recursive_merge(cli_config, file_config)
|
||||
|
||||
|
||||
def _init_with_shape_of(element):
|
||||
if isinstance(element, dict):
|
||||
return {}
|
||||
elif isinstance(element, list):
|
||||
return []
|
||||
return None
|
||||
|
||||
|
||||
def recursive_merge(main_config, aux_config):
|
||||
def _recursive_merge(main, aux, default, key, _ret_config):
|
||||
if isinstance(default, dict):
|
||||
_sub_conf = {}
|
||||
for k, v in default.items():
|
||||
_main = main[k] if k in main else _init_with_shape_of(v)
|
||||
_aux = aux[k] if k in aux else _init_with_shape_of(v)
|
||||
_recursive_merge(_main, _aux, v, k, _sub_conf)
|
||||
_ret_config[key] = _sub_conf
|
||||
elif isinstance(default, list):
|
||||
_main = main.copy()
|
||||
if aux is not None:
|
||||
_main.extend(aux)
|
||||
_ret_config[key] = list(set(_main))
|
||||
elif isinstance(default, bool):
|
||||
_ret_config[key] = default and aux and main
|
||||
else:
|
||||
raise FileNotFoundError("Could not find {} in any of {}."
|
||||
.format(CONFIG_FILE,
|
||||
', '.join(CONFIG_PATHS)))
|
||||
if main is not None:
|
||||
_ret_config[key] = main
|
||||
elif aux is not None:
|
||||
_ret_config[key] = aux
|
||||
else:
|
||||
_ret_config[key] = default
|
||||
|
||||
_config = {}
|
||||
_recursive_merge(main_config, aux_config, DEFAULT_CONFIG, "ROOT", _config)
|
||||
|
||||
return _config["ROOT"]
|
||||
|
|
54
src/phi/exceptions.py
Normal file
54
src/phi/exceptions.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
|
||||
|
||||
class PhiEntryExists(Exception):
|
||||
def __init__(self, dn):
|
||||
self.dn = dn
|
||||
|
||||
def __str__(self):
|
||||
return f"Entry exists yet ({self.dn})"
|
||||
|
||||
def __repr__(self):
|
||||
return f"PhiEntryExists({self.dn})"
|
||||
|
||||
|
||||
class PhiEntryDoesNotExist(Exception):
|
||||
def __init__(self, dn):
|
||||
self.dn = dn
|
||||
|
||||
def __str__(self):
|
||||
return f"Entry does not exist ({self.dn})"
|
||||
|
||||
def __repr__(self):
|
||||
return f"PhiEntryDoesNotExist({self.dn})"
|
||||
|
||||
|
||||
class PhiAttributeMissing(Exception):
|
||||
def __init__(self, dn, attr):
|
||||
self.dn = dn
|
||||
self.attr = attr
|
||||
|
||||
|
||||
class PhiUnauthorized(Exception):
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.user = user
|
||||
|
||||
|
||||
class PhiUnexpectedRuntimeValue(RuntimeWarning):
|
||||
def __init__(self, msg, result):
|
||||
super().__init__(self)
|
||||
self.msg = msg
|
||||
self.result = result
|
||||
|
||||
|
||||
class PhiCannotExecute(RuntimeWarning):
|
||||
def __init__(self, msg):
|
||||
super().__init__(self)
|
||||
self.msg = msg
|
||||
|
||||
def __str__(self):
|
||||
return f"Cannot execute: {self.msg}"
|
||||
|
||||
def __repr__(self):
|
||||
return f"PhiCannotExecute({self.msg})"
|
|
@ -14,7 +14,8 @@ class Client:
|
|||
host=None, port=389,
|
||||
encryption=None, ciphers=None, validate=False, ca_certs=None,
|
||||
username=None, password=None,
|
||||
base_dn=None):
|
||||
base_dn=None,
|
||||
attribute_id='uid', attribute_mail='mail'):
|
||||
log.info("Initializing LDAP Client.")
|
||||
|
||||
self.host = host
|
||||
|
@ -30,6 +31,9 @@ class Client:
|
|||
|
||||
self.base_dn = base_dn
|
||||
|
||||
self.attribute_id = attribute_id
|
||||
self.attribute_mail = attribute_mail
|
||||
|
||||
self.connection_lock = Lock()
|
||||
self.connection = make_connection(host=self.host, port=self.port,
|
||||
encryption=self.encryption,
|
||||
|
|
36
src/phi/ldap/entry.py
Normal file
36
src/phi/ldap/entry.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES
|
||||
|
||||
from phi.logging import get_logger
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
def get_entry_by_uid(client, uid):
|
||||
log.info("Searching entry with identifier: {}".format(uid))
|
||||
|
||||
filter_ = "({}={})".format(client.attribute_id, uid)
|
||||
log.debug("Search filter: {}".format(filter_))
|
||||
|
||||
response_id = client.connection.search(
|
||||
client.base_dn, filter_,
|
||||
search_scope='SUBTREE',
|
||||
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES]
|
||||
)
|
||||
|
||||
response, result, request = client.connection.get_response(
|
||||
response_id, get_request=True
|
||||
)
|
||||
|
||||
log.debug("Request: {}".format(request))
|
||||
log.debug("Response: {}".format(response))
|
||||
log.debug("Result: {}".format(result))
|
||||
|
||||
if not response:
|
||||
return None
|
||||
|
||||
if response[1:]:
|
||||
log.error("Looking for exactly one result but server gave {}. "
|
||||
"Taking the first and ignoring the rest."
|
||||
.format(len(response)))
|
||||
|
||||
return response[0]
|
|
@ -1,61 +0,0 @@
|
|||
from ldap3 import ALL_ATTRIBUTES, MODIFY_ADD
|
||||
from phi.ldap.utils import get_response, make_group_dict
|
||||
from phi.logging import get_logger
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
def get_group_by_cn(client, cn):
|
||||
log.info("Searching groups with common name: {}".format(cn))
|
||||
|
||||
dn = 'cn={},ou=Groups,{}'.format(cn, client.base_dn)
|
||||
log.debug("Search dn: {}".format(dn))
|
||||
|
||||
response_id = client.connection.search(
|
||||
dn, '(objectclass=groupOfNames)',
|
||||
search_scope='SUBTREE',
|
||||
attributes=[ALL_ATTRIBUTES]
|
||||
)
|
||||
|
||||
response = get_response(client, response_id)
|
||||
|
||||
if not response:
|
||||
return None
|
||||
|
||||
if len(response) > 1:
|
||||
log.error("Looking for exactly one result but server gave {}. "
|
||||
"Taking the first and ignoring the rest."
|
||||
.format(len(response)))
|
||||
|
||||
group = make_group_dict(client, response[0])
|
||||
return group
|
||||
|
||||
|
||||
def get_all_groups(client):
|
||||
log.info("Searching all the groups")
|
||||
dn = 'ou=Groups,{}'.format(client.base_dn)
|
||||
|
||||
log.debug("Search dn: {}".format(dn))
|
||||
|
||||
response_id = client.connection.search(
|
||||
dn, '(objectclass=groupOfNames)',
|
||||
search_scope='SUBTREE',
|
||||
attributes=[ALL_ATTRIBUTES]
|
||||
)
|
||||
|
||||
response = get_response(client, response_id)
|
||||
groups = [make_group_dict(client, entry) for entry in response]
|
||||
return groups
|
||||
|
||||
|
||||
def add_group_member(client, group, user):
|
||||
group_dn = group['dn']
|
||||
member_dn = user['dn']
|
||||
log.debug('Found adding {} to {}'.format(member_dn, group_dn))
|
||||
|
||||
response_id = client.connection.modify(
|
||||
group_dn,
|
||||
{'member': [(MODIFY_ADD, [member_dn])]}
|
||||
)
|
||||
|
||||
return get_response(client, response_id)
|
|
@ -1,77 +1,26 @@
|
|||
from ldap3 import ALL_ATTRIBUTES, HASHED_SALTED_SHA
|
||||
from ldap3.utils.hashed import hashed
|
||||
from phi.ldap.utils import get_response, make_user_dict, add_entry, delete_entry
|
||||
from phi.logging import get_logger
|
||||
from phi.ldap.entry import get_entry_by_uid
|
||||
from phi.ldap.utils import flatten_attributes
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
def user_attributes_mapping(client):
|
||||
return {
|
||||
client.attribute_id: "uid",
|
||||
client.attribute_mail: "mail",
|
||||
"createTimestamp": "created_at",
|
||||
"modifyTimestamp": "modified_at",
|
||||
}
|
||||
|
||||
|
||||
def get_user_by_uid(client, uid):
|
||||
log.info("Searching entry with identifier: {}".format(uid))
|
||||
entry = get_entry_by_uid(client, uid)
|
||||
|
||||
filter_ = "({}={})".format('uid', uid)
|
||||
log.debug("Search filter: {}".format(filter_))
|
||||
|
||||
response_id = client.connection.search(
|
||||
client.base_dn, filter_,
|
||||
search_scope='SUBTREE',
|
||||
attributes=[ALL_ATTRIBUTES]
|
||||
)
|
||||
|
||||
response = get_response(client, response_id)
|
||||
|
||||
if not response:
|
||||
if not entry:
|
||||
return None
|
||||
|
||||
if len(response) > 1:
|
||||
log.error("Looking for exactly one result but server gave {}. "
|
||||
"Taking the first and ignoring the rest."
|
||||
.format(len(response)))
|
||||
mapping = user_attributes_mapping(client)
|
||||
|
||||
return make_user_dict(client, response[0])
|
||||
|
||||
|
||||
def get_all_users(client):
|
||||
log.info("Searching all the users")
|
||||
|
||||
dn = 'ou=Hackers,{}'.format(client.base_dn)
|
||||
log.debug("Search dn: {}".format(dn))
|
||||
|
||||
response_id = client.connection.search(
|
||||
dn, '(objectclass=person)',
|
||||
search_scope='SUBTREE',
|
||||
attributes=[ALL_ATTRIBUTES]
|
||||
)
|
||||
|
||||
response = get_response(client, response_id)
|
||||
|
||||
users = [make_user_dict(client, entry) for entry in response]
|
||||
return users
|
||||
|
||||
|
||||
def add_user(client, uid, cn, sn, mail, password):
|
||||
dn = 'uid={},ou=Hackers,{}'.format(uid, client.base_dn)
|
||||
hashed_password = hashed(HASHED_SALTED_SHA, password)
|
||||
|
||||
attributes={
|
||||
'objectClass': [
|
||||
'inetOrgPerson',
|
||||
'organizationalPerson',
|
||||
'person', 'top'
|
||||
],
|
||||
'cn': cn,
|
||||
'sn': sn,
|
||||
'mail': mail,
|
||||
'userPassword': hashed_password
|
||||
user = {
|
||||
mapping[k]: v for k, v in entry["attributes"].items() if k in mapping.keys()
|
||||
}
|
||||
|
||||
add_entry(client, dn, attributes)
|
||||
|
||||
|
||||
def delete_user(client, user):
|
||||
delete_entry(client, user['dn'])
|
||||
|
||||
|
||||
def delete_user_by_uid(client, uid):
|
||||
dn = 'uid={},ou=Hackers,{}'.format(uid, client.base_dn)
|
||||
delete_entry(client, dn)
|
||||
return flatten_attributes(user)
|
||||
|
|
|
@ -1,70 +1,3 @@
|
|||
import re
|
||||
from phi.logging import get_logger
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
def make_user_dict(client, entry):
|
||||
attributes = entry['attributes']
|
||||
|
||||
user = {}
|
||||
user['uid'] = attributes['uid'][0]
|
||||
user['dn'] = 'uid={},ou=Hackers,{}'.format(user['uid'], client.base_dn)
|
||||
user['cn'] = attributes['cn'][0]
|
||||
user['sn'] = attributes['sn'][0]
|
||||
user['mail'] = attributes['mail'][0]
|
||||
user['password'] = attributes['userPassword'][0]
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def get_uid_from_dn(client, dn):
|
||||
uid = re.search('uid=(.+?),ou=Hackers,{}'.format(client.base_dn),
|
||||
dn).group(1)
|
||||
return uid
|
||||
|
||||
|
||||
def make_group_dict(client, entry):
|
||||
attributes = entry['attributes']
|
||||
|
||||
cn = attributes['cn'][0]
|
||||
dn = 'cn={},ou=Groups,{}'.format(cn, client.base_dn)
|
||||
members = [get_uid_from_dn(client, u_dn)
|
||||
for u_dn in attributes['member']]
|
||||
|
||||
group = {}
|
||||
group['dn'] = dn
|
||||
group['cn'] = cn
|
||||
group['members'] = members
|
||||
|
||||
return group
|
||||
|
||||
|
||||
def get_response(client, response_id):
|
||||
response, result, request = client.connection.get_response(
|
||||
response_id, get_request=True
|
||||
)
|
||||
|
||||
log.debug("Request: {}".format(request))
|
||||
log.debug("Response: {}".format(response))
|
||||
log.debug("Result: {}".format(result))
|
||||
|
||||
if result['description'] is not 'success':
|
||||
raise Exception(result['description'])
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def add_entry(client, dn, attributes):
|
||||
log.info('Adding entry with distinguiscet name: {}'
|
||||
'and attributes {}'.format(dn, attributes))
|
||||
response_id = client.connection.add(dn, attributes=attributes)
|
||||
response = get_response(client, response_id)
|
||||
return response
|
||||
|
||||
|
||||
def delete_entry(client, dn):
|
||||
log.info('Deleting entry with distinguiscet name: {}')
|
||||
response_id = client.connection.delete(dn)
|
||||
response = get_response(client, response_id)
|
||||
return response
|
||||
def flatten_attributes(d):
|
||||
return {k: (v[0] if isinstance(v, list) else v)
|
||||
for k, v in d.items()}
|
||||
|
|
29
src/phi/security.py
Normal file
29
src/phi/security.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
from bonsai.ldapvaluelist import LDAPValueList
|
||||
from passlib.hash import (
|
||||
ldap_sha1,
|
||||
ldap_bcrypt,
|
||||
ldap_sha256_crypt,
|
||||
ldap_sha512_crypt,
|
||||
ldap_pbkdf2_sha256,
|
||||
ldap_pbkdf2_sha512,
|
||||
)
|
||||
|
||||
HASH_CALLABLE = {
|
||||
"sha1": ldap_sha1.hash,
|
||||
"bcrypt": ldap_bcrypt.hash,
|
||||
"sha256_crypt": ldap_sha256_crypt.hash,
|
||||
"sha512_crypt": ldap_sha512_crypt.hash,
|
||||
"pbkdf2_sha256": ldap_pbkdf2_sha256.hash,
|
||||
"pbkdf2_sha512": ldap_pbkdf2_sha512.hash,
|
||||
}
|
||||
|
||||
|
||||
def hash_pass(password, method="sha1"):
|
||||
return HASH_CALLABLE[method](password)
|
||||
|
||||
|
||||
def handle_password(password, method="sha1"):
|
||||
if isinstance(password, LDAPValueList):
|
||||
return password
|
||||
return hash_pass(password, method)
|
130
src/phicli
130
src/phicli
|
@ -1,130 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
from pprint import pformat as pp
|
||||
from getpass import getpass
|
||||
|
||||
from phi.config import get_config
|
||||
from phi.logging import setup_logging, get_logger
|
||||
from phi import cli
|
||||
import phi.ldap.client
|
||||
from phi.ldap.user import get_user_by_uid, add_user, delete_user
|
||||
from phi.ldap.group import get_group_by_cn, get_all_groups, add_group_member
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
@cli.register('dispaly user fields', ['user identifier'])
|
||||
def showuser(uid):
|
||||
user = get_user_by_uid(client, uid)
|
||||
if user is None:
|
||||
print('User {} not found'.format(uid))
|
||||
return
|
||||
|
||||
print(pp(user))
|
||||
|
||||
|
||||
@cli.register('add a new user', ['user identifier'])
|
||||
def adduser(uid):
|
||||
def ask(prompt, default):
|
||||
full_prompt = '{} [{}] '.format(prompt, default)
|
||||
return input(full_prompt) or default
|
||||
|
||||
user = get_user_by_uid(client, uid)
|
||||
if user is not None:
|
||||
print("User {} already existing".format(uid))
|
||||
return
|
||||
|
||||
cn = ask('Common name:', uid)
|
||||
sn = ask('Last name:', uid)
|
||||
mail = ask('Mail:', '{}@localhost'.format(uid))
|
||||
|
||||
password = getpass()
|
||||
pass_check = getpass('Retype password: ')
|
||||
if password != pass_check:
|
||||
print('Password not matching')
|
||||
return
|
||||
|
||||
add_user(client, uid, cn, sn, mail, password)
|
||||
|
||||
# Check
|
||||
user = get_user_by_uid(client, uid)
|
||||
print()
|
||||
print(pp(user))
|
||||
|
||||
|
||||
@cli.register('delete an user', ['user identifier'])
|
||||
def deluser(uid):
|
||||
check = input('Are you sure? [y/N] ') or 'N'
|
||||
if check.lower() != 'y':
|
||||
print('Ok then')
|
||||
return
|
||||
|
||||
user = get_user_by_uid(client, uid)
|
||||
if user is not None:
|
||||
delete_user(client, user)
|
||||
print('Done')
|
||||
else:
|
||||
print('User {} not found'.format(uid))
|
||||
|
||||
|
||||
@cli.register('show a group', ['group common name'])
|
||||
def showgroup(cn):
|
||||
group = get_group_by_cn(client, cn)
|
||||
if group is None:
|
||||
print('Group {} not found'.format(gcn))
|
||||
return
|
||||
|
||||
print(pp(group))
|
||||
|
||||
|
||||
@cli.register('list all groups')
|
||||
def listgroups():
|
||||
groups = get_all_groups(client)
|
||||
|
||||
for group in groups:
|
||||
print(group['cn'])
|
||||
|
||||
|
||||
@cli.register('add an user to a group',
|
||||
['user identifier', 'group common name'])
|
||||
def addtogroup(uid, gcn):
|
||||
user = get_user_by_uid(client, uid)
|
||||
group = get_group_by_cn(client, gcn)
|
||||
|
||||
if user is None:
|
||||
print('User {} not found'.format(uid))
|
||||
return
|
||||
|
||||
if group is None:
|
||||
print('Group {} not found'.format(gcn))
|
||||
return
|
||||
|
||||
if uid in group['members']:
|
||||
print('User {} is already in group {}'.format(uid, gcn))
|
||||
return
|
||||
|
||||
add_group_member(client, group, user)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli.add_arg('--config', 'config.yml', 'custom configuration file')
|
||||
args = cli.get_args()
|
||||
|
||||
config_file = args['config']
|
||||
|
||||
config_file, config = get_config(config_file)
|
||||
setup_logging(config.get('logging', {}))
|
||||
log.info("Using configuration at '{}':\n{}"
|
||||
.format(config_file, pp(config)))
|
||||
|
||||
# TODO: check fields in config
|
||||
client = phi.ldap.client.Client(**config['ldap'])
|
||||
|
||||
log.info('Opening LDAP client')
|
||||
client.open()
|
||||
|
||||
log.info('Arguments: {}'.format(pp(args)))
|
||||
|
||||
cli.run(args)
|
||||
|
||||
log.info('Closing LDAP client')
|
||||
client.close()
|
22
src/phid
22
src/phid
|
@ -1,22 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
from pprint import pformat as pp
|
||||
|
||||
from phi.config import get_config
|
||||
from phi.logging import setup_logging, get_logger
|
||||
from phi.app import setup_app, run_app
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
config_file, config = get_config()
|
||||
|
||||
# Beware that everything happened until now
|
||||
# could not possibly get logged.
|
||||
setup_logging(config.get('logging', {}))
|
||||
|
||||
log.info("Found configuration at '{}':\n{}"
|
||||
.format(config_file, pp(config)))
|
||||
|
||||
app = setup_app(config)
|
||||
run_app(app)
|
159
test/aux_async_model.py
Normal file
159
test/aux_async_model.py
Normal file
|
@ -0,0 +1,159 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import asyncio
|
||||
from pprint import pprint as pp
|
||||
|
||||
from phi.async_ldap.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()])
|
|
@ -1,6 +1,6 @@
|
|||
import pytest
|
||||
import phi.ldap.client
|
||||
# import phi.api.app
|
||||
import phi.api.app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -9,27 +9,26 @@ def ldap_client():
|
|||
host='localhost', port=389,
|
||||
encryption='TLSv1.2', ciphers='HIGH',
|
||||
validate=False,
|
||||
# username='uid=phi,ou=Services,dc=unit,dc=macaomilano,dc=org',
|
||||
# password='phi',
|
||||
username='cn=root,dc=unit,dc=macaomilano,dc=org',
|
||||
password='root',
|
||||
base_dn='dc=unit,dc=macaomilano,dc=org')
|
||||
username='uid=phi,ou=Services,dc=unit,dc=macaomilano,dc=org',
|
||||
password='phi',
|
||||
base_dn='dc=unit,dc=macaomilano,dc=org',
|
||||
attribute_id='uid', attribute_mail='mail')
|
||||
client.open()
|
||||
yield client
|
||||
client.close()
|
||||
|
||||
|
||||
# @pytest.fixture
|
||||
# def api_app():
|
||||
# return phi.api.app.api_app({
|
||||
# 'ldap': {
|
||||
# 'host': 'localhost',
|
||||
# 'port': 389,
|
||||
# 'encryption': 'TLSv1.2',
|
||||
# 'ciphers': 'HIGH',
|
||||
# 'validate': 'False',
|
||||
# 'username': 'uid=phi,ou=Services,dc=unit,dc=macaomilano,dc=org',
|
||||
# 'password': 'phi',
|
||||
# 'base_dn': 'dc=unit,dc=macaomilano,dc=org',
|
||||
# 'attribute_id': 'uid',
|
||||
# 'attribute_mail': 'mail'}})
|
||||
@pytest.fixture
|
||||
def api_app():
|
||||
return phi.api.app.api_app({
|
||||
'ldap': {
|
||||
'host': 'localhost',
|
||||
'port': 389,
|
||||
'encryption': 'TLSv1.2',
|
||||
'ciphers': 'HIGH',
|
||||
'validate': 'False',
|
||||
'username': 'uid=phi,ou=Services,dc=unit,dc=macaomilano,dc=org',
|
||||
'password': 'phi',
|
||||
'base_dn': 'dc=unit,dc=macaomilano,dc=org',
|
||||
'attribute_id': 'uid',
|
||||
'attribute_mail': 'mail'}})
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
# async def test_user_request_not_valid(test_client, api_app):
|
||||
# client = await test_client(api_app)
|
||||
async def test_user_request_not_valid(test_client, api_app):
|
||||
client = await test_client(api_app)
|
||||
|
||||
# resp = await client.get('/user')
|
||||
# assert resp.status == 422
|
||||
# resp = None
|
||||
resp = await client.get('/user')
|
||||
assert resp.status == 422
|
||||
resp = None
|
||||
|
||||
# resp = await client.get('/user/')
|
||||
# assert resp.status == 422
|
||||
resp = await client.get('/user/')
|
||||
assert resp.status == 422
|
||||
|
||||
|
||||
# async def test_user_not_found(test_client, api_app):
|
||||
# client = await test_client(api_app)
|
||||
# resp = await client.get('/user/nonexistent')
|
||||
# assert resp.status == 404
|
||||
async def test_user_not_found(test_client, api_app):
|
||||
client = await test_client(api_app)
|
||||
resp = await client.get('/user/nonexistent')
|
||||
assert resp.status == 404
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
from phi.ldap.user import get_user_by_uid, get_all_users, \
|
||||
add_user, delete_user_by_uid, delete_user
|
||||
|
||||
from phi.ldap.group import add_group_member, get_group_by_cn, get_all_groups
|
||||
from phi.ldap.user import get_user_by_uid
|
||||
|
||||
|
||||
def test_connection(ldap_client):
|
||||
|
@ -10,90 +7,6 @@ def test_connection(ldap_client):
|
|||
|
||||
|
||||
def test_get_user_by_id(ldap_client):
|
||||
user = get_user_by_uid(ldap_client, 'conte_mascetti')
|
||||
assert user['uid'] == 'conte_mascetti'
|
||||
assert user['mail'] == 'rmascetti@autistici.org'
|
||||
|
||||
|
||||
def test_get_all_users(ldap_client):
|
||||
users = get_all_users(ldap_client)
|
||||
# print(users)
|
||||
assert 'conte_mascetti' in [u['uid'] for u in users]
|
||||
|
||||
|
||||
def test_add_delete_user(ldap_client):
|
||||
uid = 'rosa_rossi'
|
||||
cn = 'Rosa'
|
||||
sn = 'Rossi'
|
||||
mail = 'foo@autistici.org'
|
||||
password = 'changeme'
|
||||
|
||||
add_user(ldap_client, uid, cn, sn, mail, password)
|
||||
|
||||
user = get_user_by_uid(ldap_client, uid)
|
||||
assert user['uid'] == uid
|
||||
assert user['mail'] == mail
|
||||
|
||||
delete_user(ldap_client, user)
|
||||
# print(user)
|
||||
|
||||
user = get_user_by_uid(ldap_client, uid)
|
||||
assert user is None
|
||||
|
||||
|
||||
def test_failing_add_user(ldap_client):
|
||||
uid = 'conte_mascetti'
|
||||
|
||||
try:
|
||||
add_user(ldap_client, uid, 'name', 'surname', 'mail', 'pass')
|
||||
except: # User alrady existing
|
||||
pass
|
||||
else:
|
||||
assert False
|
||||
|
||||
def test_failing_delete_user(ldap_client):
|
||||
uid = 'rosa_rossi'
|
||||
|
||||
try:
|
||||
delete_user_by_uid(ldap_client, uid)
|
||||
except: # User already not existing
|
||||
pass
|
||||
else:
|
||||
assert False
|
||||
|
||||
|
||||
def test_get_all_groups(ldap_client):
|
||||
groups = get_all_groups(ldap_client)
|
||||
|
||||
cns = [g['cn'] for g in groups]
|
||||
assert 'WikiUsers' in cns
|
||||
|
||||
|
||||
def test_add_to_group(ldap_client):
|
||||
client = ldap_client
|
||||
|
||||
group_cn = 'WikiUsers'
|
||||
member_uid = 'rosa_rossi'
|
||||
add_user(client, member_uid, 'name', 'surname', 'mail', 'pass')
|
||||
|
||||
user = get_user_by_uid(client, member_uid)
|
||||
# print(user)
|
||||
|
||||
group = get_group_by_cn(client, group_cn)
|
||||
group_members = group['members']
|
||||
|
||||
assert len(group_members) == 1
|
||||
# print(group_members)
|
||||
|
||||
add_group_member(client, group, user)
|
||||
|
||||
group = get_group_by_cn(client, group_cn)
|
||||
group_members = group['members']
|
||||
|
||||
assert len(group_members) == 2
|
||||
assert user['uid'] in group_members
|
||||
|
||||
# print(group_members)
|
||||
# print(user)
|
||||
|
||||
delete_user(client, user)
|
||||
entry = get_user_by_uid(ldap_client, 'conte_mascetti')
|
||||
assert entry['uid'] == 'conte_mascetti'
|
||||
assert entry['mail'] == 'rmascetti@autistici.org'
|
||||
|
|
Loading…
Reference in New Issue
Block a user