diff --git a/doorman/application.py b/doorman/application.py index e3f18e5..81d299a 100644 --- a/doorman/application.py +++ b/doorman/application.py @@ -7,8 +7,8 @@ from doorman.assets import assets from doorman.manage import blueprint as backend from doorman.extensions import ( - db, debug_toolbar, log_tee, login_manager, mail, make_celery, metrics, - migrate, rule_manager + db, debug_toolbar, ldap_manager, log_tee, login_manager, mail, + make_celery, metrics, migrate, rule_manager ) from doorman.settings import ProdConfig from doorman.tasks import celery @@ -105,6 +105,12 @@ def register_auth_method(app): login_manager.login_view = 'users.login' login_manager.login_message_category = 'warning' + if app.config['DOORMAN_AUTH_METHOD'] == 'ldap': + ldap_manager.init_app(app) + return + + # no other authentication methods left, falling back to OAuth + if app.config['DOORMAN_AUTH_METHOD'] != 'doorman': login_manager.login_message = None login_manager.needs_refresh_message = None diff --git a/doorman/extensions.py b/doorman/extensions.py index 6e20883..a437f78 100644 --- a/doorman/extensions.py +++ b/doorman/extensions.py @@ -4,6 +4,7 @@ from celery import Celery from flask_bcrypt import Bcrypt from flask_debugtoolbar import DebugToolbarExtension +from flask_ldap3_login import LDAP3LoginManager from flask_login import LoginManager from flask_mail import Mail from flask_migrate import Migrate @@ -229,6 +230,7 @@ def init_app(self, app): migrate = Migrate() debug_toolbar = DebugToolbarExtension() log_tee = LogTee() +ldap_manager = LDAP3LoginManager() login_manager = LoginManager() metrics = Metrics() rule_manager = RuleManager() diff --git a/doorman/settings.py b/doorman/settings.py index 8b85e98..1dc1816 100644 --- a/doorman/settings.py +++ b/doorman/settings.py @@ -118,9 +118,12 @@ class Config(object): # only applicable when DOORMAN_AUTH_METHOD = 'doorman' SESSION_PROTECTION = "strong" + BCRYPT_LOG_ROUNDS = 13 + DOORMAN_AUTH_METHOD = None # DOORMAN_AUTH_METHOD = 'doorman' # DOORMAN_AUTH_METHOD = 'google' + # DOORMAN_AUTH_METHOD = 'ldap' DOORMAN_OAUTH_GOOGLE_ALLOWED_DOMAINS = [ ] @@ -131,7 +134,31 @@ class Config(object): DOORMAN_OAUTH_CLIENT_ID = '' DOORMAN_OAUTH_CLIENT_SECRET = '' - BCRYPT_LOG_ROUNDS = 13 + # When using DOORMAN_AUTH_METHOD = 'ldap', see + # http://flask-ldap3-login.readthedocs.io/en/latest/configuration.html#core + # Note: not all configuration options are documented at the link + # provided above. A complete list of options can be groked by + # reviewing the the flask-ldap3-login code. + + # LDAP_HOST = None + # LDAP_PORT = 636 + # LDAP_USE_SSL = True + # LDAP_BASE_DN = 'dc=example,dc=org' + # LDAP_USER_DN = 'ou=People' + # LDAP_GROUP_DN = '' + # LDAP_USER_OBJECT_FILTER = '(objectClass=inetOrgPerson)' + # LDAP_USER_LOGIN_ATTR = 'uid' + # LDAP_USER_RDN_ATTR = 'uid' + # LDAP_GROUP_SEARCH_SCOPE = 'SEARCH_SCOPE_WHOLE_SUBTREE' + # LDAP_GROUP_OBJECT_FILTER = '(cn=*)(objectClass=groupOfUniqueNames)' + # LDAP_GROUP_MEMBERS_ATTR = 'uniquemember' + # LDAP_GET_GROUP_ATTRIBUTES = ['cn'] + # LDAP_OPT_X_TLS_CACERTFILE = None + # LDAP_OPT_X_TLS_CERTIFICATE_FILE = None + # LDAP_OPT_X_TLS_PRIVATE_KEY_FILE = None + # LDAP_OPT_X_TLS_REQUIRE_CERT = 2 # ssl.CERT_REQUIRED + # LDAP_OPT_X_TLS_USE_VERSION = 3 # ssl.PROTOCOL_TLSv1 + # LDAP_OPT_X_TLS_VALID_NAMES = [] class ProdConfig(Config): diff --git a/doorman/templates/login.html b/doorman/templates/login.html index 5ac0f6b..fff9817 100644 --- a/doorman/templates/login.html +++ b/doorman/templates/login.html @@ -20,7 +20,7 @@ - {% if form.auth_method == 'doorman' %} + {% if form.auth_method in ('doorman', 'ldap') %}
diff --git a/doorman/users/forms.py b/doorman/users/forms.py index 5ef4a7d..24b96e9 100644 --- a/doorman/users/forms.py +++ b/doorman/users/forms.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- from flask import current_app +from flask_ldap3_login import AuthenticationResponseStatus from flask_wtf import Form from wtforms import BooleanField, PasswordField, StringField from wtforms.validators import DataRequired, Optional -from doorman.extensions import bcrypt +from doorman.extensions import bcrypt, ldap_manager from doorman.models import User @@ -42,10 +43,30 @@ def validate(self): self.username.errors.append(error_message) return False + return True + elif current_app.config['DOORMAN_AUTH_METHOD'] == 'ldap': - pass + result = ldap_manager.authenticate( + self.username.data, + self.password.data + ) + + if result.status == AuthenticationResponseStatus.fail: + self.username.errors.append(error_message) + return False + + self.user = ldap_manager._save_user( + result.user_dn, + result.user_id, + result.user_info, + result.user_groups + ) + return True + + elif current_app.config['DOORMAN_AUTH_METHOD'] is None: + return True - return True + return False @property def auth_method(self): diff --git a/doorman/users/views.py b/doorman/users/views.py index dfd45d3..2441cbe 100644 --- a/doorman/users/views.py +++ b/doorman/users/views.py @@ -13,7 +13,7 @@ from oauthlib.oauth2 import OAuth2Error from .forms import LoginForm -from doorman.extensions import login_manager +from doorman.extensions import ldap_manager, login_manager from doorman.models import User from doorman.utils import flash_errors @@ -29,6 +29,21 @@ def load_user(user_id): return User.get_by_id(int(user_id)) +@ldap_manager.save_user +def save_user(dn, username, userdata, memberships): + user = User.query.filter_by(username=username).first() + kwargs = {} + kwargs['username'] = username + + if 'givenName' in userdata: + kwargs['first_name'] = userdata['givenName'][0] + + if 'sn' in userdata: + kwargs['last_name'] = userdata['sn'][0] + + return user.update(**kwargs) if user else User.create(**kwargs) + + @blueprint.route('/login', methods=['GET', 'POST']) def login(): next = request.args.get('next', url_for('manage.index')) diff --git a/requirements/dev.txt b/requirements/dev.txt index ca1ea66..a47275d 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -8,12 +8,14 @@ blinker==1.4 celery==3.1.23 cffi==1.6.0 cssmin==0.2.0 +enum34==1.1.6 factory-boy==2.7.0 fake-factory==0.5.7 Flask==0.10.1 Flask-Assets==0.11 Flask-Bcrypt==0.7.1 Flask-DebugToolbar==0.10.0 +flask-ldap3-login==0.9.9 Flask-Login==0.3.2 Flask-Mail==0.9.1 Flask-Migrate==1.8.0 @@ -27,6 +29,7 @@ itsdangerous==0.24 Jinja2==2.8 jsmin==2.2.1 kombu==3.0.35 +ldap3==1.3.1 Mako==1.0.4 MarkupSafe==0.23 mock==2.0.0 @@ -34,6 +37,7 @@ oauthlib==1.1.1 pbr==1.9.1 psycopg2==2.6.1 py==1.4.31 +pyasn1==0.1.9 pycparser==2.14 pytest==2.9.1 python-dateutil==2.5.3 diff --git a/requirements/prod.txt b/requirements/prod.txt index 0d72f15..ec91237 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -7,10 +7,12 @@ blinker==1.4 celery==3.1.23 cffi==1.6.0 cssmin==0.2.0 +enum34==1.1.6 Flask==0.10.1 Flask-Assets==0.11 Flask-Bcrypt==0.7.1 Flask-DebugToolbar==0.10.0 +flask-ldap3-login==0.9.9 Flask-Login==0.3.2 Flask-Mail==0.9.1 Flask-Migrate==1.8.0 @@ -22,10 +24,12 @@ itsdangerous==0.24 Jinja2==2.8 jsmin==2.2.1 kombu==3.0.35 +ldap3==1.3.1 Mako==1.0.4 MarkupSafe==0.23 oauthlib==1.1.1 psycopg2==2.6.1 +pyasn1==0.1.9 pycparser==2.14 python-editor==1.0 pytz==2016.4 diff --git a/setup.py b/setup.py index ed994ca..8b640f6 100644 --- a/setup.py +++ b/setup.py @@ -18,15 +18,22 @@ 'alembic==0.8.6', 'amqp==1.4.9', 'anyjson==0.3.3', + 'bcrypt==2.0.0', 'billiard==3.3.0.23', 'blinker==1.4', 'celery==3.1.23', + 'cffi==1.6.0', 'cssmin==0.2.0', + 'enum34==1.1.6', 'Flask==0.10.1', 'Flask-Assets==0.11', 'Flask-Bcrypt==0.7.1', + 'Flask-DebugToolbar==0.10.0', + 'flask-ldap3-login==0.9.9', 'Flask-Login==0.3.2', + 'Flask-Mail==0.9.1', 'Flask-Migrate==1.8.0', + 'flask-paginate==0.4.1', 'Flask-Script==2.0.5', 'Flask-SQLAlchemy==2.1', 'Flask-WTF==0.12', @@ -34,9 +41,13 @@ 'Jinja2==2.8', 'jsmin==2.2.1', 'kombu==3.0.35', + 'ldap3==1.3.1', 'Mako==1.0.4', 'MarkupSafe==0.23', + 'oauthlib==1.1.1', 'psycopg2==2.6.1', + 'pyasn1==0.1.9', + 'pycparser==2.14', 'python-editor==1.0', 'pytz==2016.4', 'redis==2.10.5',