Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatic creation of users with ldap authentication #227

Merged
merged 14 commits into from
Apr 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 2.0.1
current_version = 2.0.2
commit = True
tag = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-(?P<release>.*)-(?P<build>\d+))?
Expand Down
11 changes: 11 additions & 0 deletions .env.model
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,25 @@ IRIS_AUTHENTICATION_TYPE=local
#IRIS_ADM_API_KEY=B8BA5D730210B50F41C06941582D7965D57319D5685440587F98DFDC45A01594
#IRIS_ADM_EMAIL=admin@localhost
#IRIS_ADM_USERNAME=administrator
# requests the just-in-time creation of users with ldap authentification (see https://github.com/dfir-iris/iris-web/issues/203)
#IRIS_AUTHENTICATION_CREATE_USER_IF_NOT_EXIST=True

# -- FOR LDAP AUTHENTICATION
#IRIS_AUTHENTICATION_TYPE=ldap
#LDAP_SERVER=127.0.0.1
#LDAP_AUTHENTICATION_TYPE=SIMPLE
#LDAP_PORT=3890
#LDAP_USER_PREFIX=uid=
#LDAP_USER_SUFFIX=ou=people,dc=example,dc=com
#LDAP_USE_SSL=False
# base DN in which to search for users
#LDAP_SEARCH_DN=ou=users,dc=example,dc=org
# unique identifier to search the user
#LDAP_ATTRIBUTE_IDENTIFIER=cn
# name of the attribute to retrieve the user's display name
#LDAP_ATTRIBUTE_DISPLAY_NAME=displayName
# name of the attribute to retrieve the user's email address
#LDAP_ATTRIBUTE_MAIL=mail
#LDAP_VALIDATE_CERTIFICATE=True
#LDAP_TLS_VERSION=1.2
#LDAP_SERVER_CERTIFICATE=
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<p align="center">
Incident Response Investigation System
<br>
<i>Current Version v2.0.1</i>
<i>Current Version v2.0.2</i>
<br>
<a href="https://v200.beta.dfir-iris.org">Online Demonstration</a>
</p>
Expand Down Expand Up @@ -52,7 +52,7 @@ git clone https://github.com/dfir-iris/iris-web.git
cd iris-web

# Checkout to the last tagged version
git checkout v2.0.1
git checkout v2.0.2

# Copy the environment file
cp .env.model .env
Expand Down
8 changes: 4 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ services:
build:
context: docker/db
container_name: iriswebapp_db
image: iriswebapp_db:v2.0.1
image: iriswebapp_db:v2.0.2
restart: always
# Used for debugging purposes, should be deleted for production
ports:
Expand All @@ -48,7 +48,7 @@ services:
build:
context: .
dockerfile: docker/webApp/Dockerfile
image: iriswebapp_app:v2.0.1
image: iriswebapp_app:v2.0.2
container_name: iriswebapp_app
command: ['nohup', './iris-entrypoint.sh', 'iriswebapp']
volumes:
Expand Down Expand Up @@ -85,7 +85,7 @@ services:
build:
context: .
dockerfile: docker/webApp/Dockerfile
image: iriswebapp_app:v2.0.1
image: iriswebapp_app:v2.0.2
container_name: iriswebapp_worker
command: ['./wait-for-iriswebapp.sh', 'app:8000', './iris-entrypoint.sh', 'iris-worker']
volumes:
Expand Down Expand Up @@ -120,7 +120,7 @@ services:
args:
NGINX_CONF_GID: 1234
NGINX_CONF_FILE: nginx.conf
image: iriswebapp_nginx:v2.0.1
image: iriswebapp_nginx:v2.0.2
container_name: iriswebapp_nginx
environment:
- IRIS_UPSTREAM_SERVER
Expand Down
98 changes: 55 additions & 43 deletions source/app/blueprints/login/login_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from app.models.cases import Cases
from app.models.authorization import User
from app.util import is_authentication_ldap
from app.datamgmt.manage.manage_users_db import get_active_user_by_login


login_blueprint = Blueprint(
Expand All @@ -51,6 +52,50 @@
log = app.logger


# filter User out of database through username
def _retrieve_user_by_username(username):
user = get_active_user_by_login(username)
if not user:
track_activity("someone tried to log in with user '{}', which does not exist".format(username),
ctx_less=True, display_in_ui=False)
return user


def _render_template_login(form, msg):
organisation_name = app.config.get('ORGANISATION_NAME')
return render_template('login.html', form=form, msg=msg, organisation_name=organisation_name)


def _authenticate_ldap(form, username, password):
try:
if ldap_authenticate(username, password) is False:
track_activity("wrong login password for user '{}' using LDAP auth".format(username),
ctx_less=True, display_in_ui=False)
return _render_template_login(form, 'Wrong credentials. Please try again.')

user = _retrieve_user_by_username(username)
if not user:
return _render_template(form, 'Wrong credentials. Please try again.')

return wrap_login_user(user)
except Exception as e:
log.error(e.__str__())
return _render_template_login(form, 'LDAP authentication unavailable. Check server logs')


def _authenticate_password(form, username, password):
user = _retrieve_user_by_username(username)
if not user:
return _render_template(form, 'Wrong credentials. Please try again.')

if bc.check_password_hash(user.password, password):
return wrap_login_user(user)

track_activity("wrong login password for user '{}' using local auth".format(username), ctx_less=True,
display_in_ui=False)
return _render_template(form, 'Wrong credentials. Please try again.')


# CONTENT ------------------------------------------------
# Authenticate user
if app.config.get("AUTHENTICATION_TYPE") in ["local", "ldap"]:
Expand All @@ -62,52 +107,19 @@ def login():
return redirect(url_for('index.index'))

form = LoginForm(request.form)
msg = None

if form.validate_on_submit():
username = form.username.data
password = form.password.data

user = User.query.filter(
User.user == username,
User.active.is_(True),
User.is_service_account.is_(False)
).first()

if user:
if is_authentication_ldap() is True:
try:
if ldap_authenticate(username, password) is True:
return wrap_login_user(user)

else:
track_activity("wrong login password for user '{}' using LDAP auth".format(username),
ctx_less=True, display_in_ui=False)

msg = "Wrong credentials. Please try again."
return render_template('login.html', form=form, msg=msg)

except Exception as e:
log.error(e.__str__())
return render_template('login.html', form=form,
msg='LDAP authentication unavailable. Check server logs')

elif bc.check_password_hash(user.password, password):
return wrap_login_user(user)
# check if both http method is POST and form is valid on submit
if not form.validate_on_submit():
return _render_template_login(form, None)

else:
track_activity("wrong login password for user '{}' using local auth".format(username),
ctx_less=True,
display_in_ui=False)
msg = "Wrong credentials. Please try again."
# assign form data to variables
username = request.form.get('username', '', type=str)
password = request.form.get('password', '', type=str)

else:
track_activity("someone tried to log in with user '{}', which does not exist".format(username),
ctx_less=True, display_in_ui=False)
msg = "Wrong credentials. Please try again."
if is_authentication_ldap() is True:
return _authenticate_ldap(form, username, password)

return render_template('login.html', form=form, msg=msg,
organisation_name=app.config.get('ORGANISATION_NAME'))
return _authenticate_password(form, username, password)


def wrap_login_user(user):
Expand All @@ -130,4 +142,4 @@ def wrap_login_user(user):
}

track_activity("user '{}' successfully logged-in".format(user), ctx_less=True, display_in_ui=False)
return redirect(url_for('index.index', cid=user.ctx_case))
return redirect(url_for('index.index', cid=user.ctx_case))
7 changes: 6 additions & 1 deletion source/app/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ class CeleryConfig:
# --------- APP ---------
class Config:
# Handled by bumpversion
IRIS_VERSION = "v2.0.1"
IRIS_VERSION = "v2.0.2"

API_MIN_VERSION = "2.0.0"
API_MAX_VERSION = "2.0.0"
Expand Down Expand Up @@ -400,6 +400,11 @@ class Config:

LDAP_AUTHENTICATION_TYPE = config.load('LDAP', 'AUTHENTICATION_TYPE')

LDAP_SEARCH_DN = config.load('LDAP', 'SEARCH_DN')
LDAP_ATTRIBUTE_IDENTIFIER = config.load('LDAP', 'ATTRIBUTE_IDENTIFIER')
LDAP_ATTRIBUTE_DISPLAY_NAME = config.load('LDAP', 'ATTRIBUTE_DISPLAY_NAME')
LDAP_ATTRIBUTE_MAIL = config.load('LDAP', 'ATTRIBUTE_MAIL')

LDAP_USE_SSL = config.load('LDAP', 'USE_SSL', fallback='True')
LDAP_USE_SSL = (LDAP_USE_SSL == 'True')

Expand Down
8 changes: 8 additions & 0 deletions source/app/datamgmt/manage/manage_users_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ def get_user(user_id, id_key: str = 'id'):
return user


def get_active_user_by_login(username):
user = User.query.filter(
User.user == username,
User.active == True
).first()
return user


def list_users_id():
users = User.query.with_entities(User.user_id).all()
return users
Expand Down
51 changes: 50 additions & 1 deletion source/app/iris_engine/access_control/ldap_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

import random
import string
import ldap3.core.exceptions
import ssl
from ldap3 import Connection
Expand All @@ -24,16 +27,59 @@
from ldap3.utils import conv

from app import app
from app.datamgmt.manage.manage_users_db import create_user, get_active_user_by_login

log = app.logger


def _get_unique_identifier(user_login):
if app.config.get('LDAP_AUTHENTICATION_TYPE').lower() == 'ntlm':
return user_login[user_login.find('\\')+1:]
return user_login


def _provision_user(connection, user_login):
if get_active_user_by_login(user_login):
return
search_base = app.config.get('LDAP_SEARCH_DN')
attribute_unique_identifier = app.config.get('LDAP_ATTRIBUTE_IDENTIFIER')
unique_identifier = _get_unique_identifier(user_login)
attribute_display_name = app.config.get('LDAP_ATTRIBUTE_DISPLAY_NAME')
attribute_mail = app.config.get('LDAP_ATTRIBUTE_MAIL')
attributes = []
if attribute_display_name:
attributes.append(attribute_display_name)
if attribute_mail:
attributes.append(attribute_mail)
connection.search(search_base, f'({attribute_unique_identifier}={unique_identifier})', attributes=attributes)
entry = connection.entries[0]
if attribute_display_name:
user_name = entry[attribute_display_name].value
else:
user_name = user_login
if attribute_mail:
user_email = entry[attribute_mail].value
else:
user_email = f'{user_login}@ldap'

log.info(f'Provisioning user "{user_login}" which is present in LDAP but not yet in database.')
# TODO the user password is chosen randomly
# ideally it should be possible to create a user without providing any password
# TODO to create the user password, we use the same code as the one to generate the administrator password in post_init.py
# => should factor and reuse this code bit as a function
# => also, it should probably be more secure to use the secrets module (instead of random)
password = ''.join(random.choices(string.printable[:-6], k=16))
# TODO It seems email unicity is required (a fixed email causes a problem at the second account creation)
# The email either comes from the ldap or is forged from the login to ensure unicity
create_user(user_name, user_login, password, user_email, True)


def ldap_authenticate(ldap_user_name, ldap_user_pwd):
"""
Authenticate to the LDAP server
"""
ldap_user_pwd = conv.escape_filter_chars(ldap_user_pwd)
if app.config.get("LDAP_AUTHENTICATION_TYPE").lower() != 'ntlm':
if app.config.get('LDAP_AUTHENTICATION_TYPE').lower() != 'ntlm':
ldap_user_name = conv.escape_filter_chars(ldap_user_name)
ldap_user = f"{app.config.get('LDAP_USER_PREFIX')}{ldap_user_name.strip()},{app.config.get('LDAP_USER_SUFFIX')}"
else:
Expand Down Expand Up @@ -61,6 +107,9 @@ def ldap_authenticate(ldap_user_name, ldap_user_pwd):
log.error(f"Cannot bind to ldap server: {conn.last_error} ")
return False

if app.config.get('AUTHENTICATION_CREATE_USER_IF_NOT_EXIST'):
_provision_user(conn, ldap_user_name)

except ldap3.core.exceptions.LDAPInvalidCredentialsResult as e:
log.error(f'Wrong credentials. Error : {e.__str__()}')
return False
Expand Down