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

Ldap groups #319

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
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
7 changes: 5 additions & 2 deletions .env.model
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ IRIS_AUTHENTICATION_TYPE=local
#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
# the group to which newly created users are initially added, default value is Analysts
#IRIS_NEW_USERS_DEFAULT_GROUP=

# -- FOR LDAP AUTHENTICATION
#IRIS_AUTHENTICATION_TYPE=ldap
Expand All @@ -43,8 +41,13 @@ IRIS_AUTHENTICATION_TYPE=local
#LDAP_USER_PREFIX=uid=
#LDAP_USER_SUFFIX=ou=people,dc=example,dc=com
#LDAP_USE_SSL=False
# bind account dn and password
#LDAP_BIND_DN=
#LDAP_BIND_PASSWORD=
# base DN in which to search for users
#LDAP_SEARCH_DN=ou=users,dc=example,dc=org
# base DN in which to search for groups
#LDAP_GROUP_BASE_DN=ou=IRIS,ou=groups,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
Expand Down
6 changes: 5 additions & 1 deletion source/app/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,6 @@ class Config:

AUTHENTICATION_TYPE = authentication_type
AUTHENTICATION_CREATE_USER_IF_NOT_EXIST = (authentication_create_user_if_not_exists == "True")
IRIS_NEW_USERS_DEFAULT_GROUP = config.load('IRIS', 'NEW_USERS_DEFAULT_GROUP', fallback='Analysts')
AUTHENTICATION_LOCAL_FALLBACK = config.load('IRIS', 'AUTHENTICATION_LOCAL_FALLBACK', fallback="True") == "True"

if authentication_type == 'oidc_proxy':
Expand Down Expand Up @@ -408,6 +407,9 @@ class Config:

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

LDAP_BIND_DN = config.load('LDAP', 'BIND_DN')
LDAP_BIND_PASSWORD = config.load('LDAP', 'BIND_PASSWORD')

LDAP_SEARCH_DN = config.load('LDAP', 'SEARCH_DN')
if authentication_create_user_if_not_exists and LDAP_SEARCH_DN is None:
raise Exception('LDAP enabled with user provisioning: LDAP_SEARCH_DN should be set')
Expand All @@ -418,6 +420,8 @@ class Config:
LDAP_ATTRIBUTE_DISPLAY_NAME = config.load('LDAP', 'ATTRIBUTE_DISPLAY_NAME')
LDAP_ATTRIBUTE_MAIL = config.load('LDAP', 'ATTRIBUTE_MAIL')

LDAP_GROUP_BASE_DN = config.load('LDAP', 'GROUP_BASE_DN')

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

Expand Down
7 changes: 7 additions & 0 deletions source/app/datamgmt/manage/manage_groups_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@
from app.schema.marshables import AuthorizationGroupSchema


def create_group(name, description):
group = Group(group_name=name, group_description=description, group_permissions=0)
db.session.add(group)
db.session.commit()
return group


def get_groups_list():
groups = Group.query.all()

Expand Down
157 changes: 102 additions & 55 deletions source/app/iris_engine/access_control/ldap_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,66 +29,118 @@
from app import app
from app.datamgmt.manage.manage_users_db import get_active_user_by_login
from app.datamgmt.manage.manage_users_db import create_user
from app.datamgmt.manage.manage_users_db import add_user_to_group
from app.datamgmt.manage.manage_users_db import update_user_groups
from app.datamgmt.manage.manage_groups_db import get_group_by_name
from app.datamgmt.manage.manage_groups_db import create_group

log = app.logger
_log = app.logger
_ldap_authentication_type = app.config.get('LDAP_AUTHENTICATION_TYPE')
_attribute_unique_identifier = app.config.get('LDAP_ATTRIBUTE_IDENTIFIER')
_attribute_display_name = app.config.get('LDAP_ATTRIBUTE_DISPLAY_NAME')
_attribute_mail = app.config.get('LDAP_ATTRIBUTE_MAIL')
_ldap_group_base_dn = app.config.get('LDAP_GROUP_BASE_DN')
_ldap_user_prefix = app.config.get('LDAP_USER_PREFIX')
_ldap_user_suffix = app.config.get('LDAP_USER_SUFFIX')


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 _connect(server, ldap_user, ldap_user_pwd):
connection = Connection(server,
user=ldap_user,
password=ldap_user_pwd,
auto_referrals=False,
authentication=_ldap_authentication_type)

try:
if not connection.bind():
_log.error(f"Cannot bind to ldap server: {connection.last_error} ")
return None

except ldap3.core.exceptions.LDAPInvalidCredentialsResult as e:
_log.error(f'Wrong credentials. Error : {e.__str__()}')
return None

return connection


def _connect_bind_account(server):
ldap_bind_dn = app.config.get('LDAP_BIND_DN')
ldap_bind_password = app.config.get('LDAP_BIND_PASSWORD')
return _connect(server, ldap_bind_dn, ldap_bind_password)


def _provision_user(connection, user_login):
if get_active_user_by_login(user_login):
return
def _connect_user(server, ldap_user_name, ldap_user_pwd):
ldap_user = ldap_user_name.strip()
ldap_user = f'{_ldap_user_prefix}{ldap_user}'
# TODO idea: ldap_user_suffix could include the ',' so that we don't need to make a special case for ntlm
if _ldap_user_suffix and _ldap_authentication_type.lower() != 'ntlm':
ldap_user = f'{ldap_user},{_ldap_user_suffix}'
return _connect(server, ldap_user, ldap_user_pwd)


def _search_user_in_ldap(connection, user_login):
search_base = app.config.get('LDAP_SEARCH_DN')
attribute_unique_identifier = app.config.get('LDAP_ATTRIBUTE_IDENTIFIER')
unique_identifier = conv.escape_filter_chars(_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
unique_identifier = conv.escape_filter_chars(user_login)
attributes = ['memberOf']
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)
return connection.entries[0]


def _provision_user(user_login, ldap_user_entry):
if _attribute_display_name:
user_name = ldap_user_entry[_attribute_display_name].value
else:
user_name = user_login
if attribute_mail:
user_email = entry[attribute_mail].value
if _attribute_mail:
user_email = ldap_user_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.')
_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
user = create_user(user_name, user_login, password, user_email, True)
initial_group = get_group_by_name(app.config.get('IRIS_NEW_USERS_DEFAULT_GROUP'))
add_user_to_group(user.id, initial_group.group_id)
# TODO It seems email uniqueness 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 uniqueness
return create_user(user_name, user_login, password, user_email, True)


def _parse_cn(distinguished_name):
relative_distinguished_names = distinguished_name.split(',')
common_name = relative_distinguished_names[0]
return common_name[len('cn='):]


def _update_user_groups(user, ldap_user_entry):
ldap_group_names = ldap_user_entry['memberOf'].value
if ldap_group_names is None:
ldap_group_names = []
if isinstance(ldap_group_names, str):
ldap_group_names = [ldap_group_names]

groups = []
for ldap_group_name in ldap_group_names:
if not ldap_group_name.endswith(_ldap_group_base_dn):
continue
group_name = _parse_cn(ldap_group_name)
group = get_group_by_name(group_name)
if group is None:
_log.warning(f'Ignoring group declared in LDAP which does not exist in DFIR-IRIS: \'{group_name}\'.')
continue
groups.append(group.group_id)
update_user_groups(user.id, groups)


def ldap_authenticate(ldap_user_name, ldap_user_pwd):
"""
Authenticate to the LDAP server
"""
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') if app.config.get('LDAP_USER_SUFFIX') else ''}"
else:
ldap_user = f"{ldap_user_name.strip()}"

if app.config.get('LDAP_CUSTOM_TLS_CONFIG') is True:
tls_configuration = Tls(validate=ssl.CERT_REQUIRED,
version=app.config.get('LDAP_TLS_VERSION'),
Expand All @@ -106,28 +158,23 @@ def ldap_authenticate(ldap_user_name, ldap_user_pwd):
server = Server(f'{app.config.get("LDAP_CONNECT_STRING")}',
use_ssl=app.config.get('LDAP_USE_SSL'))

conn = Connection(server,
user=ldap_user,
password=ldap_user_pwd,
auto_referrals=False,
authentication=app.config.get('LDAP_AUTHENTICATION_TYPE'))

try:

if not conn.bind():
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)
if _ldap_authentication_type.lower() != 'ntlm':
ldap_user_name = conv.escape_filter_chars(ldap_user_name)

except ldap3.core.exceptions.LDAPInvalidCredentialsResult as e:
log.error(f'Wrong credentials. Error : {e.__str__()}')
connection = _connect_user(server, ldap_user_name, ldap_user_pwd)
if not connection:
return False

except Exception as e:
raise Exception(e.__str__())
if app.config.get('AUTHENTICATION_CREATE_USER_IF_NOT_EXIST'):
connection = _connect_bind_account(server)
if not connection:
return False
ldap_user_entry = _search_user_in_ldap(connection, ldap_user_name)
user = get_active_user_by_login(ldap_user_name)
if not user:
user = _provision_user(ldap_user_name, ldap_user_entry)
_update_user_groups(user, ldap_user_entry)

log.info(f"Successful authenticated user")
_log.info(f"Successful authenticated user")

return True
2 changes: 2 additions & 0 deletions source/app/models/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ class Group(db.Model):
server_default=text('gen_random_uuid()'), unique=True)
group_name = Column(Text, nullable=False, unique=True)
group_description = Column(Text)

# this is a mask of values defined in enum Permissions
group_permissions = Column(BigInteger, nullable=False)
group_auto_follow = Column(Boolean, nullable=False, default=False)
group_auto_follow_access_level = Column(BigInteger, nullable=False, default=0)
Expand Down