Add additional options for LDAP

Fixes #1420
This commit is contained in:
Joseph Schorr 2016-05-03 15:02:39 -04:00
parent f0af2ca9c3
commit 42515ed9ec
5 changed files with 81 additions and 18 deletions

2
app.py
View file

@ -167,7 +167,7 @@ analytics = Analytics(app)
billing = Billing(app) billing = Billing(app)
sentry = Sentry(app) sentry = Sentry(app)
build_logs = BuildLogs(app) build_logs = BuildLogs(app)
authentication = UserAuthentication(app, OVERRIDE_CONFIG_DIRECTORY) authentication = UserAuthentication(app, config_provider, OVERRIDE_CONFIG_DIRECTORY)
userevents = UserEventsBuilderModule(app) userevents = UserEventsBuilderModule(app)
superusers = SuperUserManager(app) superusers = SuperUserManager(app)
signer = Signer(app, OVERRIDE_CONFIG_DIRECTORY) signer = Signer(app, OVERRIDE_CONFIG_DIRECTORY)

View file

@ -27,16 +27,18 @@ def get_federated_service_name(authentication_type):
raise Exception('Unknown auth type: %s' % authentication_type) raise Exception('Unknown auth type: %s' % authentication_type)
LDAP_CERT_FILENAME = 'ldap.crt'
class UserAuthentication(object): class UserAuthentication(object):
def __init__(self, app=None, override_config_dir=None): def __init__(self, app=None, config_provider=None, override_config_dir=None):
self.app_secret_key = None self.app_secret_key = None
self.app = app self.app = app
if app is not None: if app is not None:
self.state = self.init_app(app, override_config_dir) self.state = self.init_app(app, config_provider, override_config_dir)
else: else:
self.state = None self.state = None
def init_app(self, app, override_config_dir): def init_app(self, app, config_provider, override_config_dir):
self.app_secret_key = app.config['SECRET_KEY'] self.app_secret_key = app.config['SECRET_KEY']
authentication_type = app.config.get('AUTHENTICATION_TYPE', 'Database') authentication_type = app.config.get('AUTHENTICATION_TYPE', 'Database')
@ -52,7 +54,15 @@ class UserAuthentication(object):
uid_attr = app.config.get('LDAP_UID_ATTR', 'uid') uid_attr = app.config.get('LDAP_UID_ATTR', 'uid')
email_attr = app.config.get('LDAP_EMAIL_ATTR', 'mail') email_attr = app.config.get('LDAP_EMAIL_ATTR', 'mail')
users = LDAPUsers(ldap_uri, base_dn, admin_dn, admin_passwd, user_rdn, uid_attr, email_attr) allow_tls_fallback = app.config.get('LDAP_ALLOW_INSECURE_FALLBACK', False)
tls_cert_path = None
if config_provider.volume_file_exists(LDAP_CERT_FILENAME):
with config_provider.get_volume_file(LDAP_CERT_FILENAME) as f:
tls_cert_path = f.name
users = LDAPUsers(ldap_uri, base_dn, admin_dn, admin_passwd, user_rdn, uid_attr, email_attr,
tls_cert_path, allow_tls_fallback)
elif authentication_type == 'JWT': elif authentication_type == 'JWT':
verify_url = app.config.get('JWT_VERIFY_ENDPOINT') verify_url = app.config.get('JWT_VERIFY_ENDPOINT')
issuer = app.config.get('JWT_AUTH_ISSUER') issuer = app.config.get('JWT_AUTH_ISSUER')

View file

@ -10,26 +10,41 @@ logger = logging.getLogger(__name__)
class LDAPConnectionBuilder(object): class LDAPConnectionBuilder(object):
def __init__(self, ldap_uri, user_dn, user_pw): def __init__(self, ldap_uri, user_dn, user_pw, tls_cert_path=None, allow_tls_fallback=False):
self._ldap_uri = ldap_uri self._ldap_uri = ldap_uri
self._user_dn = user_dn self._user_dn = user_dn
self._user_pw = user_pw self._user_pw = user_pw
self._tls_cert_path = tls_cert_path
self._allow_tls_fallback = allow_tls_fallback
def get_connection(self): def get_connection(self):
return LDAPConnection(self._ldap_uri, self._user_dn, self._user_pw) return LDAPConnection(self._ldap_uri, self._user_dn, self._user_pw,
self._tls_cert_path, self._allow_tls_fallback )
class LDAPConnection(object): class LDAPConnection(object):
def __init__(self, ldap_uri, user_dn, user_pw): def __init__(self, ldap_uri, user_dn, user_pw, tls_cert_path=None, allow_tls_fallback=False):
self._ldap_uri = ldap_uri self._ldap_uri = ldap_uri
self._user_dn = user_dn self._user_dn = user_dn
self._user_pw = user_pw self._user_pw = user_pw
self._tls_cert_path = tls_cert_path
self._allow_tls_fallback = allow_tls_fallback
self._conn = None self._conn = None
def __enter__(self): def __enter__(self):
trace_level = 2 if os.environ.get('USERS_DEBUG') == '1' else 0 trace_level = 2 if os.environ.get('USERS_DEBUG') == '1' else 0
self._conn = ldap.initialize(self._ldap_uri, trace_level=trace_level) self._conn = ldap.initialize(self._ldap_uri, trace_level=trace_level)
self._conn.set_option(ldap.OPT_REFERRALS, 1) self._conn.set_option(ldap.OPT_REFERRALS, 1)
if self._tls_cert_path is not None:
logger.debug('LDAP using custom TLS certificate path %s', self._tls_cert_path)
self._conn.set_option(ldap.OPT_X_TLS_CERTFILE, self._tls_cert_path)
if self._allow_tls_fallback:
logger.debug('TLS Fallback enabled in LDAP')
self._conn.set_option(ldap.OPT_X_TLS_TRY, 1)
self._conn.simple_bind_s(self._user_dn, self._user_pw) self._conn.simple_bind_s(self._user_dn, self._user_pw)
return self._conn return self._conn
@ -40,14 +55,20 @@ class LDAPConnection(object):
class LDAPUsers(FederatedUsers): class LDAPUsers(FederatedUsers):
_LDAPResult = namedtuple('LDAPResult', ['dn', 'attrs']) _LDAPResult = namedtuple('LDAPResult', ['dn', 'attrs'])
def __init__(self, ldap_uri, base_dn, admin_dn, admin_passwd, user_rdn, uid_attr, email_attr): def __init__(self, ldap_uri, base_dn, admin_dn, admin_passwd, user_rdn, uid_attr, email_attr,
tls_cert_path=None, allow_tls_fallback=False):
super(LDAPUsers, self).__init__('ldap') super(LDAPUsers, self).__init__('ldap')
self._ldap = LDAPConnectionBuilder(ldap_uri, admin_dn, admin_passwd) self._ldap = LDAPConnectionBuilder(ldap_uri, admin_dn, admin_passwd, tls_cert_path,
allow_tls_fallback)
self._ldap_uri = ldap_uri self._ldap_uri = ldap_uri
self._base_dn = base_dn self._base_dn = base_dn
self._user_rdn = user_rdn self._user_rdn = user_rdn
self._uid_attr = uid_attr self._uid_attr = uid_attr
self._email_attr = email_attr self._email_attr = email_attr
self._tls_cert_path = tls_cert_path
self._allow_tls_fallback = allow_tls_fallback
def _get_ldap_referral_dn(self, referral_exception): def _get_ldap_referral_dn(self, referral_exception):
logger.debug('Got referral: %s', referral_exception.args[0]) logger.debug('Got referral: %s', referral_exception.args[0])
@ -137,7 +158,8 @@ class LDAPUsers(FederatedUsers):
# First validate the password by binding as the user # First validate the password by binding as the user
try: try:
with LDAPConnection(self._ldap_uri, found_dn, password.encode('utf-8')): with LDAPConnection(self._ldap_uri, found_dn, password.encode('utf-8'),
self._tls_cert_path, self._allow_tls_fallback):
pass pass
except ldap.REFERRAL as re: except ldap.REFERRAL as re:
referral_dn = self._get_ldap_referral_dn(re) referral_dn = self._get_ldap_referral_dn(re)
@ -145,7 +167,8 @@ class LDAPUsers(FederatedUsers):
return (None, 'Invalid username') return (None, 'Invalid username')
try: try:
with LDAPConnection(self._ldap_uri, referral_dn, password.encode('utf-8')): with LDAPConnection(self._ldap_uri, referral_dn, password.encode('utf-8'),
self._tls_cert_path, self._allow_tls_fallback):
pass pass
except ldap.INVALID_CREDENTIALS: except ldap.INVALID_CREDENTIALS:
logger.exception('Invalid LDAP credentials') logger.exception('Invalid LDAP credentials')

View file

@ -625,6 +625,27 @@
</div> </div>
</td> </td>
</tr> </tr>
<tr ng-if="config.LDAP_URI.indexOf('ldaps://') == 0">
<td class="non-input">Custom TLS Certificate:</td>
<td>
<span class="config-file-field" filename="ldap.crt" has-file="hasfile.LDAPTLSCert"></span>
<div class="help-text">
If specified, the certificate (in PEM format) for the LDAP TLS connection.
</div
</td>
</tr>
<tr ng-if="config.LDAP_URI.indexOf('ldaps://') == 0">
<td class="non-input">Allow insecure:</td>
<td>
<div class="co-checkbox">
<input id="ldapfallbacktls" type="checkbox" ng-model="config.LDAP_ALLOW_INSECURE_FALLBACK.ORG_RESTRICT" class="ng-pristine ng-valid">
<label for="ldapfallbacktls">Allow fallback to non-TLS connections</label>
</div>
<div class="help-text">
If enabled, LDAP will fallback to <strong>insecure non-TLS connections</strong> if TLS does not succeed.
</div>
</td>
</tr>
</table> </table>
</div> </div>
</div> <!-- /Authentication --> </div> <!-- /Authentication -->

View file

@ -1,6 +1,4 @@
import redis import redis
import os
import json
import ldap import ldap
import peewee import peewee
import OpenSSL import OpenSSL
@ -8,13 +6,14 @@ import logging
from StringIO import StringIO from StringIO import StringIO
from fnmatch import fnmatch from fnmatch import fnmatch
from data.users import LDAP_CERT_FILENAME
from data.users.keystone import KeystoneUsers from data.users.keystone import KeystoneUsers
from data.users.externaljwt import ExternalJWTAuthN from data.users.externaljwt import ExternalJWTAuthN
from data.users.externalldap import LDAPConnection, LDAPUsers from data.users.externalldap import LDAPConnection, LDAPUsers
from flask import Flask from flask import Flask
from flask.ext.mail import Mail, Message from flask.ext.mail import Mail, Message
from data.database import validate_database_url, User from data.database import validate_database_url
from storage import get_storage_driver from storage import get_storage_driver
from auth.auth_context import get_authenticated_user from auth.auth_context import get_authenticated_user
from util.config.oauth import GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuthConfig from util.config.oauth import GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuthConfig
@ -30,8 +29,10 @@ SSL_FILENAMES = ['ssl.cert', 'ssl.key']
DB_SSL_FILENAMES = ['database.pem'] DB_SSL_FILENAMES = ['database.pem']
JWT_FILENAMES = ['jwt-authn.cert'] JWT_FILENAMES = ['jwt-authn.cert']
ACI_CERT_FILENAMES = ['signing-public.gpg', 'signing-private.gpg'] ACI_CERT_FILENAMES = ['signing-public.gpg', 'signing-private.gpg']
LDAP_FILENAMES = [LDAP_CERT_FILENAME]
CONFIG_FILENAMES = (SSL_FILENAMES + DB_SSL_FILENAMES + JWT_FILENAMES + ACI_CERT_FILENAMES +
LDAP_FILENAMES)
CONFIG_FILENAMES = SSL_FILENAMES + DB_SSL_FILENAMES + JWT_FILENAMES + ACI_CERT_FILENAMES
def get_storage_providers(config): def get_storage_providers(config):
storage_config = config.get('DISTRIBUTED_STORAGE_CONFIG', {}) storage_config = config.get('DISTRIBUTED_STORAGE_CONFIG', {})
@ -323,8 +324,15 @@ def _validate_ldap(config, password):
if not ldap_uri.startswith('ldap://') and not ldap_uri.startswith('ldaps://'): if not ldap_uri.startswith('ldap://') and not ldap_uri.startswith('ldaps://'):
raise Exception('LDAP URI must start with ldap:// or ldaps://') raise Exception('LDAP URI must start with ldap:// or ldaps://')
tls_cert_path = None
if config_provider.volume_file_exists(LDAP_CERT_FILENAME):
with config_provider.get_volume_file(LDAP_CERT_FILENAME) as f:
tls_cert_path = f.name
allow_tls_fallback = config.get('LDAP_ALLOW_INSECURE_FALLBACK', False)
try: try:
with LDAPConnection(ldap_uri, admin_dn, admin_passwd): with LDAPConnection(ldap_uri, admin_dn, admin_passwd, tls_cert_path, allow_tls_fallback):
pass pass
except ldap.LDAPError as ex: except ldap.LDAPError as ex:
values = ex.args[0] if ex.args else {} values = ex.args[0] if ex.args else {}
@ -339,7 +347,8 @@ def _validate_ldap(config, password):
uid_attr = config.get('LDAP_UID_ATTR', 'uid') uid_attr = config.get('LDAP_UID_ATTR', 'uid')
email_attr = config.get('LDAP_EMAIL_ATTR', 'mail') email_attr = config.get('LDAP_EMAIL_ATTR', 'mail')
users = LDAPUsers(ldap_uri, base_dn, admin_dn, admin_passwd, user_rdn, uid_attr, email_attr) users = LDAPUsers(ldap_uri, base_dn, admin_dn, admin_passwd, user_rdn, uid_attr, email_attr,
tls_cert_path, allow_tls_fallback)
username = get_authenticated_user().username username = get_authenticated_user().username
(result, err_msg) = users.verify_credentials(username, password) (result, err_msg) = users.verify_credentials(username, password)