From 027ada1f5c77ed66172dd68e76f1ec7b12c660ff Mon Sep 17 00:00:00 2001 From: Jake Moshenko Date: Fri, 9 May 2014 17:39:43 -0400 Subject: [PATCH] First stab at LDAP integration. --- Dockerfile | 3 + app.py | 4 ++ config.py | 3 + data/users.py | 122 +++++++++++++++++++++++++++++++++++++++++ endpoints/api/user.py | 6 +- requirements-nover.txt | 1 + 6 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 data/users.py diff --git a/Dockerfile b/Dockerfile index e5a9d7e07..ec0e1f4e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,9 @@ RUN tar xjf phantomjs.tar.bz2 && ln -s `pwd`/phantomjs*/bin/phantomjs /usr/bin/p RUN apt-get install -y nodejs RUN npm install -g grunt-cli +# LDAP +RUN apt-get install libldap2-dev libsasl2-dev + ADD binary_dependencies binary_dependencies RUN gdebi --n binary_dependencies/*.deb diff --git a/app.py b/app.py index aa663418c..b943f62d1 100644 --- a/app.py +++ b/app.py @@ -10,6 +10,7 @@ import features from storage import Storage from data.userfiles import Userfiles +from data.users import UserAuthentication from util.analytics import Analytics from util.exceptionlog import Sentry from data.billing import Billing @@ -46,3 +47,6 @@ userfiles = Userfiles(app) analytics = Analytics(app) billing = Billing(app) sentry = Sentry(app) + +from data import model +authentication = UserAuthentication(app, model) diff --git a/config.py b/config.py index d5fc126cb..9202b26d7 100644 --- a/config.py +++ b/config.py @@ -72,6 +72,9 @@ class DefaultConfig(object): STORAGE_TYPE = 'LocalStorage' STORAGE_PATH = 'test/data/registry' + # Authentication + AUTHENTICATION_TYPE = 'Database' + # Build logs BUILDLOGS = BuildLogs('logs.quay.io') # Change me diff --git a/data/users.py b/data/users.py new file mode 100644 index 000000000..895c6872c --- /dev/null +++ b/data/users.py @@ -0,0 +1,122 @@ +import ldap +import logging + + +logger = logging.getLogger(__name__) + + +class DatabaseUsers(object): + def __init__(self, app_db): + self._app_db = app_db + + def verify_user(self, username_or_email, password): + """ Simply delegate to the model implementation. """ + return self._app_db.verify_user(username_or_email, password) + + +class LDAPConnection(object): + def __init__(self, ldap_uri, user_dn, user_pw): + self._ldap_uri = ldap_uri + self._user_dn = user_dn + self._user_pw = user_pw + self._conn = None + + def __enter__(self): + self._conn = ldap.initialize(self._ldap_uri) + self._conn.simple_bind_s(self._user_dn, self._user_pw) + return self._conn + + def __exit__(self, exc_type, value, tb): + self._conn.unbind_s() + + +class LDAPUsers(object): + def __init__(self, app_db, ldap_uri, base_dn, admin_dn, admin_passwd, user_rdn, uid_attr, + email_attr, passwd_attr): + self._app_db = app_db + self._ldap_conn = LDAPConnection(ldap_uri, admin_dn, admin_passwd) + self._base_dn = base_dn + self._user_rdn = user_rdn + self._uid_attr = uid_attr + self._email_attr = email_attr + self._passwd_attr = passwd_attr + + def verify_user(self, username_or_email, password): + """ Verify the credentials with LDAP and if they are valid, create or update the user + in our database. """ + + with self._ldap_conn as conn: + user_search_dn = ','.join(self._user_rdn + self._base_dn) + query = '(|({0}={2})({1}={2}))'.format(self._uid_attr, self._email_attr, + username_or_email) + user = conn.search_s(user_search_dn, ldap.SCOPE_SUBTREE, query) + + if len(user) != 1: + return None + + found_dn, found_response = user[0] + + # First validate the password + valid_passwd = conn.compare_s(found_dn, self._passwd_attr, password) == 1 + if not valid_passwd: + return None + + logger.debug('LDAP Response: %s', found_response) + + # Now check if we have the same username in our DB + username = found_response[self._uid_attr][0] + email = found_response[self._email_attr][0] + password = found_response[self._passwd_attr][0] + db_user = self._app_db.get_user(username) + + logger.debug('Email: %s', email) + + if not db_user: + # We must create the user in our db + db_user = self._app_db.create_user(username, 'password_from_ldap', email) + db_user.verified = True + else: + # Update the db attributes from ldap + db_user.email = email + + db_user.save() + + return db_user + + +class UserAuthentication(object): + def __init__(self, app=None, model=None): + self.app = app + if app is not None: + self.state = self.init_app(app, model) + else: + self.state = None + + def init_app(self, app, model): + authentication_type = app.config.get('AUTHENTICATION_TYPE', 'Database') + + if authentication_type == 'Database': + users = DatabaseUsers(model) + elif authentication_type == 'LDAP': + ldap_uri = app.config.get('LDAP_URI', 'ldap://localhost') + base_dn = app.config.get('LDAP_BASE_DN') + admin_dn = app.config.get('LDAP_ADMIN_DN') + admin_passwd = app.config.get('LDAP_ADMIN_PASSWD') + user_rdn = app.config.get('LDAP_USER_RDN', []) + uid_attr = app.config.get('LDAP_UID_ATTR', 'uid') + email_attr = app.config.get('LDAP_EMAIL_ATTR', 'mail') + passwd_attr = app.config.get('LDAP_PASSWD_ATTR', 'userPassword') + + users = LDAPUsers(model, ldap_uri, base_dn, admin_dn, admin_passwd, user_rdn, uid_attr, + email_attr, passwd_attr) + + else: + raise RuntimeError('Unknown authentication type: %s' % authentication_type) + + # register extension with app + app.extensions = getattr(app, 'extensions', {}) + app.extensions['authentication'] = users + return users + + def __getattr__(self, name): + return getattr(self.state, name, None) diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 437f37450..4d54b3e50 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -5,7 +5,7 @@ from flask import request from flask.ext.login import logout_user from flask.ext.principal import identity_changed, AnonymousIdentity -from app import app, billing as stripe +from app import app, billing as stripe, authentication from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error, log_action, internal_only, NotFound, require_user_admin, InvalidToken, require_scope, format_date, hide_if, show_if) @@ -227,7 +227,7 @@ def conduct_signin(username_or_email, password): needs_email_verification = False invalid_credentials = False - verified = model.verify_user(username_or_email, password) + verified = authentication.verify_user(username_or_email, password) if verified: if common_login(verified): return {'success': True} @@ -289,7 +289,7 @@ class ConvertToOrganization(ApiResource): # Ensure that the sign in credentials work. admin_password = convert_data['adminPassword'] - if not model.verify_user(admin_username, admin_password): + if not authentication.verify_user(admin_username, admin_password): raise request_error(reason='invaliduser', message='The admin user credentials are not valid') diff --git a/requirements-nover.txt b/requirements-nover.txt index cc370da9d..efda6ebef 100644 --- a/requirements-nover.txt +++ b/requirements-nover.txt @@ -32,3 +32,4 @@ python-magic reportlab==2.7 blinker raven +python-ldap