-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Significant progress towards a working library. Handles authenticatio…
…n very well so far
- Loading branch information
Showing
8 changed files
with
339 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
include LICENSE README.md setup.cfg VERSION | ||
recursive-include flask_boilerplate_utils/templates *.html |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
0.1.53 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import os | ||
setup_path = os.path.join(os.path.dirname( | ||
os.path.abspath(__file__)), | ||
'setup.py', | ||
) | ||
version_path = os.path.join(os.path.dirname( | ||
os.path.abspath(__file__)), | ||
'VERSION', | ||
) | ||
version = '0.0.0' | ||
with open(version_path, 'r') as vh: | ||
version = vh.read() | ||
|
||
version = version.split('.') | ||
version[-1] = str(int(version[-1]) + 1) | ||
|
||
version = '.'.join(version) | ||
|
||
with open(version_path, 'w') as vh: | ||
vh.write(version) | ||
|
||
if not os.system('python {} sdist upload -r pypi'.format(setup_path)): | ||
print ("Uploaded to PyPI as Version {}. Don't forget to tag!".format(version)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,277 @@ | ||
import logging | ||
import flask | ||
import ldap3 | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
class LDAP3ServerConnectionException(Exception): | ||
pass | ||
|
||
from enum import Enum | ||
|
||
class AuthenticationResponseStatus(Enum): | ||
fail = 'fail' | ||
success = 'success' | ||
|
||
class AuthenticationResponse(object): | ||
status = AuthenticationResponseStatus.fail | ||
user_info = None | ||
user_id = None | ||
user_dn = None | ||
|
||
|
||
class LDAP3LoginManager(object): | ||
def __init__(self, app=None): | ||
if app is not None: | ||
self.init_app(app) | ||
|
||
self._save_user = None | ||
|
||
def init_app(self, app): | ||
''' | ||
Configures an application. This registers an `after_request` call, and | ||
attaches this `LoginManager` to it as `app.login_manager`. | ||
''' | ||
|
||
app.ldap_login_manager = self | ||
|
||
app.config.setdefault('LDAP_PORT', 389) | ||
app.config.setdefault('LDAP_HOST', None) | ||
app.config.setdefault('LDAP_USE_SSL', False) | ||
app.config.setdefault('LDAP_BASE_DN', '') | ||
app.config.setdefault('LDAP_BIND_USER_DN', None) | ||
app.config.setdefault('LDAP_BIND_USER_PASSWORD', None) | ||
|
||
app.config.setdefault('LDAP_FAIL_AUTH_ON_MULTIPLE_FOUND', False) | ||
|
||
# Prepended to the Base DN to limit scope when searching for Users/Groups. | ||
app.config.setdefault('LDAP_USER_DN', '') | ||
app.config.setdefault('LDAP_GROUP_DN', '') | ||
|
||
|
||
|
||
app.config.setdefault('LDAP_USER_SEARCH_SCOPE', 'SEARCH_SCOPE_SINGLE_LEVEL') | ||
app.config.setdefault('LDAP_GROUP_SEARCH_SCOPE', 'SEARCH_SCOPE_SINGLE_LEVEL') | ||
|
||
# Ldap Filters | ||
app.config.setdefault('LDAP_USER_OBJECT_FILTER', '(objectclass=inetorgperson)') | ||
app.config.setdefault('LDAP_USER_LOGIN_ATTR_HUMAN_NAME', 'User ID') | ||
app.config.setdefault('LDAP_USER_LOGIN_ATTR', 'uid') | ||
app.config.setdefault('LDAP_GROUP_OBJECT_FILTER', '(objectclass=groupOfUniqueNames)') | ||
app.config.setdefault('LDAP_GROUP_MEMBERS_ATTR', 'uniqueMember') | ||
app.config.setdefault('LDAP_USER_MEMBER_ATTR', 'memberOf') | ||
app.config.setdefault('LDAP_USER_RDN_ATTR', 'uid') | ||
app.config.setdefault('LDAP_GET_USER_ATTRIBUTES', ldap3.ALL_ATTRIBUTES) | ||
app.config.setdefault('LDAP_GET_GROUP_ATTRIBUTES', ldap3.ALL_ATTRIBUTES) | ||
|
||
app.config.setdefault('LDAP_BIND_AUTHENTICATION_TYPE', 'AUTH_SIMPLE') | ||
|
||
|
||
if hasattr(app, 'teardown_appcontext'): | ||
app.teardown_appcontext(self.teardown) | ||
else: | ||
app.teardown_request(self.teardown) | ||
|
||
self.app = app | ||
self.config = app.config | ||
|
||
self._server_pool = ldap3.ServerPool( | ||
[], | ||
ldap3.POOLING_STRATEGY_FIRST, | ||
active=True, | ||
exhaust=True | ||
) | ||
|
||
self.add_server( | ||
hostname=self.config.get('LDAP_HOST'), | ||
port=self.config.get('LDAP_PORT'), | ||
use_ssl=self.config.get('LDAP_USE_SSL') | ||
) | ||
|
||
def add_server(self, hostname, port, use_ssl): | ||
server = ldap3.Server(hostname, port=port, use_ssl=use_ssl) | ||
self._server_pool.add(server) | ||
return self._server_pool | ||
|
||
def teardown(self, exception): | ||
print("TEARDOWN FOR SOMETING?") | ||
pass | ||
|
||
|
||
def save_user(self, callback): | ||
''' | ||
This sets the callback for staving a user that has been looked up from from ldap. | ||
The function you set should take a username (unicode) and and userdata (dict). | ||
:param callback: The callback for retrieving a user object. | ||
''' | ||
|
||
self._save_user = callback | ||
return callback | ||
|
||
|
||
def authenticate(self, username, password): | ||
if self.config.get('LDAP_USER_RDN_ATTR') == self.config.get('LDAP_USER_LOGIN_ATTR'): | ||
# Since the user's RDN is the same as the login field, | ||
# we can do a direct bind. | ||
result = self.authenticate_direct_bind(username, password) | ||
else: | ||
# We need to search the User's DN to find who the user is (and their DN) | ||
# so we can try bind with their password. | ||
result = self.authenticate_search_bind(username, password) | ||
|
||
return result | ||
|
||
|
||
def authenticate_direct_bind(self, username, password): | ||
# Format the username for direct binding | ||
bind_user = '{rdn}={username},{user_search_dn}'.format( | ||
rdn=self.config.get('LDAP_USER_RDN_ATTR'), | ||
username=username, | ||
user_search_dn=self.full_user_search_dn, | ||
) | ||
|
||
log.debug("Directly binding a connection to a server with user:'{}'".format(bind_user)) | ||
connection = self.make_connection( | ||
bind_user=bind_user, | ||
bind_password=password, | ||
) | ||
|
||
response = AuthenticationResponse() | ||
|
||
try: | ||
connection.bind() | ||
log.debug("Authentication was successful for user '{}'".format(username)) | ||
response.status = AuthenticationResponseStatus.success | ||
# Get user info here. | ||
|
||
except ldap3.LDAPInvalidCredentialsResult as e: | ||
log.debug("Authentication for user '{}' returned with "\ | ||
"result '{}'".format(username, e)) | ||
response.status = AuthenticationResponseStatus.fail | ||
except Exception as e: | ||
self.destroy_connection(connection) | ||
log.error(e) | ||
raise e | ||
|
||
return response | ||
|
||
def authenticate_search_bind(self, username, password): | ||
connection = self.make_connection( | ||
bind_user=self.config.get('LDAP_BIND_USER_DN'), | ||
bind_password=self.config.get('LDAP_BIND_USER_PASSWORD'), | ||
) | ||
|
||
try: | ||
connection.bind() | ||
log.debug("Successfully bound to LDAP as '{}' for search_bind method".format( | ||
self.config.get('LDAP_BIND_USER_DN') or 'Anonymous' | ||
)) | ||
except Exception as e: | ||
self.destroy_connection(connection) | ||
log.error(e) | ||
raise e | ||
|
||
# Find the user in the search path. | ||
user_filter = '({search_attr}={username})'.format( | ||
search_attr=self.config.get('LDAP_USER_LOGIN_ATTR'), | ||
username=username | ||
) | ||
search_filter = '(&{}{})'.format( | ||
self.config.get('LDAP_USER_OBJECT_FILTER'), | ||
user_filter, | ||
) | ||
|
||
connection.search( | ||
search_base=self.full_user_search_dn, | ||
search_filter=search_filter, | ||
search_scope=getattr(ldap3, self.config.get('LDAP_USER_SEARCH_SCOPE')), | ||
attributes=self.config.get('LDAP_GET_USER_ATTRIBUTES') | ||
) | ||
|
||
# print(connection.result) | ||
# print(connection.response) | ||
response = AuthenticationResponse() | ||
|
||
if len(connection.response) == 0 or \ | ||
(self.config.get('LDAP_FAIL_AUTH_ON_MULTIPLE_FOUND')\ | ||
and len(connection.response) > 1): | ||
# Don't allow them to log in. | ||
log.debug("Authentication was not successful for user '{}'".format(username)) | ||
|
||
else: | ||
for user in connection.response: | ||
# Attempt to bind with each user we find until we can find | ||
# one that works. | ||
user_connection = self.make_connection( | ||
bind_user=user['dn'], | ||
bind_password=password | ||
) | ||
log.debug("Directly binding a connection to a server with user:'{}'".format(user['dn'])) | ||
try: | ||
user_connection.bind() | ||
log.debug("Authentication was successful for user '{}'".format(username)) | ||
response.status = AuthenticationResponseStatus.success | ||
|
||
# Populate User Data | ||
user['attributes']['dn'] = user['dn'] | ||
response.user_info = user['attributes'] | ||
response.user_id = username, | ||
response.user_dn = user['dn'] | ||
break | ||
|
||
|
||
except ldap3.LDAPInvalidCredentialsResult as e: | ||
log.debug("Authentication was not successful for user '{}'".format(username)) | ||
response.status = AuthenticationResponseStatus.fail | ||
except Exception as e: | ||
self.destroy_connection(user_connection) | ||
log.error(e) | ||
raise e | ||
|
||
|
||
self.destroy_connection(connection) | ||
return response | ||
|
||
def get_user_groups(self): | ||
pass | ||
|
||
def get_user_info(self, uid, connection=None): | ||
|
||
pass | ||
|
||
def make_connection(self, bind_user=None, bind_password=None): | ||
authentication = ldap3.AUTH_ANONYMOUS | ||
if bind_user: | ||
authentication = getattr(ldap3, self.config.get( | ||
'LDAP_BIND_AUTHENTICATION_TYPE')) | ||
|
||
connection = ldap3.Connection( | ||
server=self._server_pool, | ||
read_only=True, | ||
user=bind_user, | ||
password=bind_password, | ||
client_strategy=ldap3.STRATEGY_SYNC, | ||
authentication=authentication, | ||
check_names=True, | ||
raise_exceptions=True | ||
) | ||
return connection | ||
|
||
|
||
def destroy_connection(self, connection): | ||
connection.unbind() | ||
|
||
@property | ||
def full_user_search_dn(self): | ||
return '{user_dn},{base_dn}'.format( | ||
user_dn=self.config.get('LDAP_USER_DN'), | ||
base_dn=self.config.get('LDAP_BASE_DN'), | ||
) | ||
|
||
@property | ||
def full_group_search_dn(self): | ||
return '{group_dn},{base_dn}'.format( | ||
user_dn=self.config.get('LDAP_GROUP_DN'), | ||
base_dn=self.config.get('LDAP_BASE_DN'), | ||
) | ||
|
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
[metadata] | ||
description-file = README.rst |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import os | ||
from setuptools import setup | ||
|
||
readme_path = os.path.join(os.path.dirname( | ||
os.path.abspath(__file__)), | ||
'README.rst', | ||
) | ||
long_description = open(readme_path).read() | ||
version_path = os.path.join(os.path.dirname( | ||
os.path.abspath(__file__)), | ||
'VERSION', | ||
) | ||
version = open(version_path).read() | ||
|
||
|
||
setup( | ||
name='flask-boilerplate-utils', | ||
version=version, | ||
packages=['flask_ldap3_login'], | ||
author="Nick Whyte", | ||
author_email='nick@nickwhyte.com', | ||
description="Flask-Login support for LDAP3.", | ||
long_description=long_description, | ||
url='https://github.com/nickw444/flask-ldap3-login', | ||
zip_safe=False, | ||
install_requires=[ | ||
"ldap3", | ||
"Flask-Login", | ||
"Flask-Principal", | ||
"Flask" | ||
], | ||
) |