diff --git a/.env b/.env new file mode 100644 index 0000000..e9d5533 --- /dev/null +++ b/.env @@ -0,0 +1,58 @@ +######################################### +## GENERAL +######################################### + +HOSTNAME=localhost +# Adapt to your production domain and use a reverse proxy +#HOSTNAME=taiga.company.com + +TAIGA_DB_PWD=somethingverysecure,changethis! +PORT=8080 + +######################################### +## SECURITY +######################################### + +TAIGA_SECRET=somethingevenmoresecure!!really,changethis! + +TAIGA_ADMIN_PASSWORD=betterthan123123 + +######################################### +## EMAIL +######################################### + +TAIGA_EMAIL_DOMAIN=company.com +TAIGA_SMTP_USER= +TAIGA_SMTP_PWD= + + +######################################### +## LDAP +######################################### + +TAIGA_ENABLE_LDAP=True +# Adapt to your base domain name +# If you need only one domain, leave another domains emply +TAIGA_LDAP_DOMAIN_1=openldap +TAIGA_LDAP_DOMAIN_2=openldap +TAIGA_LDAP_DOMAIN_3=openldap + +TAIGA_LDAP_BASE_DN=dc=openldap +TAIGA_LDAP_COMPANY=taiga + +TAIGA_LDAP_BIND_DN=cn=admin,dc=openldap +TAIGA_LDAP_BIND_PASSWORD=password +LDAP_TLS=false +LDAP_REMOVE_CONFIG_AFTER_SETUP=False + + + +TAIGA_LDAP_LOG_LEVEL=trace + + +######################################### +## EVENTS +######################################### + +TAIGA_RABBIT_USER=taiga +TAIGA_RABBIT_PASSWORD=somethingverysecure diff --git a/.gitignore b/.gitignore index b608332..eaf3400 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ __pycache__ *.eggs *.egg-info dist +.idea +ldif/*.log \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3cb1abb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +#TODO: How to give ability to another version and variant? +FROM monogramm/docker-taiga-back-base:4.2-alpine +LABEL maintainer="Monogramm maintainers " + +# Taiga additional properties +ENV TAIGA_ENABLE_LDAP=False \ + TAIGA_LDAP_USE_TLS=True \ + TAIGA_LDAP_SERVER= \ + TAIGA_LDAP_PORT=389 \ + TAIGA_LDAP_BIND_DN= \ + TAIGA_LDAP_BIND_PASSWORD= \ + TAIGA_LDAP_BASE_DN= \ + TAIGA_LDAP_USERNAME_ATTRIBUTE=uid \ + TAIGA_LDAP_EMAIL_ATTRIBUTE=mail \ + TAIGA_LDAP_FULL_NAME_ATTRIBUTE=cn \ + TAIGA_LDAP_SAVE_LOGIN_PASSWORD=True \ + TAIGA_LDAP_FALLBACK=normal + +# Backend healthcheck +HEALTHCHECK CMD curl --fail http://127.0.0.1:8001/api/v1/ || exit 1 + +# Erase original entrypoint and conf with custom one +COPY local.py /taiga/ +COPY entrypoint.sh ./ + +COPY . /usr/src/taiga-contrib-ldap-auth-ext + +ARG BUILD_DATE + +# COPY dist/taiga_contrib_ldap_auth_ext-0.4.4-py3-none-any.whl /usr/src/taiga-contrib-ldap-auth-ext/dist/ +# Fix entrypoint permissions +# Install LDAP extension +RUN set -ex; \ + chmod 755 /entrypoint.sh; \ + cd /usr/src/taiga-contrib-ldap-auth-ext/; \ + python setup.py bdist_wheel; \ + LC_ALL=C pip install --no-cache-dir dist/taiga_contrib_ldap_auth_ext-0.4.4-py3-none-any.whl; + #rm -r /usr/local/lib/python3.6/site-packages/taiga_contrib_ldap_auth_ext diff --git a/README.md b/README.md index 745589e..a8a7e61 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Add the following to `settings/local.py`: INSTALLED_APPS += ["taiga_contrib_ldap_auth_ext"] # TODO https://github.com/Monogramm/taiga-contrib-ldap-auth-ext/issues/16 -LDAP_SERVER = 'ldap://ldap.example.com' +LDAP_SERVER = ['ldap://ldap.example1.com','ldap://ldap.example2.com'] LDAP_PORT = 389 # Flag to enable LDAP with STARTTLS before bind diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..917bb23 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,209 @@ +version: '2' + +services: + taigadb: + image: postgres:10-alpine + container_name: taigadb + #restart: always + ports: + - 5432:5432 + volumes: + - /srv/taiga/db/data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=taiga + - POSTGRES_USER=taiga + - POSTGRES_PASSWORD=${TAIGA_DB_PWD} + + taiga_back: + # For CI or local modifications + build: + context: . + dockerfile: Dockerfile + image: docker-taiga-back-ldap:test + hostname: ${HOSTNAME} + container_name: taiga_back + #restart: always + depends_on: + - taigadb + ports: + - 8001:8001 + volumes: + # Media and uploads directory. Required (or you will lose all uploads) + - /srv/taiga/back/media:/usr/src/taiga-back/media + - /srv/taiga/back/static:/usr/src/taiga-back/static + # Taiga configuration directory. Makes it easier to change configuration with your own + #- /srv/taiga/back/conf:/taiga + - /usr/local/lib/python3.6/site-packages/taiga_contrib_ldap_auth_ext + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro + environment: + # Your hostname (REQUIRED) + - TAIGA_HOSTNAME=${HOSTNAME}:${PORT} + #- TAIGA_SSL=False + #- TAIGA_SSL_BY_REVERSE_PROXY=True + # Secret key for cryptographic signing + - TAIGA_SECRET_KEY=${TAIGA_SECRET} + # Admin account default password + - TAIGA_ADMIN_PASSWORD=${TAIGA_ADMIN_PASSWORD} + # Database settings + - TAIGA_DB_HOST=taigadb + - TAIGA_DB_NAME=taiga + - TAIGA_DB_USER=taiga + - TAIGA_DB_PASSWORD=${TAIGA_DB_PWD} + # when the db comes up from docker, it is usually too quick + - TAIGA_SLEEP=5 + # To use an external SMTP for emails, fill in these values: + - TAIGA_ENABLE_EMAIL=True + - TAIGA_EMAIL_FROM=taiga@${TAIGA_EMAIL_DOMAIN} + - TAIGA_EMAIL_USE_TLS=False + - TAIGA_EMAIL_HOST=smtp.${TAIGA_EMAIL_DOMAIN} + - TAIGA_EMAIL_PORT=587 + - TAIGA_EMAIL_USER=${TAIGA_SMTP_USER} + - TAIGA_EMAIL_PASS=${TAIGA_SMTP_PWD} + # Backend settings + - TAIGA_DEBUG=False + - TAIGA_PUBLIC_REGISTER_ENABLED=False + - TAIGA_FEEDBACK_ENABLED=True + - TAIGA_FEEDBACK_EMAIL=taiga@${TAIGA_EMAIL_DOMAIN} + # Events settings + - TAIGA_EVENTS_ENABLED=True + - RABBIT_USER=${TAIGA_RABBIT_USER} + - RABBIT_PASSWORD=${TAIGA_RABBIT_PASSWORD} + - RABBIT_VHOST='/' + - RABBIT_HOST=taiga_rabbit + - RABBIT_PORT=5672 + # Async settings + # To enable async mode, uncomment the following lines: + #- TAIGA_ASYNC_ENABLED=True + #- REDIS_HOST=taiga_redis + #- REDIS_PORT=6379 + ### Additional parameters + # LDAP Settings + - TAIGA_ENABLE_LDAP=${TAIGA_ENABLE_LDAP} + - TAIGA_LDAP_USE_TLS=false + - TAIGA_LDAP_SERVER=ldap://${TAIGA_LDAP_DOMAIN_1},ldap://${TAIGA_LDAP_DOMAIN_2},ldap://${TAIGA_LDAP_DOMAIN_3} + - TAIGA_LDAP_PORT=389 + - TAIGA_LDAP_BIND_DN=${TAIGA_LDAP_BIND_DN} + - TAIGA_LDAP_BIND_PASSWORD=${TAIGA_LDAP_BIND_PASSWORD} + - TAIGA_LDAP_BASE_DN=ou=People,${TAIGA_LDAP_BASE_DN} + - TAIGA_LDAP_USERNAME_ATTRIBUTE=uid + - TAIGA_LDAP_EMAIL_ATTRIBUTE=mail + - TAIGA_LDAP_FULL_NAME_ATTRIBUTE=displayName + - TAIGA_LDAP_FALLBACK=normal + + taiga_front: + # For CI or local modifications + #build: ./front + # For production + image: monogramm/docker-taiga-front:4.2-alpine + hostname: ${HOSTNAME} + container_name: taiga_front + #restart: always + depends_on: + - taiga_back + # To disable taiga-events, comment the following lines: + - taiga_events + - taiga_rabbit + # To enable async mode, uncomment the following lines: + #- taiga_redis + ports: + # If using SSL, uncomment 443 and comment out 80 + - ${PORT}:80 + #- ${PORT}:443 + volumes: + # Media and uploads directory. Required for NGinx + - /srv/taiga/back/media:/usr/src/taiga-back/media:ro + - /srv/taiga/back/static:/usr/src/taiga-back/static:ro + # Taiga configuration directory. Makes it easier to change configuration with your own + #- /srv/taiga/front/conf:/taiga + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro + environment: + # Your hostname (REQUIRED) + - TAIGA_HOSTNAME=${HOSTNAME}:${PORT} + #- TAIGA_SSL=True + #- TAIGA_SSL_BY_REVERSE_PROXY=True + #- TAIGA_BACKEND_SSL=True + # Frontend settings + - TAIGA_DEBUG=true + - TAIGA_DEBUG_INFO=true + - TAIGA_DEFAULT_LANGUAGE=en + - TAIGA_THEMES=taiga material-design high-contrast + - TAIGA_DEFAULT_THEME=taiga + - TAIGA_PUBLIC_REGISTER_ENABLED=false + - TAIGA_FEEDBACK_ENABLED=true + - TAIGA_SUPPORT_URL=https://tree.taiga.io/support + - TAIGA_PRIVACY_POLICY_URL= + - TAIGA_LOGIN_FORM_TYPE=ldap + # Backend settings + - TAIGA_BACK_HOST=taiga_back + - TAIGA_BACK_PORT=8001 + # Events settings + - TAIGA_EVENTS_ENABLED=True + - TAIGA_EVENTS_HOST=taiga_events + - TAIGA_EVENTS_PORT=8888 + + # To disable taiga-events, comment all the following lines: + taiga_rabbit: + image: rabbitmq:3-alpine + hostname: taiga_rabbit + container_name: taiga_rabbit + #restart: always + ports: + - 5672:5672 + environment: + - RABBITMQ_DEFAULT_USER=${TAIGA_RABBIT_USER} + - RABBITMQ_DEFAULT_PASS=${TAIGA_RABBIT_PASSWORD} + + taiga_events: + image: monogramm/docker-taiga-events:alpine + container_name: taiga_events + #restart: always + links: + - taiga_rabbit + ports: + - 8888:8888 + environment: + - RABBIT_USER=${TAIGA_RABBIT_USER} + - RABBIT_PASSWORD=${TAIGA_RABBIT_PASSWORD} + - RABBIT_VHOST='/' + - RABBIT_HOST=taiga_rabbit + - RABBIT_PORT=5672 + - TAIGA_EVENTS_SECRET=${TAIGA_SECRET} + - TAIGA_EVENTS_PORT=8888 + + taiga_openldap: + image: osixia/openldap:latest + container_name: taiga_openldap + command: --copy-service + ports: + - "389:389" + - "636:636" + environment: + LDAP_LOG_LEVEL: ${TAIGA_LDAP_LOG_LEVEL} + LDAP_ORGANISATION: ${TAIGA_LDAP_COMPANY} + LDAP_BASE_DN: ${TAIGA_LDAP_BASE_DN} + LDAP_ADMIN_PASSWORD: ${TAIGA_LDAP_BIND_PASSWORD} + LDAP_DOMAIN: ${TAIGA_LDAP_DOMAIN_1} + LDAP_TLS: ${LDAP_TLS} + LDAP_REMOVE_CONFIG_AFTER_SETUP : "false" + HOSTMANE: "openldap" + volumes: + - ./ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom + restart: on-failure + + taiga_dev_mailer: + image: sj26/mailcatcher:latest + hostname: taiga_dev_mailer + container_name: taiga_dev_mailer + restart: always + expose: + - 1025 + ports: + - 1080:1080 + volumes: + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro + +networks: + default: \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..1373077 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,77 @@ +set -e + +log() { + echo "[$(date +%Y-%m-%dT%H:%M:%S%:z)] $@" +} + +# Sleep when asked to, to allow the database time to start +# before Taiga tries to run /checkdb.py below. +: ${TAIGA_SLEEP:=0} +sleep $TAIGA_SLEEP + +# Setup and check database automatically if needed +if [ -z "$TAIGA_SKIP_DB_CHECK" ]; then + log "Running database check" + set +e + python /checkdb.py + DB_CHECK_STATUS=$? + set -e + + if [ $DB_CHECK_STATUS -eq 1 ]; then + log "Failed to connect to database server or database does not exist." + exit 1 + fi + + # Database migration check should be done in all startup in case of backend upgrade + log "Execute database migrations..." + python manage.py migrate --noinput + + if [ $DB_CHECK_STATUS -eq 2 ]; then + log "Configuring initial user" + python manage.py loaddata initial_user + log "Configuring initial project templates" + python manage.py loaddata initial_project_templates + + # shellcheck disable=SC2070 + if [ -n $TAIGA_ADMIN_PASSWORD ]; then + log "Changing initial admin password" + python manage.py shell < /changeadminpasswd.py + fi + fi + + # TODO This works... but requires to persist the backend to keep track of already executed migrations + # BREAKING CHANGES INCOMING + #if python manage.py migrate --noinput | grep 'Your models have changes that are not yet reflected in a migration'; then + # log "Generate database migrations..." + # python manage.py makemigrations + # log "Execute database migrations..." + # python manage.py migrate --noinput + #fi + +fi + +# In case of frontend upgrade, locales and statics should be regenerated +log "Compiling messages and collecting static" +python manage.py compilemessages > /dev/null +python manage.py collectstatic --noinput > /dev/null + +log "Start gunicorn server" +GUNICORN_TIMEOUT="${GUINCORN_TIMEOUT:-60}" +GUNICORN_WORKERS="${GUNICORN_WORKERS:-4}" +GUNICORN_LOGLEVEL="${GUNICORN_LOGLEVEL:-debug}" + +GUNICORN_ARGS=". -t ${GUNICORN_TIMEOUT} --workers ${GUNICORN_WORKERS} --bind ${BIND_ADDRESS}:${PORT} --log-level ${GUNICORN_LOGLEVEL} --reload" + +if [ -n "${GUNICORN_CERTFILE}" ]; then + GUNICORN_ARGS="${GUNICORN_ARGS} --certfile=${GUNICORN_CERTFILE}" +fi + +if [ -n "${GUNICORN_KEYFILE}" ]; then + GUNICORN_ARGS="${GUNICORN_ARGS} --keyfile=${GUNICORN_KEYFILE}" +fi + +if [ "$1" == "gunicorn" ]; then + exec "$@" "$GUNICORN_ARGS" +else + exec "$@" +fi \ No newline at end of file diff --git a/ldif/00_base.ldif b/ldif/00_base.ldif new file mode 100644 index 0000000..70ae911 --- /dev/null +++ b/ldif/00_base.ldif @@ -0,0 +1,22 @@ +# Create an initial structure for LDAP +version: 1 + +# Services base +dn: ou=Services,dc=openldap +objectClass: top +objectClass: organizationalUnit +ou: Services + + +# Groups base +dn: ou=Groups,dc=openldap +objectClass: top +objectClass: organizationalUnit +ou: Groups + + +# Users base +dn: ou=People,dc=openldap +objectClass: top +objectClass: organizationalUnit +ou: People \ No newline at end of file diff --git a/ldif/01_user.ldif b/ldif/01_user.ldif new file mode 100644 index 0000000..d9ee2f4 --- /dev/null +++ b/ldif/01_user.ldif @@ -0,0 +1,10 @@ +version: 1 + +dn: uid=test_user2,ou=People,dc=openldap +objectClass: mailAccount +objectClass: uidObject +objectClass: organizationalUnit +objectClass: top +mail: test_user2 +ou: People +uid: test_user2 \ No newline at end of file diff --git a/local.py b/local.py new file mode 100644 index 0000000..14e4db0 --- /dev/null +++ b/local.py @@ -0,0 +1,79 @@ +# If you want to modify this file, I recommend check out docker-taiga-example +# https://github.com/benhutchins/docker-taiga-example +# +# Please modify this file as needed, see the local.py.example for details: +# https://github.com/taigaio/taiga-back/blob/master/settings/local.py.example +# +# Importing docker provides common settings, see: +# https://github.com/benhutchins/docker-taiga/blob/master/docker-settings.py +# https://github.com/taigaio/taiga-back/blob/master/settings/common.py + +from .docker import * +from ldap3 import Tls +import ssl + +######################################### +## LDAP +######################################### + +if os.getenv('TAIGA_ENABLE_LDAP').lower() == 'true': + # mailcatcher configuration + EMAIL_HOST = os.getenv('TAIGA_EMAIL_HOST') + EMAIL_HOST_USER = os.getenv('TAIGA_SMTP_USER') + EMAIL_HOST_PASSWORD = os.getenv('TAIGA_EMAIL_PASS') + EMAIL_PORT = 1025 + EMAIL_USE_TLS = False + + + # see https://github.com/Monogramm/taiga-contrib-ldap-auth-ext + print("Taiga contrib LDAP Auth Ext enabled", file=sys.stderr) + INSTALLED_APPS += ["taiga_contrib_ldap_auth_ext"] + + if os.getenv('TAIGA_LDAP_USE_TLS').lower() == 'true': + # Flag to enable LDAP with STARTTLS before bind + LDAP_START_TLS = True + LDAP_TLS_CERTS = Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1, ciphers='RSA+3DES') + else: + LDAP_START_TLS = False + + LDAP_SERVER = os.getenv('TAIGA_LDAP_SERVER') + LDAP_PORT = int(os.getenv('TAIGA_LDAP_PORT')) + + # Full DN of the service account use to connect to LDAP server and search for login user's account entry + # If LDAP_BIND_DN is not specified, or is blank, then an anonymous bind is attempated + LDAP_BIND_DN = os.getenv('TAIGA_LDAP_BIND_DN') + LDAP_BIND_PASSWORD = os.getenv('TAIGA_LDAP_BIND_PASSWORD') + + # Starting point within LDAP structure to search for login user + # Something like 'ou=People,dc=company,dc=com' + LDAP_SEARCH_BASE = os.getenv('TAIGA_LDAP_BASE_DN') + + # Additional search criteria to the filter (will be ANDed) + # LDAP_SEARCH_FILTER_ADDITIONAL = '(mail=*)' + + # Names of attributes to get username, e-mail and full name values from + # These fields need to have a value in LDAP + LDAP_USERNAME_ATTRIBUTE = os.getenv('TAIGA_LDAP_USERNAME_ATTRIBUTE') + LDAP_EMAIL_ATTRIBUTE = os.getenv('TAIGA_LDAP_EMAIL_ATTRIBUTE') + LDAP_FULL_NAME_ATTRIBUTE = os.getenv('TAIGA_LDAP_FULL_NAME_ATTRIBUTE') + + # Option to not store the passwords in the local db + if os.getenv('TAIGA_LDAP_SAVE_LOGIN_PASSWORD').lower() == 'false': + LDAP_SAVE_LOGIN_PASSWORD = False + + # Fallback on normal authentication method if this LDAP auth fails. Uncomment to enable. + LDAP_FALLBACK = os.getenv('TAIGA_LDAP_FALLBACK') + + # Function to map LDAP username to local DB user unique identifier. + # Upon successful LDAP bind, will override returned username attribute + # value. May result in unexpected failures if changed after the database + # has been populated. + def _ldap_slugify(uid: str) -> str: + # example: force lower-case + uid = uid.lower() + return uid + + LDAP_MAP_USERNAME_TO_UID = _ldap_slugify + + # For additional configuration options, look at: + # https://github.com/taigaio/taiga-back/blob/master/settings/local.py.example \ No newline at end of file diff --git a/taiga_contrib_ldap_auth_ext/connector.py b/taiga_contrib_ldap_auth_ext/connector.py index 9f53ce6..6d92246 100644 --- a/taiga_contrib_ldap_auth_ext/connector.py +++ b/taiga_contrib_ldap_auth_ext/connector.py @@ -11,7 +11,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from ldap3 import Server, Connection, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND, ANONYMOUS, SIMPLE, SYNC, SUBTREE, NONE +from ldap3 import Server, Connection, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND, ANONYMOUS, SIMPLE, SYNC, SUBTREE, \ + NONE from django.conf import settings from taiga.base.connectors.exceptions import ConnectorBaseException @@ -47,35 +48,83 @@ class LDAPUserLoginError(LDAPError): START_TLS = getattr(settings, "LDAP_START_TLS", False) +def find_data_from_ldap_server(connection, search_filter): + if connection is None: + return + + try: + print(SEARCH_BASE) + print(search_filter) + connection.search(search_base=SEARCH_BASE, + search_filter=search_filter, + search_scope=SUBTREE, + attributes=[USERNAME_ATTRIBUTE, + EMAIL_ATTRIBUTE, FULL_NAME_ATTRIBUTE], + paged_size=5) + print(connection.response) + except Exception as e: + error_message = "LDAP login incorrect: %s" % e + print(error_message, flush=True) + raise LDAPUserLoginError({"error_message": error_message}) + # we are only interested in user objects in the response + connection.response = [r for r in connection.response if 'raw_attributes' in r and 'dn' in r] + # stop if no search results + if not connection.response: + raise LDAPUserLoginError({"error_message": "LDAP login not found"}) + + # handle multiple matches + if len(connection.response) > 1: + raise LDAPUserLoginError( + {"error_message": "LDAP login could not be determined."}) + + # handle missing mandatory attributes + raw_attributes = connection.response[0].get('raw_attributes') + if raw_attributes.get(USERNAME_ATTRIBUTE) or raw_attributes.get(EMAIL_ATTRIBUTE) or raw_attributes.get( + FULL_NAME_ATTRIBUTE): + raise LDAPUserLoginError({"error_message": "LDAP login is invalid."}) + + # attempt LDAP bind + username = raw_attributes.get(USERNAME_ATTRIBUTE)[0].decode('utf-8') + email = raw_attributes.get(EMAIL_ATTRIBUTE)[0].decode('utf-8') + full_name = raw_attributes.get(FULL_NAME_ATTRIBUTE)[0].decode('utf-8') + # LDAP binding successful, but some values might have changed, or + # this is the user's first login, so return them + return username, email, full_name + + def login(username: str, password: str) -> tuple: """ Connect to LDAP server, perform a search and attempt a bind. - Can raise `exc.LDAPConnectionError` exceptions if the - connection to LDAP fails. - Can raise `exc.LDAPUserLoginError` exceptions if the login to LDAP fails. - + :returns: tuple (username, email, full_name) - """ - tls = None if TLS_CERTS: tls = TLS_CERTS - # connect to the LDAP server - if SERVER.lower().startswith("ldaps://"): - use_ssl = True - else: - use_ssl = False - try: - server = Server(SERVER, port=PORT, get_info=NONE, - use_ssl=use_ssl, tls=tls) - except Exception as e: - error = "Error connecting to LDAP server: %s" % e - raise LDAPConnectionError({"error_message": error}) + + + # Dict with server key and connection value + server_ldap_list = [] + # Connect to the LDAP servers + servers_list = SERVER.split(',') + for server_address in servers_list: + if len(server_address) <= 10: + continue + if server_address.lower().startswith("ldaps://"): + use_ssl = True + else: + use_ssl = False + try: + server = Server(host=server_address, port=int(PORT), get_info=NONE, + use_ssl=use_ssl, tls=tls) + server_ldap_list.append(server) + except Exception as e: + error = "Error connecting to LDAP server: %s" % e + raise LDAPConnectionError({"error_message": error}) # authenticate as service if credentials provided, anonymously otherwise if BIND_DN is not None and BIND_DN != '': @@ -91,58 +140,22 @@ def login(username: str, password: str) -> tuple: if START_TLS: auto_bind = AUTO_BIND_TLS_BEFORE_BIND - try: - c = Connection(server, auto_bind=auto_bind, client_strategy=SYNC, check_names=True, - user=service_user, password=service_pass, authentication=service_auth) - except Exception as e: - error = "Error connecting to LDAP server: %s" % e - raise LDAPConnectionError({"error_message": error}) - # search for user-provided login search_filter = '(|(%s=%s)(%s=%s))' % ( USERNAME_ATTRIBUTE, username, EMAIL_ATTRIBUTE, username) if SEARCH_FILTER_ADDITIONAL: search_filter = '(&%s%s)' % (search_filter, SEARCH_FILTER_ADDITIONAL) - try: - c.search(search_base=SEARCH_BASE, - search_filter=search_filter, - search_scope=SUBTREE, - attributes=[USERNAME_ATTRIBUTE, - EMAIL_ATTRIBUTE, FULL_NAME_ATTRIBUTE], - paged_size=5) - except Exception as e: - error = "LDAP login incorrect: %s" % e - raise LDAPUserLoginError({"error_message": error}) - - # we are only interested in user objects in the response - c.response = [r for r in c.response if 'raw_attributes' in r and 'dn' in r] - # stop if no search results - if not c.response: - raise LDAPUserLoginError({"error_message": "LDAP login not found"}) - - # handle multiple matches - if len(c.response) > 1: - raise LDAPUserLoginError( - {"error_message": "LDAP login could not be determined."}) - - # handle missing mandatory attributes - raw_attributes = c.response[0].get('raw_attributes') - if raw_attributes.get(USERNAME_ATTRIBUTE) or raw_attributes.get(EMAIL_ATTRIBUTE) or raw_attributes.get(FULL_NAME_ATTRIBUTE): - raise LDAPUserLoginError({"error_message": "LDAP login is invalid."}) - - # attempt LDAP bind - username = raw_attributes.get(USERNAME_ATTRIBUTE)[0].decode('utf-8') - email = raw_attributes.get(EMAIL_ATTRIBUTE)[0].decode('utf-8') - full_name = raw_attributes.get(FULL_NAME_ATTRIBUTE)[0].decode('utf-8') - try: - dn = str(bytes(c.response[0].get('dn'), 'utf-8'), encoding='utf-8') - Connection(server, auto_bind=auto_bind, client_strategy=SYNC, - check_names=True, authentication=SIMPLE, - user=dn, password=password) - except Exception as e: - error = "LDAP bind failed: %s" % e - raise LDAPUserLoginError({"error_message": error}) - # LDAP binding successful, but some values might have changed, or - # this is the user's first login, so return them - return (username, email, full_name) + for server in server_ldap_list: + try: + c = Connection(server, auto_bind=auto_bind, client_strategy=SYNC, check_names=True, + user=service_user, password=service_pass, authentication=service_auth) + data = find_data_from_ldap_server(c, search_filter) + print(data) + except Exception as e: + print("Error: {0}".format(e), flush=True) + print("Failed to authenticate against LDAP {0}".format(server.name), flush=True) + continue + if data is not None: + break + raise LDAPUserLoginError({"error_message": "Unable to authenticate user to LDAP server"})