├── .gitignore ├── .travis.yml ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── Pipfile ├── Pipfile.lock ├── README.rst ├── flask_ldapconn ├── __init__.py ├── attribute.py ├── entry.py └── query.py ├── requirements.txt ├── setup.py └── test_flask_ldapconn.py /.gitignore: -------------------------------------------------------------------------------- 1 | config.py 2 | 3 | *.py[co] 4 | __pycache__/ 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | *build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | 19 | # Installer logs 20 | pip-log.txt 21 | 22 | # Unit test / coverage reports 23 | .coverage 24 | .tox 25 | htmlcov 26 | .cache 27 | 28 | #Translations 29 | *.mo 30 | 31 | #Mr Developer 32 | .mr.developer.cfg 33 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: required 3 | dist: xenial 4 | 5 | services: 6 | - docker 7 | 8 | before_install: 9 | - docker pull rroemhild/test-openldap 10 | - docker run -d --privileged -p 127.0.0.1:389:389 -p 127.0.0.1:636:636 rroemhild/test-openldap 11 | 12 | python: 13 | - "3.5" 14 | - "3.6" 15 | - "3.7" 16 | 17 | install: 18 | - pip install pipenv 19 | - pipenv install --dev 20 | 21 | script: py.test 22 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.10.2 (2024-05-07) 5 | ------------------- 6 | 7 | * Replace deprecated _app_ctx_stack.top with g in app init 8 | * Remove dependency on deprecated python3-six 9 | 10 | 0.10.1 (2019-12-23) 11 | ------------------- 12 | 13 | * Fix security issue: allows authentication without password (Roland) 14 | 15 | 0.10.0 (2019-10-20) 16 | ------------------- 17 | 18 | * End support for Python 2.7 19 | * fix adding zero integer attribute value (HAMANO Tsukasa) 20 | 21 | 0.9.0 (2019-08-17) 22 | ------------------ 23 | 24 | * Fix anonymous binding where no security layer is need at all (Matthias Tafelmeier @cherusk) 25 | 26 | 0.8.0 (2019-05-09) 27 | ------------------ 28 | 29 | * Refactored LDAPAttribute class (Alexei Margasov @alexei38) 30 | * Add support for Python 3.7 31 | * End support for Python 3.4 32 | * Update requirements in Pipfile.lock 33 | 34 | 0.7.2 (2018-06-14) 35 | ------------------ 36 | 37 | * Add support to return string values in JSON 38 | * Add support for LDAP_RAISE_EXCEPTIONS (Robert Wikman) 39 | * Rename LDAP_TIMEOUT to LDAP_CONNECT_TIMEOUT (Robert Wikman) 40 | 41 | 0.7.1 (2018-04-07) 42 | ------------------ 43 | 44 | * Add setting FORCE_ATTRIBUTE_VALUE_AS_LIST 45 | * Add Pipfile and Pipfile.lock for pipenv 46 | * Add Python 3.5 & 3.6 to unittest 47 | 48 | 0.7.0 (2017-11-09) 49 | ------------------ 50 | 51 | * Allow model inheritance (Dominik George) 52 | * Fix/revisit attribute access (Dominik George) 53 | * Update ldap3 to version 2.3 54 | * Update Flaks to 0.12 55 | 56 | 0.6.13 (2016-05-30) 57 | ------------------- 58 | 59 | * Fix get entries with multivalued RDNs 60 | * Update ldap3 to version 1.3.1 61 | 62 | 0.6.12 (2016-04-03) 63 | ------------------- 64 | 65 | * Update ldap3 to version 1.2.2 66 | * Dropped support for Python 3.3 67 | 68 | 0.6.11 (2016-01-28) 69 | ------------------- 70 | 71 | * Use components_in_and flag in Reader object 72 | * Update ldap3 to version 1.0.4 73 | 74 | 0.6.10 (2015-12-15) 75 | ------------------- 76 | 77 | * Update ldap3 to version 1.0.3 78 | 79 | 0.6.9 (2015-12-15) 80 | ------------------ 81 | 82 | * Update ldap3 to version 1.0.2 83 | 84 | 0.6.8 (2015-12-07) 85 | ------------------ 86 | 87 | * Add read-only option 88 | * Update ldap3 to version 1.0.1 89 | 90 | 91 | 0.6.7 (2015-10-11) 92 | ------------------ 93 | 94 | * Use connections saved on flask.g.ldap_conn 95 | 96 | 0.6.6 (2015-10-8) 97 | ------------------ 98 | 99 | * Return manager class in queries instead of fix LDAPEntry class 100 | * Update six 1.9.0 -> 1.10.0 101 | 102 | 0.6.5 103 | ----- 104 | 105 | * Update ldap3 to version 0.9.9.1 106 | 107 | 0.6.4 (2015-08-16) 108 | ------------------ 109 | 110 | * Update ldap3 to version 0.9.8.8 111 | 112 | 0.6.3 (2015-07-07) 113 | ------------------ 114 | 115 | * Update ldap3 to version 0.9.8.6 116 | 117 | 0.6.2 (2015-06-21) 118 | ------------------ 119 | 120 | * Fix TLS settings 121 | 122 | 0.6.1 (2015-05-29) 123 | ------------------ 124 | 125 | * Update ldap3 to v0.9.8.4 126 | 127 | 0.6 (2015-03-31) 128 | ---------------- 129 | 130 | * Refactored the LDAPModel class 131 | * LDAPModel is now LDAPEntry 132 | * Add write operation save (add, modify) and delete 133 | * LDAPEntry now use a query class to simplify ldap query 134 | 135 | 0.5.2 (2015-03-11) 136 | ------------------ 137 | 138 | * LDAPModel classes can now be instantiated with arguments. 139 | 140 | 0.5.1 (2015-03-11) 141 | ------------------ 142 | 143 | * Fixed installer problem. Handle flask-ldapconn as package. 144 | * Refactored the LDAPModel class 145 | 146 | 0.5 (2015-03-07) 147 | ---------------- 148 | 149 | * Refactored the LDAPModel class 150 | 151 | 0.4 (2015-03-07) 152 | ---------------- 153 | 154 | * Add authentication method 155 | * Deprecate mapped connection methods 156 | * Update Flask to 0.10.1 and ldap3 to 0.9.7.10 157 | 158 | 0.3.4 159 | ----- 160 | 161 | * v0.3.4: Add configuration option for SSL (Bartosz Marcinkowski) 162 | * v0.3.4: Add support for Python 3 (Bartosz Marcinkowski) 163 | * v0.3.4: Update python-ldap3 to v0.9.7.5 164 | 165 | 0.3.3 166 | ----- 167 | 168 | * v0.3.3: Allow anonymous auth 169 | 170 | 0.3.2 171 | ----- 172 | 173 | * v0.3.2: BUGFIX: Allow unsecure connections 174 | 175 | 0.3.1 176 | ------ 177 | 178 | * v0.3.1: Return entries instead of Reader object in models 179 | 180 | 0.3 (2015-02-10) 181 | ---------------- 182 | 183 | * Add simple read-only class model 184 | 185 | 0.2 (2015-02-05) 186 | ---------------- 187 | 188 | * Switch to python-ldap3 189 | 190 | 0.1 (2015-02-02) 191 | ---------------- 192 | 193 | * Conception 194 | * Initial Commit of Package to GitHub 195 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2019, Rafael Römhild 2 | Copyright (c) 2017, Dominik George 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst CHANGELOG.rst LICENSE 2 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | ldap3 = ">=2.3" 8 | Flask = ">=0.12" 9 | 10 | [dev-packages] 11 | docker = "*" 12 | pytest = "*" 13 | coverage = "*" 14 | pathlib2 = "*" 15 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "fdf46cf42f083515aff164aee80707b3813d0c63b236a312ed89b9ddbc289cdd" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": {}, 8 | "sources": [ 9 | { 10 | "name": "pypi", 11 | "url": "https://pypi.python.org/simple", 12 | "verify_ssl": true 13 | } 14 | ] 15 | }, 16 | "default": { 17 | "blinker": { 18 | "hashes": [ 19 | "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01", 20 | "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83" 21 | ], 22 | "markers": "python_version >= '3.8'", 23 | "version": "==1.8.2" 24 | }, 25 | "click": { 26 | "hashes": [ 27 | "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", 28 | "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" 29 | ], 30 | "markers": "python_version >= '3.7'", 31 | "version": "==8.1.7" 32 | }, 33 | "flask": { 34 | "hashes": [ 35 | "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3", 36 | "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842" 37 | ], 38 | "index": "pypi", 39 | "markers": "python_version >= '3.8'", 40 | "version": "==3.0.3" 41 | }, 42 | "itsdangerous": { 43 | "hashes": [ 44 | "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", 45 | "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173" 46 | ], 47 | "markers": "python_version >= '3.8'", 48 | "version": "==2.2.0" 49 | }, 50 | "jinja2": { 51 | "hashes": [ 52 | "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", 53 | "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" 54 | ], 55 | "markers": "python_version >= '3.7'", 56 | "version": "==3.1.4" 57 | }, 58 | "ldap3": { 59 | "hashes": [ 60 | "sha256:2bc966556fc4d4fa9f445a1c31dc484ee81d44a51ab0e2d0fd05b62cac75daa6", 61 | "sha256:5630d1383e09ba94839e253e013f1aa1a2cf7a547628ba1265cb7b9a844b5687", 62 | "sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70", 63 | "sha256:5ab7febc00689181375de40c396dcad4f2659cd260fc5e94c508b6d77c17e9d5", 64 | "sha256:f3e7fc4718e3f09dda568b57100095e0ce58633bcabbed8667ce3f8fbaa4229f" 65 | ], 66 | "index": "pypi", 67 | "version": "==2.9.1" 68 | }, 69 | "markupsafe": { 70 | "hashes": [ 71 | "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", 72 | "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", 73 | "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", 74 | "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", 75 | "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", 76 | "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", 77 | "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", 78 | "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", 79 | "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", 80 | "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", 81 | "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", 82 | "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", 83 | "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", 84 | "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", 85 | "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", 86 | "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", 87 | "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", 88 | "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", 89 | "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", 90 | "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", 91 | "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", 92 | "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", 93 | "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", 94 | "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", 95 | "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", 96 | "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", 97 | "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", 98 | "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", 99 | "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", 100 | "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", 101 | "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", 102 | "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", 103 | "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", 104 | "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", 105 | "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", 106 | "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", 107 | "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", 108 | "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", 109 | "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", 110 | "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", 111 | "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", 112 | "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", 113 | "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", 114 | "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", 115 | "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", 116 | "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", 117 | "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", 118 | "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", 119 | "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", 120 | "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", 121 | "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", 122 | "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", 123 | "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", 124 | "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", 125 | "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", 126 | "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", 127 | "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", 128 | "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", 129 | "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", 130 | "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" 131 | ], 132 | "markers": "python_version >= '3.7'", 133 | "version": "==2.1.5" 134 | }, 135 | "pyasn1": { 136 | "hashes": [ 137 | "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c", 138 | "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473" 139 | ], 140 | "markers": "python_version >= '3.8'", 141 | "version": "==0.6.0" 142 | }, 143 | "werkzeug": { 144 | "hashes": [ 145 | "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18", 146 | "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8" 147 | ], 148 | "markers": "python_version >= '3.8'", 149 | "version": "==3.0.3" 150 | } 151 | }, 152 | "develop": { 153 | "certifi": { 154 | "hashes": [ 155 | "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", 156 | "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" 157 | ], 158 | "markers": "python_version >= '3.6'", 159 | "version": "==2024.2.2" 160 | }, 161 | "charset-normalizer": { 162 | "hashes": [ 163 | "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", 164 | "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", 165 | "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", 166 | "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", 167 | "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", 168 | "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", 169 | "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", 170 | "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", 171 | "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", 172 | "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", 173 | "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", 174 | "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", 175 | "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", 176 | "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", 177 | "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", 178 | "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", 179 | "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", 180 | "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", 181 | "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", 182 | "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", 183 | "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", 184 | "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", 185 | "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", 186 | "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", 187 | "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", 188 | "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", 189 | "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", 190 | "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", 191 | "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", 192 | "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", 193 | "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", 194 | "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", 195 | "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", 196 | "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", 197 | "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", 198 | "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", 199 | "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", 200 | "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", 201 | "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", 202 | "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", 203 | "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", 204 | "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", 205 | "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", 206 | "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", 207 | "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", 208 | "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", 209 | "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", 210 | "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", 211 | "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", 212 | "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", 213 | "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", 214 | "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", 215 | "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", 216 | "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", 217 | "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", 218 | "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", 219 | "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", 220 | "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", 221 | "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", 222 | "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", 223 | "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", 224 | "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", 225 | "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", 226 | "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", 227 | "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", 228 | "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", 229 | "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", 230 | "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", 231 | "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", 232 | "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", 233 | "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", 234 | "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", 235 | "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", 236 | "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", 237 | "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", 238 | "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", 239 | "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", 240 | "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", 241 | "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", 242 | "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", 243 | "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", 244 | "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", 245 | "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", 246 | "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", 247 | "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", 248 | "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", 249 | "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", 250 | "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", 251 | "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", 252 | "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" 253 | ], 254 | "markers": "python_full_version >= '3.7.0'", 255 | "version": "==3.3.2" 256 | }, 257 | "coverage": { 258 | "hashes": [ 259 | "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de", 260 | "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661", 261 | "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26", 262 | "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41", 263 | "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d", 264 | "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981", 265 | "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2", 266 | "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34", 267 | "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f", 268 | "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a", 269 | "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35", 270 | "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223", 271 | "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1", 272 | "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746", 273 | "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90", 274 | "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c", 275 | "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca", 276 | "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8", 277 | "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596", 278 | "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e", 279 | "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd", 280 | "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e", 281 | "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3", 282 | "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e", 283 | "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312", 284 | "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7", 285 | "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572", 286 | "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428", 287 | "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f", 288 | "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07", 289 | "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e", 290 | "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4", 291 | "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136", 292 | "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5", 293 | "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8", 294 | "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d", 295 | "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228", 296 | "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206", 297 | "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa", 298 | "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e", 299 | "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be", 300 | "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5", 301 | "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668", 302 | "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601", 303 | "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057", 304 | "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146", 305 | "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f", 306 | "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8", 307 | "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7", 308 | "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987", 309 | "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19", 310 | "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece" 311 | ], 312 | "index": "pypi", 313 | "markers": "python_version >= '3.8'", 314 | "version": "==7.5.1" 315 | }, 316 | "docker": { 317 | "hashes": [ 318 | "sha256:12ba681f2777a0ad28ffbcc846a69c31b4dfd9752b47eb425a274ee269c5e14b", 319 | "sha256:323736fb92cd9418fc5e7133bc953e11a9da04f4483f828b527db553f1e7e5a3" 320 | ], 321 | "index": "pypi", 322 | "markers": "python_version >= '3.8'", 323 | "version": "==7.0.0" 324 | }, 325 | "idna": { 326 | "hashes": [ 327 | "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", 328 | "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" 329 | ], 330 | "markers": "python_version >= '3.5'", 331 | "version": "==3.7" 332 | }, 333 | "iniconfig": { 334 | "hashes": [ 335 | "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", 336 | "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" 337 | ], 338 | "markers": "python_version >= '3.7'", 339 | "version": "==2.0.0" 340 | }, 341 | "packaging": { 342 | "hashes": [ 343 | "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", 344 | "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" 345 | ], 346 | "markers": "python_version >= '3.7'", 347 | "version": "==24.0" 348 | }, 349 | "pathlib2": { 350 | "hashes": [ 351 | "sha256:5266a0fd000452f1b3467d782f079a4343c63aaa119221fbdc4e39577489ca5b", 352 | "sha256:9fe0edad898b83c0c3e199c842b27ed216645d2e177757b2dd67384d4113c641" 353 | ], 354 | "index": "pypi", 355 | "version": "==2.3.7.post1" 356 | }, 357 | "pluggy": { 358 | "hashes": [ 359 | "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", 360 | "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" 361 | ], 362 | "markers": "python_version >= '3.8'", 363 | "version": "==1.5.0" 364 | }, 365 | "pytest": { 366 | "hashes": [ 367 | "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233", 368 | "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f" 369 | ], 370 | "index": "pypi", 371 | "markers": "python_version >= '3.8'", 372 | "version": "==8.2.0" 373 | }, 374 | "requests": { 375 | "hashes": [ 376 | "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5", 377 | "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8" 378 | ], 379 | "index": "pypi", 380 | "markers": "python_version >= '3.8'", 381 | "version": "==2.32.0" 382 | }, 383 | "six": { 384 | "hashes": [ 385 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 386 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 387 | ], 388 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 389 | "version": "==1.16.0" 390 | }, 391 | "urllib3": { 392 | "hashes": [ 393 | "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", 394 | "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" 395 | ], 396 | "markers": "python_version >= '3.8'", 397 | "version": "==2.2.1" 398 | } 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Flask-LDAPConn 2 | ============== 3 | 4 | .. image:: https://travis-ci.org/rroemhild/flask-ldapconn.svg?branch=master 5 | :target: https://travis-ci.org/rroemhild/flask-ldapconn 6 | 7 | .. image:: https://badge.fury.io/py/Flask-LDAPConn.svg 8 | :target: https://badge.fury.io/py/Flask-LDAPConn 9 | 10 | Flask-LDAPConn is a Flask extension providing `ldap3 `_ (an LDAP V3 pure Python client) connection for accessing LDAP servers. 11 | 12 | To abstract access to LDAP data this extension provides a simple ORM model. 13 | 14 | 15 | Installation 16 | ------------ 17 | 18 | .. code-block:: shell 19 | 20 | pip install flask-ldapconn 21 | 22 | 23 | Configuration 24 | ------------- 25 | 26 | Your configuration should be declared within your Flask config. Sample configuration: 27 | 28 | .. code-block:: python 29 | 30 | import ssl 31 | 32 | LDAP_SERVER = 'localhost' 33 | LDAP_PORT = 389 34 | LDAP_BINDDN = 'cn=admin,dc=example,dc=com' 35 | LDAP_SECRET = 'forty-two' 36 | LDAP_CONNECT_TIMEOUT = 10 # Honored when the TCP connection is being established 37 | LDAP_USE_TLS = True # default 38 | LDAP_REQUIRE_CERT = ssl.CERT_NONE # default: CERT_REQUIRED 39 | LDAP_TLS_VERSION = ssl.PROTOCOL_TLSv1_2 # default: PROTOCOL_TLSv1 40 | LDAP_CERT_PATH = '/etc/openldap/certs' 41 | 42 | If you want to always get any entry attribute value as a list, instead of a string if only one item is in the attribute list, then set: 43 | 44 | .. code-block:: python 45 | 46 | FORCE_ATTRIBUTE_VALUE_AS_LIST = True 47 | 48 | Default is ``False`` and will return a string if only one item is in the attribute list. 49 | 50 | 51 | Setup 52 | ----- 53 | 54 | Create the LDAP instance in your application. 55 | 56 | .. code-block:: python 57 | 58 | from flask import Flask 59 | from flask_ldapconn import LDAPConn 60 | 61 | app = Flask(__name__) 62 | ldap = LDAPConn(app) 63 | 64 | 65 | Client sample 66 | ------------- 67 | 68 | .. code-block:: python 69 | 70 | from flask import Flask 71 | from flask_ldapconn import LDAPConn 72 | from ldap3 import SUBTREE 73 | 74 | app = Flask(__name__) 75 | ldap = LDAPConn(app) 76 | 77 | @app.route('/') 78 | def index(): 79 | ldapc = ldap.connection 80 | basedn = 'ou=people,dc=example,dc=com' 81 | search_filter = '(objectClass=posixAccount)' 82 | attributes = ['sn', 'givenName', 'uid', 'mail'] 83 | ldapc.search(basedn, search_filter, SUBTREE, 84 | attributes=attributes) 85 | response = ldapc.response 86 | 87 | 88 | User model samples 89 | ------------------ 90 | 91 | .. code-block:: python 92 | 93 | from flask import Flask 94 | from flask_ldapconn import LDAPConn 95 | 96 | app = Flask(__name__) 97 | ldap = LDAPConn(app) 98 | 99 | class User(ldap.Entry): 100 | 101 | base_dn = 'ou=people,dc=example,dc=com' 102 | object_classes = ['inetOrgPerson'] 103 | 104 | name = ldap.Attribute('cn') 105 | email = ldap.Attribute('mail') 106 | userid = ldap.Attribute('uid') 107 | surname = ldap.Attribute('sn') 108 | givenname = ldap.Attribute('givenName') 109 | 110 | with app.app_context(): 111 | 112 | # get a list of entries 113 | entries = User.query.filter('email: *@example.com').all() 114 | for entry in entries: 115 | print u'Name: {}'.format(entry.name) 116 | 117 | # get the first entry 118 | user = User.query.filter('userid: user1').first() 119 | 120 | # new entry 121 | new_user = User( 122 | name='User Three', 123 | email='user3@example.com', 124 | userid='user3', 125 | surname='Three', 126 | givenname='User' 127 | ) 128 | new_user.save() 129 | 130 | # modify entry 131 | mod_user = User.query.filter('userid: user1').first() 132 | mod_user.name = 'User Number Three' 133 | mod_user.email.append.('u.three@example.com') 134 | mod_user.givenname.delete() 135 | mod_user.save() 136 | 137 | # remove entry 138 | rm_user = User.query.filter('userid: user1').first() 139 | rm_user.delete() 140 | 141 | # authenticate user 142 | auth_user = User.query.filter('userid: user1').first() 143 | if auth_user: 144 | if auth_user.authenticate('password1234'): 145 | print('Authenticated') 146 | else: 147 | print('Wrong password') 148 | 149 | 150 | Authenticate with Client 151 | ------------------------ 152 | 153 | .. code-block:: python 154 | 155 | from flask import Flask 156 | from flask_ldapconn import LDAPConn 157 | 158 | app = Flask(__name__) 159 | ldap = LDAPConn(app) 160 | 161 | username = 'user1' 162 | password = 'userpass' 163 | attribute = 'uid' 164 | search_filter = ('(active=1)') 165 | 166 | with app.app_context(): 167 | retval = ldap.authenticate(username, password, attribute, 168 | basedn, search_filter) 169 | if not retval: 170 | return 'Invalid credentials.' 171 | return 'Welcome %s.' % username 172 | 173 | 174 | Bind as user 175 | ------------ 176 | 177 | To bind as user for the current request instance a new connection from ``flask.g.ldap_conn``: 178 | 179 | .. code-block:: python 180 | 181 | g.ldap_conn = ldap.connect(userdn, password) 182 | user = User.query.get(userdn) 183 | 184 | 185 | Unit Test 186 | --------- 187 | 188 | I use a simple Docker image to run the tests on localhost. The test file ``test_flask_ldapconn.py`` tries to handle ``start`` and ``stop`` of the docker container: 189 | 190 | .. code-block:: shell 191 | 192 | pip install docker-py 193 | docker pull rroemhild/test-openldap 194 | python test_flask_ldapconn.py 195 | 196 | Run the docker container manual: 197 | 198 | .. code-block:: shell 199 | 200 | docker run --privileged -d -p 389:389 --name flask_ldapconn rroemhild/test-openldap 201 | DOCKER_RUN=False python test_flask_ldapconn.py 202 | 203 | Unit test with your own settings from a file: 204 | 205 | .. code-block:: shell 206 | 207 | LDAP_SETTINGS=my_settings.py python test_flask_ldapconn.py 208 | 209 | 210 | Contribute 211 | ---------- 212 | 213 | #. Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug. 214 | #. Fork `the repository`_ on Github to start making your changes. 215 | #. Write a test which shows that the bug was fixed or that the feature works as expected. 216 | #. Send a pull request and bug the maintainer until it gets merged and published. 217 | 218 | .. _`the repository`: http://github.com/rroemhild/flask-ldapconn 219 | -------------------------------------------------------------------------------- /flask_ldapconn/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import ssl 3 | 4 | from flask import current_app, g 5 | from ldap3 import Server, Connection, Tls 6 | from ldap3 import SYNC, ALL, SUBTREE 7 | from ldap3 import AUTO_BIND_NONE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND 8 | from ldap3 import ANONYMOUS, SIMPLE, SASL 9 | from ldap3.core.exceptions import (LDAPBindError, LDAPInvalidFilterError, 10 | LDAPInvalidDnError) 11 | from ldap3.utils.dn import parse_dn 12 | 13 | from .entry import LDAPEntry 14 | from .attribute import LdapField 15 | 16 | 17 | __all__ = ('LDAPConn',) 18 | 19 | 20 | class LDAPConn(object): 21 | 22 | def __init__(self, app=None): 23 | 24 | self.Entry = LDAPEntry 25 | self.Attribute = LdapField 26 | self.Model = self.Entry 27 | self.app = app 28 | 29 | if app is not None: 30 | self.init_app(app) 31 | 32 | def init_app(self, app): 33 | ssl_defaults = ssl.get_default_verify_paths() 34 | 35 | # Default config 36 | app.config.setdefault('LDAP_SERVER', 'localhost') 37 | app.config.setdefault('LDAP_PORT', 389) 38 | app.config.setdefault('LDAP_BINDDN', None) 39 | app.config.setdefault('LDAP_SECRET', None) 40 | app.config.setdefault('LDAP_CONNECT_TIMEOUT', 10) 41 | app.config.setdefault('LDAP_READ_ONLY', False) 42 | app.config.setdefault('LDAP_VALID_NAMES', None) 43 | app.config.setdefault('LDAP_PRIVATE_KEY_PASSWORD', None) 44 | app.config.setdefault('LDAP_RAISE_EXCEPTIONS', False) 45 | 46 | app.config.setdefault('LDAP_CONNECTION_STRATEGY', SYNC) 47 | 48 | app.config.setdefault('LDAP_USE_SSL', False) 49 | app.config.setdefault('LDAP_USE_TLS', True) 50 | app.config.setdefault('LDAP_TLS_VERSION', ssl.PROTOCOL_TLSv1) 51 | app.config.setdefault('LDAP_REQUIRE_CERT', ssl.CERT_REQUIRED) 52 | 53 | app.config.setdefault('LDAP_CLIENT_PRIVATE_KEY', None) 54 | app.config.setdefault('LDAP_CLIENT_CERT', None) 55 | 56 | app.config.setdefault('LDAP_CA_CERTS_FILE', ssl_defaults.cafile) 57 | app.config.setdefault('LDAP_CA_CERTS_PATH', ssl_defaults.capath) 58 | app.config.setdefault('LDAP_CA_CERTS_DATA', None) 59 | 60 | app.config.setdefault('FORCE_ATTRIBUTE_VALUE_AS_LIST', False) 61 | 62 | self.tls = Tls( 63 | local_private_key_file=app.config['LDAP_CLIENT_PRIVATE_KEY'], 64 | local_certificate_file=app.config['LDAP_CLIENT_CERT'], 65 | validate=app.config['LDAP_REQUIRE_CERT'], 66 | version=app.config['LDAP_TLS_VERSION'], 67 | ca_certs_file=app.config['LDAP_CA_CERTS_FILE'], 68 | valid_names=app.config['LDAP_VALID_NAMES'], 69 | ca_certs_path=app.config['LDAP_CA_CERTS_PATH'], 70 | ca_certs_data=app.config['LDAP_CA_CERTS_DATA'], 71 | local_private_key_password=app.config['LDAP_PRIVATE_KEY_PASSWORD'] 72 | ) 73 | 74 | self.ldap_server = Server( 75 | host=app.config['LDAP_SERVER'], 76 | port=app.config['LDAP_PORT'], 77 | use_ssl=app.config['LDAP_USE_SSL'], 78 | connect_timeout=app.config['LDAP_CONNECT_TIMEOUT'], 79 | tls=self.tls, 80 | get_info=ALL 81 | ) 82 | 83 | # Store ldap_conn object to extensions 84 | app.extensions['ldap_conn'] = self 85 | 86 | # Teardown appcontext 87 | app.teardown_appcontext(self.teardown) 88 | 89 | def connect(self, user, password, anonymous=False): 90 | auto_bind_strategy = AUTO_BIND_TLS_BEFORE_BIND 91 | authentication_policy = SIMPLE 92 | if current_app.config['LDAP_USE_TLS'] is not True: 93 | auto_bind_strategy = AUTO_BIND_NO_TLS 94 | if anonymous: 95 | authentication_policy = ANONYMOUS 96 | user = None 97 | password = None 98 | 99 | ldap_conn = Connection( 100 | self.ldap_server, 101 | auto_bind=auto_bind_strategy, 102 | client_strategy=current_app.config['LDAP_CONNECTION_STRATEGY'], 103 | raise_exceptions=current_app.config['LDAP_RAISE_EXCEPTIONS'], 104 | authentication=authentication_policy, 105 | user=user, 106 | password=password, 107 | check_names=True, 108 | read_only=current_app.config['LDAP_READ_ONLY'], 109 | ) 110 | 111 | return ldap_conn 112 | 113 | def teardown(self, exception): 114 | if 'ldap_conn' in g: 115 | g.ldap_conn.unbind() 116 | 117 | @property 118 | def connection(self): 119 | if not 'ldap_conn' in g: 120 | g.ldap_conn = self.connect( 121 | current_app.config['LDAP_BINDDN'], 122 | current_app.config['LDAP_SECRET'], 123 | anonymous=None in [current_app.config['LDAP_BINDDN'], current_app.config['LDAP_SECRET']] 124 | ) 125 | return g.ldap_conn 126 | 127 | def authenticate(self, 128 | username, 129 | password, 130 | attribute=None, 131 | base_dn=None, 132 | search_filter=None, 133 | search_scope=SUBTREE): 134 | '''Attempts to bind a user to the LDAP server. 135 | 136 | Args: 137 | username (str): DN or the username to attempt to bind with. 138 | password (str): The password of the username. 139 | attribute (str): The LDAP attribute for the username. 140 | base_dn (str): The LDAP basedn to search on. 141 | search_filter (str): LDAP searchfilter to attempt the user 142 | search with. 143 | 144 | Returns: 145 | bool: ``True`` if successful or ``False`` if the 146 | credentials are invalid. 147 | ''' 148 | # If the username is no valid DN we can bind with, we need to find 149 | # the user first. 150 | valid_dn = False 151 | 152 | try: 153 | parse_dn(username) 154 | valid_dn = True 155 | except LDAPInvalidDnError: 156 | pass 157 | 158 | if valid_dn is False: 159 | user_filter = '({0}={1})'.format(attribute, username) 160 | if search_filter is not None: 161 | user_filter = '(&{0}{1})'.format(user_filter, search_filter) 162 | 163 | try: 164 | self.connection.search(base_dn, user_filter, search_scope, 165 | attributes=[attribute]) 166 | response = self.connection.response 167 | username = response[0]['dn'] 168 | except (LDAPInvalidDnError, LDAPInvalidFilterError, IndexError): 169 | return False 170 | 171 | try: 172 | conn = self.connect(username, password) 173 | conn.unbind() 174 | return True 175 | except LDAPBindError: 176 | return False 177 | 178 | def whoami(self): 179 | '''Deprecated 180 | 181 | Use LDAPConn.connection.extend.standard.who_am_i() 182 | ''' 183 | return self.connection.extend.standard.who_am_i() 184 | 185 | def result(self): 186 | '''Deprecated 187 | 188 | Use LDAPConn.connection.result 189 | ''' 190 | return self.connection.result 191 | 192 | def response(self): 193 | '''Deprecated 194 | 195 | Use LDAPConn.connection.response 196 | ''' 197 | return self.connection.response 198 | 199 | def search(self, *args, **kwargs): 200 | '''Deprecated 201 | 202 | Use LDAPConn.connection.search() 203 | ''' 204 | return self.connection.search(*args, **kwargs) 205 | -------------------------------------------------------------------------------- /flask_ldapconn/attribute.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask import current_app 4 | 5 | from ldap3 import AttrDef 6 | from ldap3.core.exceptions import LDAPAttributeError 7 | from ldap3 import (STRING_TYPES, NUMERIC_TYPES, MODIFY_ADD, MODIFY_DELETE, 8 | MODIFY_REPLACE) 9 | 10 | 11 | class LdapField(object): 12 | 13 | def __init__(self, name, validate=None, default=None, dereference_dn=None): 14 | self.name = name 15 | self.validate = validate 16 | self.default = default 17 | self.dereference_dn = None 18 | 19 | def get_abstract_attr_def(self, key): 20 | return AttrDef(name=self.name, key=key, 21 | validate=self.validate, 22 | default=self.default, 23 | dereference_dn=self.dereference_dn) 24 | 25 | 26 | class LDAPAttribute(object): 27 | 28 | def __init__(self, name): 29 | self.__dict__['name'] = name 30 | self.__dict__['values'] = [] 31 | self.__dict__['changetype'] = None 32 | 33 | def __str__(self): 34 | if isinstance(self.value, STRING_TYPES): 35 | return self.value 36 | else: 37 | return str(self.value) 38 | 39 | def __len__(self): 40 | return len(self.values) 41 | 42 | def __iter__(self): 43 | return self.values.__iter__() 44 | 45 | def __contains__(self, item): 46 | return item in self.__dict__['values'] 47 | 48 | def __setattr__(self, item, value): 49 | if item not in ['value', '_init']: 50 | raise LDAPAttributeError('can not set key') 51 | 52 | # set changetype 53 | if item == 'value': 54 | if self.__dict__['values']: 55 | if not value: 56 | self.__dict__['changetype'] = MODIFY_DELETE 57 | else: 58 | self.__dict__['changetype'] = MODIFY_REPLACE 59 | else: 60 | self.__dict__['changetype'] = MODIFY_ADD 61 | 62 | if isinstance(value, (STRING_TYPES, NUMERIC_TYPES)): 63 | value = [value] 64 | 65 | self.__dict__['values'] = value 66 | 67 | @property 68 | def value(self): 69 | '''Return single value or list of values from the attribute. 70 | If FORCE_ATTRIBUTE_VALUE_AS_LIST is True, always return a 71 | list with values. 72 | ''' 73 | if len(self.__dict__['values']) == 1 and current_app.config['FORCE_ATTRIBUTE_VALUE_AS_LIST'] is False: 74 | return self.__dict__['values'][0] 75 | else: 76 | return self.__dict__['values'] 77 | 78 | @property 79 | def changetype(self): 80 | return self.__dict__['changetype'] 81 | 82 | def get_changes_tuple(self): 83 | values = [val.encode('UTF-8') for val in self.__dict__['values']] 84 | return (self.changetype, values) 85 | 86 | def append(self, value): 87 | '''Add another value to the attribute''' 88 | if self.__dict__['values']: 89 | self.__dict__['changetype'] = MODIFY_REPLACE 90 | 91 | self.__dict__['values'].append(value) 92 | 93 | def delete(self): 94 | '''Delete this attribute 95 | 96 | This property sets the value to an empty list an the changetype 97 | to delete. 98 | ''' 99 | self.value = [] 100 | -------------------------------------------------------------------------------- /flask_ldapconn/entry.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | from flask import current_app 4 | from ldap3.utils.dn import safe_dn 5 | from ldap3.utils.conv import check_json_dict, format_json 6 | from ldap3.core.exceptions import LDAPAttributeError 7 | 8 | from .query import BaseQuery 9 | from .attribute import LDAPAttribute, LdapField 10 | 11 | 12 | __all__ = ('LDAPEntry',) 13 | 14 | 15 | class LDAPEntryMeta(type): 16 | 17 | def __init__(cls, name, bases, attr): 18 | cls._fields = {} 19 | for key, value in attr.items(): 20 | if isinstance(value, LdapField): 21 | cls._fields[key] = value 22 | 23 | for base in bases: 24 | if isinstance(base, LDAPEntryMeta): 25 | cls._fields.update(base._fields) 26 | # Deduplicate object classes 27 | cls.object_classes = list( 28 | set(cls.object_classes + base.object_classes)) 29 | 30 | @property 31 | def query(cls): 32 | return BaseQuery(cls) 33 | 34 | 35 | class LDAPEntry(object, metaclass=LDAPEntryMeta): 36 | 37 | base_dn = None 38 | entry_rdn = ['cn'] 39 | object_classes = ['top'] 40 | sub_tree = True 41 | operational_attributes = False 42 | _changetype = 'add' 43 | 44 | def __init__(self, dn=None, changetype='add', **kwargs): 45 | self._attributes = {} 46 | self._dn = dn 47 | self._changetype = changetype 48 | if kwargs: 49 | for key, value in kwargs.items(): 50 | self._store_attr(key, value, init=True) 51 | for key, ldap_attr in self._fields.items(): 52 | if not self._isstored(key): 53 | self._store_attr(key, []) 54 | 55 | @property 56 | def dn(self): 57 | if self._dn is None: 58 | self.generate_dn_from_entry() 59 | return self._dn 60 | 61 | def generate_dn_from_entry(self): 62 | rdn_list = list() 63 | for key, attr in self._attributes.items(): 64 | if attr.name in self.entry_rdn: 65 | if len(self._attributes[key]) == 1: 66 | rdn = '{attr}={value}'.format( 67 | attr=attr.name, 68 | value=self._attributes[key].value 69 | ) 70 | rdn_list.append(rdn) 71 | dn = '{rdn},{base_dn}'.format(rdn='+'.join(rdn_list), 72 | base_dn=self.base_dn) 73 | self._dn = safe_dn(dn) 74 | 75 | @classmethod 76 | def _get_field(cls, attr): 77 | return cls._fields.get(attr) 78 | 79 | @classmethod 80 | def _get_field_name(cls, attr): 81 | if cls._get_field(attr): 82 | return cls._get_field(attr).name 83 | 84 | def _store_attr(self, attr, value=[], init=False): 85 | if not self._get_field(attr): 86 | raise LDAPAttributeError('attribute not found') 87 | if value is None: 88 | value = [] 89 | if not self._attributes.get(attr): 90 | self._attributes[attr] = LDAPAttribute(self._get_field_name(attr)) 91 | self._attributes[attr].value = value 92 | if init: 93 | self._attributes[attr].__dict__['changetype'] = None 94 | 95 | def _isstored(self, attr): 96 | return self._attributes.get(attr) 97 | 98 | def _get_attr(self, attr): 99 | if self._isstored(attr): 100 | return self._attributes[attr].value 101 | return None 102 | 103 | def __getattribute__(self, item): 104 | if item != '_fields' and item in self._fields: 105 | return self._get_attr(item) 106 | return super(LDAPModel, self).__getattribute__(item) 107 | 108 | def __setattr__(self, key, value): 109 | if key != '_fields' and key in self._fields: 110 | self._store_attr(key, value) 111 | else: 112 | return super(LDAPModel, self).__setattr__(key, value) 113 | 114 | def get_attributes_dict(self): 115 | return dict((attribute_key, attribute_value.values) for (attribute_key, 116 | attribute_value) in self._attributes.items()) 117 | 118 | def get_entry_add_dict(self, attr_dict): 119 | add_dict = dict() 120 | for attribute_key, attribute_value in attr_dict.items(): 121 | if self._attributes[attribute_key].value != []: 122 | add_dict.update({self._get_field_name(attribute_key): attribute_value}) 123 | return add_dict 124 | 125 | def get_entry_modify_dict(self, attr_dict): 126 | modify_dict = dict() 127 | for attribute_key in attr_dict.keys(): 128 | if self._attributes[attribute_key].changetype is not None: 129 | changes = self._attributes[attribute_key].get_changes_tuple() 130 | modify_dict.update({self._get_field_name(attribute_key): changes}) 131 | return modify_dict 132 | 133 | @property 134 | def connection(self): 135 | return current_app.extensions.get('ldap_conn') 136 | 137 | def delete(self): 138 | '''Delete this entry from LDAP server''' 139 | return self.connection.connection.delete(self.dn) 140 | 141 | def save(self): 142 | '''Save the current instance''' 143 | attrs = self.get_attributes_dict() 144 | if self._changetype == 'add': 145 | changes = self.get_entry_add_dict(attrs) 146 | return self.connection.connection.add(self.dn, 147 | self.object_classes, 148 | changes) 149 | elif self._changetype == 'modify': 150 | changes = self.get_entry_modify_dict(attrs) 151 | return self.connection.connection.modify(self.dn, changes) 152 | 153 | return False 154 | 155 | def authenticate(self, password): 156 | '''Authenticate a user with an LDAPModel class 157 | 158 | Args: 159 | password (str): The user password. 160 | 161 | ''' 162 | return self.connection.authenticate(self.dn, password) 163 | 164 | def to_json(self, indent=2, sort=True, str_values=False): 165 | json_entry = dict() 166 | json_entry['dn'] = self.dn 167 | 168 | # Get "single values" from attributes as str instead list if 169 | # `str_values=True` else get all attributes as list. This only 170 | # works if `FORCE_ATTRIBUTE_VALUE_AS_LIST` is False (default). 171 | if str_values is True: 172 | json_entry['attributes'] = {} 173 | for attr in self._attributes.keys(): 174 | json_entry['attributes'][attr] = self._attributes[attr].value 175 | else: 176 | json_entry['attributes'] = self.get_attributes_dict() 177 | 178 | if str == bytes: 179 | check_json_dict(json_entry) 180 | 181 | json_output = json.dumps(json_entry, 182 | ensure_ascii=True, 183 | sort_keys=sort, 184 | indent=indent, 185 | check_circular=True, 186 | default=format_json, 187 | separators=(',', ': ')) 188 | 189 | return json_output 190 | 191 | 192 | LDAPModel = LDAPEntry 193 | -------------------------------------------------------------------------------- /flask_ldapconn/query.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | from flask import current_app 4 | from ldap3 import BASE, Reader, SUBTREE, ObjectDef 5 | 6 | 7 | __all__ = ('BaseQuery',) 8 | 9 | 10 | class BaseQuery(object): 11 | 12 | def __init__(self, obj): 13 | self.obj = obj 14 | self.query = [] 15 | self.base_dn = obj.base_dn 16 | self.sub_tree = obj.sub_tree 17 | self.object_def = ObjectDef(obj.object_classes) 18 | self.operational_attributes = obj.operational_attributes 19 | self.components_in_and = True 20 | 21 | def add_abstract_attr_def(self): 22 | for name, attr in self.obj._fields.items(): 23 | attr_def = attr.get_abstract_attr_def(name) 24 | self.object_def.add_attribute(attr_def) 25 | 26 | def __iter__(self): 27 | for entry in self.get_reader_result(): 28 | module = sys.modules.get(self.obj.__module__) 29 | new_cls = getattr(module, self.obj.__name__) 30 | ldapentry = new_cls(dn=entry.entry_dn, 31 | changetype='modify', 32 | **entry.entry_attributes_as_dict) 33 | yield ldapentry 34 | 35 | def get_reader_result(self): 36 | query = ','.join(self.query) 37 | ldapc = current_app.extensions.get('ldap_conn') 38 | self.add_abstract_attr_def() 39 | reader = Reader(connection=ldapc.connection, 40 | object_def=self.object_def, 41 | query=query, 42 | base=self.base_dn, 43 | components_in_and=self.components_in_and, 44 | sub_tree=self.sub_tree, 45 | get_operational_attributes=self.operational_attributes, 46 | controls=None) 47 | reader.search() 48 | return reader.entries 49 | 50 | def get(self, ldap_dn): 51 | '''Return an LDAP entry by DN 52 | 53 | Args: 54 | ldap_dn (str): LDAP DN 55 | ''' 56 | self.base_dn = ldap_dn 57 | self.sub_tree = BASE 58 | return self.first() 59 | 60 | def filter(self, *query_filter): 61 | '''Set the query filter to perform the query with 62 | 63 | Args: 64 | *query_filter: Simplified Query Language filter 65 | ''' 66 | for query in query_filter: 67 | self.query.append(query) 68 | return self 69 | 70 | def first(self): 71 | '''Execute the query and return the first result 72 | 73 | If there are no entries, first returns ``None`` 74 | ''' 75 | for entry in iter(self): 76 | return entry 77 | return None 78 | 79 | def all(self, components_in_and=True): 80 | '''Return all of the results of a query in a list''' 81 | self.components_in_and = components_in_and 82 | return [obj for obj in iter(self)] 83 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask>=0.12 2 | ldap3>=2.3 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ''' 4 | Flask-LDAPConn 5 | -------------- 6 | 7 | Flask extension providing ldap3 connection object and ORM 8 | to accessing LDAP servers. 9 | ''' 10 | 11 | 12 | from setuptools import setup 13 | 14 | 15 | setup( 16 | name='Flask-LDAPConn', 17 | version='0.10.2', 18 | url='http://github.com/rroemhild/flask-ldapconn', 19 | license='BSD', 20 | author='Rafael Römhild', 21 | author_email='rafael@roemhild.de', 22 | keywords='flask ldap ldap3 orm', 23 | description='Pure python, LDAP connection and ORM for Flask Applications', 24 | long_description=open('README.rst').read(), 25 | packages=[ 26 | 'flask_ldapconn' 27 | ], 28 | platforms='any', 29 | install_requires=[ 30 | 'Flask>=0.12', 31 | 'ldap3>=2.3', 32 | ], 33 | classifiers=[ 34 | 'Development Status :: 5 - Production/Stable', 35 | 'Environment :: Web Environment', 36 | 'Intended Audience :: Developers', 37 | 'License :: OSI Approved :: BSD License', 38 | 'Operating System :: OS Independent', 39 | 'Programming Language :: Python :: 3.5', 40 | 'Programming Language :: Python :: 3.6', 41 | 'Programming Language :: Python :: 3.7', 42 | 'Framework :: Flask', 43 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 44 | 'Topic :: Software Development :: Libraries :: Python Modules' 45 | ], 46 | ) 47 | -------------------------------------------------------------------------------- /test_flask_ldapconn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import sys 4 | import ssl 5 | import json 6 | import time 7 | import random 8 | import string 9 | import unittest 10 | import flask 11 | 12 | from ldap3 import SUBTREE, STRING_TYPES 13 | from ldap3.core.exceptions import LDAPAttributeError, LDAPStartTLSError 14 | 15 | from flask_ldapconn import LDAPConn 16 | 17 | from flask_ldapconn.entry import LDAPEntry 18 | from flask_ldapconn.attribute import LdapField 19 | 20 | 21 | TESTING = True 22 | USER_EMAIL = 'fry@planetexpress.com' 23 | USER_PASSWORD = 'fry' 24 | LDAP_SERVER = 'localhost' 25 | LDAP_BINDDN = 'cn=admin,dc=planetexpress,dc=com' 26 | LDAP_SECRET = 'GoodNewsEveryone' 27 | LDAP_BASEDN = 'dc=planetexpress,dc=com' 28 | LDAP_SEARCH_ATTR = 'mail' 29 | LDAP_SEARCH_FILTER = '(mail=%s)' % USER_EMAIL 30 | LDAP_QUERY_FILTER = 'email: %s' % USER_EMAIL 31 | LDAP_TLS_VERSION = ssl.PROTOCOL_TLSv1 32 | LDAP_REQUIRE_CERT = ssl.CERT_NONE 33 | 34 | LDAP_AUTH_BASEDN = 'ou=people,dc=planetexpress,dc=com' 35 | LDAP_AUTH_ATTR = 'mail' 36 | LDAP_AUTH_SEARCH_FILTER = '(objectClass=inetOrgPerson)' 37 | 38 | UID_SUFFIX = ''.join(random.choice( 39 | string.ascii_lowercase + string.digits 40 | ) for _ in range(6)) 41 | 42 | 43 | def is_json(myjson): 44 | try: 45 | json.loads(myjson) 46 | except (ValueError, TypeError): 47 | return False 48 | return True 49 | 50 | 51 | class User(LDAPEntry): 52 | # LDAP meta-data 53 | base_dn = LDAP_AUTH_BASEDN 54 | entry_rdn = ['cn', 'uid'] 55 | object_classes = ['inetOrgPerson'] 56 | 57 | # inetOrgPerson 58 | name = LdapField('cn') 59 | email = LdapField('mail') 60 | title = LdapField('title') 61 | userid = LdapField('uid') 62 | surname = LdapField('sn') 63 | givenname = LdapField('givenName') 64 | 65 | 66 | class Account(User): 67 | # LDAP meta-data 68 | object_classes = ['posixAccount'] 69 | 70 | # posixAccount 71 | uidnumber = LdapField('uidNumber') 72 | gidnumber = LdapField('gidNumber') 73 | shell = LdapField('loginShell') 74 | home = LdapField('homeDirectory') 75 | password = LdapField('userPassword') 76 | 77 | 78 | class LDAPConnTestCase(unittest.TestCase): 79 | 80 | def setUp(self): 81 | app = flask.Flask(__name__) 82 | app.config.from_object(__name__) 83 | app.config.from_envvar('LDAP_SETTINGS', silent=True) 84 | ldap = LDAPConn(app) 85 | 86 | self.app = app 87 | self.ldap = ldap 88 | 89 | 90 | class LDAPConnSearchTestCase(LDAPConnTestCase): 91 | 92 | def test_connection_search(self): 93 | attr = self.app.config['LDAP_SEARCH_ATTR'] 94 | with self.app.test_request_context(): 95 | ldapc = self.ldap.connection 96 | ldapc.search(self.app.config['LDAP_BASEDN'], 97 | self.app.config['LDAP_SEARCH_FILTER'], 98 | SUBTREE, attributes=[attr]) 99 | result = ldapc.result 100 | response = ldapc.response 101 | self.assertTrue(response) 102 | self.assertEqual(response[0]['attributes'][attr][0], 103 | self.app.config['USER_EMAIL']) 104 | 105 | def test_whoami(self): 106 | with self.app.test_request_context(): 107 | conn = self.ldap.connection 108 | self.assertEqual(conn.extend.standard.who_am_i(), 109 | 'dn:{}'.format(self.app.config['LDAP_BINDDN'])) 110 | 111 | 112 | class LDAPConnModelTestCase(unittest.TestCase): 113 | 114 | def setUp(self): 115 | app = flask.Flask(__name__) 116 | app.config.from_object(__name__) 117 | app.config.from_envvar('LDAP_SETTINGS', silent=True) 118 | ldap = LDAPConn(app) 119 | 120 | self.app = app 121 | self.ldap = ldap 122 | self.user = User 123 | 124 | def test_model_search(self): 125 | with self.app.test_request_context(): 126 | entry = self.user.query.filter( 127 | 'email: %s' % self.app.config['USER_EMAIL'] 128 | ).first() 129 | self.assertEqual(entry.email, 130 | self.app.config['USER_EMAIL']) 131 | 132 | def test_model_search_set_attribute(self): 133 | new_email = 'philip@planetexpress.com' 134 | with self.app.test_request_context(): 135 | entry = self.user.query.filter( 136 | 'email: %s' % self.app.config['USER_EMAIL'] 137 | ).first() 138 | entry.email = new_email 139 | self.assertEqual(entry.email, new_email) 140 | 141 | def test_model_search_set_attribute_list(self): 142 | new_email_list = ['philip@planetexpress.com', 143 | 'a.fry@planetexpress.com'] 144 | with self.app.test_request_context(): 145 | entry = self.user.query.filter( 146 | 'email: %s' % self.app.config['USER_EMAIL'] 147 | ).first() 148 | entry.email = new_email_list 149 | self.assertEqual(entry.email, new_email_list) 150 | 151 | def test_model_search_set_undefined_attr(self): 152 | def new_model(): 153 | user = self.user(active='1') 154 | with self.app.test_request_context(): 155 | self.assertRaises(LDAPAttributeError, new_model) 156 | 157 | def test_model_new(self): 158 | with self.app.test_request_context(): 159 | user = self.user(name='Rafael Römhild', 160 | email='rafael@planetexpress.com') 161 | self.assertEqual(user.email, 'rafael@planetexpress.com') 162 | 163 | def test_model_fetch_entry(self): 164 | uid = 'bender' 165 | with self.app.test_request_context(): 166 | user = self.user.query.filter('userid: {}'.format(uid)).first() 167 | self.assertEqual(user.userid, uid) 168 | 169 | def test_model_fetch_entry_with_components_in_and_false(self): 170 | uid = 'bender' 171 | with self.app.test_request_context(): 172 | user = self.user.query.filter( 173 | 'email: {0}, userid: {0}'.format(uid) 174 | ).all(components_in_and=False) 175 | self.assertEqual(user[0].userid, uid) 176 | 177 | def test_model_fetch_entry_authenticate(self): 178 | uid = 'fry' 179 | with self.app.test_request_context(): 180 | user = self.user.query.filter('userid: {}'.format(uid)).first() 181 | password = self.app.config['USER_PASSWORD'] 182 | self.assertTrue(user.authenticate(password)) 183 | 184 | def test_model_fetch_entry_exception(self): 185 | uid = 'xyz' 186 | with self.app.test_request_context(): 187 | user = self.user.query.filter('userid: {}'.format(uid)).first() 188 | self.assertEqual(user, None) 189 | 190 | def test_model_fetch_multible_entries(self): 191 | expected_uids = ['bender', 'fry', 'hermes', 'leela', 'professor', 192 | 'zoidberg'] 193 | response_uids = [] 194 | query_filter = 'email: *@planetexpress.com' 195 | with self.app.test_request_context(): 196 | entries = self.user.query.filter(query_filter).all() 197 | for entry in entries: 198 | response_uids.append(entry.userid) 199 | matched_uids = set(expected_uids).intersection(response_uids) 200 | self.assertEqual(len(expected_uids), len(matched_uids)) 201 | 202 | def test_model_get_dn(self): 203 | dn = 'cn=Philip J. Fry,ou=people,dc=planetexpress,dc=com' 204 | with self.app.test_request_context(): 205 | user = self.user.query.get(dn) 206 | self.assertEqual(dn, user.dn) 207 | 208 | def test_model_get_multivalued_rdn(self): 209 | dn = 'cn=Amy Wong+sn=Kroker,ou=people,dc=planetexpress,dc=com' 210 | with self.app.test_request_context(): 211 | user = self.user.query.get(dn) 212 | self.assertEqual('Kroker', user.surname) 213 | 214 | def test_model_get_attributes_dict(self): 215 | with self.app.test_request_context(): 216 | user = self.user.query.filter('userid: bender').first() 217 | attrs = ['name', 'email', 'userid'] 218 | attr_dict = user.get_attributes_dict() 219 | self.assertTrue(isinstance(attr_dict, dict)) 220 | for attr in attrs: 221 | self.assertTrue(isinstance(attr_dict[attr], list)) 222 | 223 | def test_model_to_json(self): 224 | with self.app.test_request_context(): 225 | user = self.user.query.filter('userid: bender').first() 226 | self.assertTrue(is_json(user.to_json())) 227 | 228 | def test_model_to_json_str_values(self): 229 | with self.app.test_request_context(): 230 | user = self.user.query.filter('userid: bender').first() 231 | self.assertTrue(is_json(user.to_json(str_values=True))) 232 | 233 | def test_model_iter(self): 234 | with self.app.test_request_context(): 235 | user = self.user.query.filter('userid: bender').first() 236 | for name, field in user._fields.items(): 237 | self.assertTrue(isinstance(field, self.ldap.Attribute)) 238 | 239 | def test_model_contains(self): 240 | with self.app.test_request_context(): 241 | user = self.user.query.filter('userid: bender').first() 242 | self.assertTrue(hasattr(user, 'userid')) 243 | 244 | def test_model_getarr_att_not_found(self): 245 | with self.app.test_request_context(): 246 | user = self.user.query.filter('userid: bender').first() 247 | self.assertFalse(hasattr(user, 'active')) 248 | 249 | def test_model_setattr(self): 250 | with self.app.test_request_context(): 251 | user = self.user.query.filter('userid: fry').first() 252 | user.userid = 'xyz' 253 | self.assertEqual(user.userid, 'xyz') 254 | 255 | def test_model_attribute_str(self): 256 | with self.app.test_request_context(): 257 | user = self.user.query.filter('userid: fry').first() 258 | self.assertTrue(isinstance(user.userid, STRING_TYPES)) 259 | 260 | def test_model_attribute_value_force_list(self): 261 | with self.app.test_request_context(): 262 | self.app.config['FORCE_ATTRIBUTE_VALUE_AS_LIST'] = True 263 | user = self.user.query.filter('userid: fry').first() 264 | self.assertTrue(isinstance(user.userid, list)) 265 | 266 | def test_model_attribute_iter(self): 267 | with self.app.test_request_context(): 268 | user = self.user.query.filter('userid: professor').first() 269 | self.assertTrue(isinstance(user.email, list)) 270 | for mail in user.email: 271 | pass 272 | 273 | def test_model_operation_add(self): 274 | uid = 'rafael-{}'.format(UID_SUFFIX) 275 | query_filter = 'userid: {}'.format(uid) 276 | with self.app.test_request_context(): 277 | new_user = self.user(name='Rafael Römhild', 278 | userid=uid, 279 | email='rafael@planetexpress.com', 280 | surname='Römhild', 281 | givenname='Raphael') 282 | self.assertTrue(new_user.save()) 283 | user = self.user.query.filter(query_filter).first() 284 | self.assertEqual(new_user.userid, user.userid) 285 | 286 | def test_model_operation_modify(self): 287 | uid = 'rafael-{}'.format(UID_SUFFIX) 288 | query_filter = 'userid: {}'.format(uid) 289 | with self.app.test_request_context(): 290 | mod_user = self.user.query.filter(query_filter).first() 291 | mod_user.givenname = 'Rafael' 292 | mod_user.title = 'SysAdmin' 293 | mod_user.email = [mod_user.email, 'it@planetexpress.co'] 294 | self.assertTrue(mod_user.save()) 295 | user = self.user.query.filter(query_filter).first() 296 | self.assertEqual(user.givenname, 'Rafael') 297 | self.assertEqual(user.surname, u'Römhild') 298 | self.assertEqual(user.title, 'SysAdmin') 299 | self.assertTrue('it@planetexpress.co' in user.email) 300 | 301 | def test_model_operation_remove(self): 302 | uid = 'rafael-{}'.format(UID_SUFFIX) 303 | query_filter = 'userid: {}'.format(uid) 304 | with self.app.test_request_context(): 305 | user = self.user.query.filter(query_filter).first() 306 | self.assertTrue(user.delete()) 307 | user = self.user.query.filter(query_filter).first() 308 | self.assertEqual(user, None) 309 | 310 | 311 | class LDAPConnModelInheritanceTestCase(unittest.TestCase): 312 | 313 | def setUp(self): 314 | app = flask.Flask(__name__) 315 | app.config.from_object(__name__) 316 | app.config.from_envvar('LDAP_SETTINGS', silent=True) 317 | ldap = LDAPConn(app) 318 | 319 | self.app = app 320 | self.ldap = ldap 321 | self.user = Account 322 | 323 | def test_model_operation_add(self): 324 | uid = 'rafael-{}'.format(UID_SUFFIX) 325 | query_filter = 'userid: {}'.format(uid) 326 | with self.app.test_request_context(): 327 | new_user = self.user(name='Rafael Römhild', 328 | userid=uid, 329 | email='rafael@planetexpress.com', 330 | surname='Römhild', 331 | givenname='Raphael', 332 | uidnumber=1000, 333 | gidnumber=1000, 334 | shell='/bin/false', 335 | home='/home/' + uid) 336 | self.assertTrue(new_user.save()) 337 | user = self.user.query.filter(query_filter).first() 338 | self.assertEqual(new_user.userid, user.userid) 339 | 340 | def test_model_operation_modify(self): 341 | uid = 'rafael-{}'.format(UID_SUFFIX) 342 | query_filter = 'userid: {}'.format(uid) 343 | with self.app.test_request_context(): 344 | mod_user = self.user.query.filter(query_filter).first() 345 | mod_user.givenname = 'Rafael' 346 | mod_user.title = 'SysAdmin' 347 | mod_user.shell = '/bin/bash' 348 | mod_user.email = [mod_user.email, 'it@planetexpress.co'] 349 | self.assertTrue(mod_user.save()) 350 | user = self.user.query.filter(query_filter).first() 351 | self.assertEqual(user.givenname, 'Rafael') 352 | self.assertEqual(user.surname, u'Römhild') 353 | self.assertEqual(user.title, 'SysAdmin') 354 | self.assertTrue('it@planetexpress.co' in user.email) 355 | 356 | def test_model_operation_remove(self): 357 | uid = 'rafael-{}'.format(UID_SUFFIX) 358 | query_filter = 'userid: {}'.format(uid) 359 | with self.app.test_request_context(): 360 | user = self.user.query.filter(query_filter).first() 361 | self.assertTrue(user.delete()) 362 | user = self.user.query.filter(query_filter).first() 363 | self.assertEqual(user, None) 364 | 365 | 366 | class LDAPConnAuthTestCase(LDAPConnTestCase): 367 | 368 | def test_authenticate_user(self): 369 | with self.app.test_request_context(): 370 | retval = self.ldap.authenticate( 371 | username=self.app.config['USER_EMAIL'], 372 | password=self.app.config['USER_PASSWORD'], 373 | base_dn=self.app.config['LDAP_AUTH_BASEDN'], 374 | attribute=self.app.config['LDAP_SEARCH_ATTR'], 375 | ) 376 | self.assertTrue(retval) 377 | 378 | def test_authenticate_user_with_dn(self): 379 | dn = 'cn=Philip J. Fry,ou=people,dc=planetexpress,dc=com' 380 | with self.app.test_request_context(): 381 | retval = self.ldap.authenticate( 382 | username=dn, 383 | password=self.app.config['USER_PASSWORD'], 384 | ) 385 | self.assertTrue(retval) 386 | 387 | def test_authenticate_user_basedn_filter(self): 388 | with self.app.test_request_context(): 389 | retval = self.ldap.authenticate( 390 | username=self.app.config['USER_EMAIL'], 391 | password=self.app.config['USER_PASSWORD'], 392 | attribute=self.app.config['LDAP_SEARCH_ATTR'], 393 | base_dn=self.app.config['LDAP_AUTH_BASEDN'], 394 | search_filter=self.app.config['LDAP_AUTH_SEARCH_FILTER'] 395 | ) 396 | self.assertTrue(retval) 397 | 398 | def test_authenticate_user_invalid_credentials(self): 399 | with self.app.test_request_context(): 400 | retval = self.ldap.authenticate( 401 | username=self.app.config['USER_EMAIL'], 402 | password='testpass', 403 | attribute=self.app.config['LDAP_SEARCH_ATTR'], 404 | base_dn=self.app.config['LDAP_AUTH_BASEDN'], 405 | ) 406 | self.assertFalse(retval) 407 | 408 | def test_authenticate_user_invalid_search_filter(self): 409 | with self.app.test_request_context(): 410 | retval = self.ldap.authenticate( 411 | username=self.app.config['USER_EMAIL'], 412 | password=self.app.config['USER_PASSWORD'], 413 | attribute=self.app.config['LDAP_SEARCH_ATTR'], 414 | base_dn=self.app.config['LDAP_AUTH_BASEDN'], 415 | search_filter='x=y' 416 | ) 417 | self.assertFalse(retval) 418 | 419 | def test_authenticate_user_search_filter_no_result(self): 420 | with self.app.test_request_context(): 421 | retval = self.ldap.authenticate( 422 | username=self.app.config['USER_EMAIL'], 423 | password=self.app.config['USER_PASSWORD'], 424 | attribute=self.app.config['LDAP_SEARCH_ATTR'], 425 | base_dn=self.app.config['LDAP_AUTH_BASEDN'], 426 | search_filter='(uidNumber=*)' 427 | ) 428 | self.assertFalse(retval) 429 | 430 | 431 | class LDAPConnSSLTestCase(unittest.TestCase): 432 | 433 | def setUp(self): 434 | app = flask.Flask(__name__) 435 | app.config.from_object(__name__) 436 | app.config.from_envvar('LDAP_SETTINGS', silent=True) 437 | app.config['LDAP_PORT'] = app.config.get('LDAP_SSL_PORT', 636) 438 | app.config['LDAP_USE_SSL'] = True 439 | ldap = LDAPConn(app) 440 | 441 | self.app = app 442 | self.ldap = ldap 443 | 444 | def test_whoami(self): 445 | with self.app.test_request_context(): 446 | conn = self.ldap.connection 447 | self.assertEqual(conn.extend.standard.who_am_i(), 448 | 'dn:{}'.format(self.app.config['LDAP_BINDDN'])) 449 | 450 | 451 | class LDAPConnAnonymousTestCase(unittest.TestCase): 452 | 453 | def setUp(self): 454 | app = flask.Flask(__name__) 455 | app.config.from_object(__name__) 456 | app.config.from_envvar('LDAP_SETTINGS', silent=True) 457 | app.config['LDAP_BINDDN'] = None 458 | app.config['LDAP_SECRET'] = None 459 | ldap = LDAPConn(app) 460 | 461 | self.app = app 462 | self.ldap = ldap 463 | 464 | def test_whoami(self): 465 | with self.app.test_request_context(): 466 | conn = self.ldap.connection 467 | self.assertEqual(conn.extend.standard.who_am_i(), None) 468 | 469 | 470 | class LDAPConnTLSCertRequiredTestCase(unittest.TestCase): 471 | def setUp(self): 472 | app = flask.Flask(__name__) 473 | app.config.from_object(__name__) 474 | app.config.from_envvar('LDAP_SETTINGS', silent=True) 475 | app.config['LDAP_BINDDN'] = None 476 | app.config['LDAP_SECRET'] = None 477 | app.config['LDAP_REQUIRE_CERT'] = ssl.CERT_REQUIRED 478 | ldap = LDAPConn(app) 479 | 480 | self.app = app 481 | self.ldap = ldap 482 | 483 | def connect(self): 484 | return self.ldap.connection 485 | 486 | def test_connection(self): 487 | with self.app.test_request_context(): 488 | self.assertRaises(LDAPStartTLSError, self.connect) 489 | 490 | 491 | class LDAPConnNoTLSAnonymousTestCase(unittest.TestCase): 492 | 493 | def setUp(self): 494 | app = flask.Flask(__name__) 495 | app.config.from_object(__name__) 496 | app.config.from_envvar('LDAP_SETTINGS', silent=True) 497 | app.config['LDAP_BINDDN'] = None 498 | app.config['LDAP_SECRET'] = None 499 | app.config['LDAP_USE_TLS'] = False 500 | ldap = LDAPConn(app) 501 | 502 | self.app = app 503 | self.ldap = ldap 504 | 505 | def test_whoami(self): 506 | with self.app.test_request_context(): 507 | conn = self.ldap.connection 508 | self.assertEqual(conn.extend.standard.who_am_i(), None) 509 | 510 | 511 | class LDAPConnDeprecatedTestCase(LDAPConnTestCase): 512 | 513 | def test_connection_search(self): 514 | attr = self.app.config['LDAP_SEARCH_ATTR'] 515 | with self.app.test_request_context(): 516 | self.ldap.search(self.app.config['LDAP_BASEDN'], 517 | self.app.config['LDAP_SEARCH_FILTER'], 518 | SUBTREE, attributes=[attr]) 519 | result = self.ldap.result() 520 | response = self.ldap.response() 521 | self.assertTrue(response) 522 | self.assertEqual(response[0]['attributes'][attr][0], 523 | self.app.config['USER_EMAIL']) 524 | 525 | def test_whoami_deprecated(self): 526 | with self.app.test_request_context(): 527 | whoami = self.ldap.whoami() 528 | self.assertEqual(whoami, 529 | 'dn:{}'.format(self.app.config['LDAP_BINDDN'])) 530 | 531 | 532 | if __name__ == '__main__': 533 | success = False 534 | try: 535 | import docker 536 | client = docker.from_env() 537 | container = client.containers.run('rroemhild/test-openldap', 538 | ports={'389/tcp': 389, 539 | '636/tcp': 636}, 540 | detach=True, 541 | privileged=True, 542 | remove=True) 543 | 544 | print('Docker container {0} started'.format(container.id)) 545 | print('Wait 3 seconds until slapd is started...') 546 | time.sleep(3) 547 | 548 | print('Run unit test...') 549 | runner = unittest.main(exit=False) 550 | success = runner.result.wasSuccessful() 551 | 552 | print('Stop and removing container...') 553 | container.stop() 554 | except (ImportError, ValueError): 555 | print('Can\'t run docker image. Try to run tests without.') 556 | unittest.main() 557 | 558 | if success is not True: 559 | sys.exit(1) 560 | --------------------------------------------------------------------------------