Connecter Neo4j à un annuaire LDAP

Objectif

Vous allez découvrir comment connecter un annuaire LDAP à Neo4j pour lui confier la gestion de l’authentification et des accès.

Installation d’un serveur OpenLDAP

Au lieu d’installer un serveur OpenLDAP sur ma machine, je vais utiliser Docker avec l’image suivante : https://github.com/osixia/docker-openldap

Celle-ci me permet de lancer un serveur OpenLDAP pré-configuré où : * le domaine par défault est dc=example,dc=org * l’administrateur est cn=admin,dc=example,dc=org avec le password admin.

De plus on peut lui passer en paramètre le chemin d’accès d’un répertoire contenant des fichiers LDIF, qui vont être éxécutés à son lancement.

Ainsi, voici le fichier LDIF que j’ai réalisé pour créer la structure de mon LDAP et importer les données au démarrage du serveur :

dn: ou=users,dc=example,dc=org
objectClass: organizationalUnit
objectClass: top
ou: users

dn: ou=groups,dc=example,dc=org
objectClass: organizationalUnit
objectClass: top
ou: groups


dn: cn=reader,ou=groups,dc=example,dc=org
objectClass: groupOfNames
objectClass: top
cn: reader
member:uid=reader,ou=users,dc=example,dc=org

dn: cn=publisher,ou=groups,dc=example,dc=org
objectClass: groupOfNames
objectClass: top
cn: publisher
member:uid=publisher,ou=users,dc=example,dc=org

dn: cn=architect,ou=groups,dc=example,dc=org
objectClass: groupOfNames
objectClass: top
cn: architect
member:uid=architect,ou=users,dc=example,dc=org

dn: cn=admin,ou=groups,dc=example,dc=org
objectClass: groupOfNames
objectClass: top
cn: admin
member:uid=admin,ou=users,dc=example,dc=org

dn: uid=reader,ou=users,dc=example,dc=org
objectClass: organizationalPerson
objectClass: person
objectClass: extensibleObject
objectClass: uidObject
objectClass: inetOrgPerson
objectClass: top
cn: Reader User
givenName: Reader
sn: reader
uid: reader
mail: reader@example.com
ou: users
userpassword: test
memberOf: cn=reader,ou=groups,dc=example,dc=org

dn: uid=publisher,ou=users,dc=example,dc=org
objectClass: organizationalPerson
objectClass: person
objectClass: extensibleObject
objectClass: uidObject
objectClass: inetOrgPerson
objectClass: top
cn: Publisher User
givenName: Publisher
sn: publisher
uid: publisher
mail: publisher@example.com
ou: users
userpassword: test
memberOf: cn=publisher,ou=groups,dc=example,dc=org

dn: uid=architect,ou=users,dc=example,dc=org
objectClass: organizationalPerson
objectClass: person
objectClass: extensibleObject
objectClass: uidObject
objectClass: inetOrgPerson
objectClass: top
cn: Architect User
givenName: Architect
sn: architect
uid: architect
mail: architect@example.com
ou: users
userpassword: test
memberOf: cn=architect,ou=groups,dc=example,dc=org

dn: uid=admin,ou=users,dc=example,dc=org
objectClass: organizationalPerson
objectClass: person
objectClass: extensibleObject
objectClass: uidObject
objectClass: inetOrgPerson
objectClass: top
cn: Admin User
givenName: Architect
sn: admin
uid: admin
mail: admin@example.com
ou: users
userpassword: test
memberOf: cn=admin,ou=groups,dc=example,dc=org

Comme vous pouvez le constater, j’y définis les éléments suivants :

  • une organization unit groups : ou=groups,dc=example,dc=org, contenant la définition des groupes suivants

    • groupe admin cn=admin,ou=groupss,dc=example,dc=org

    • groupe architect cn=architect,ou=groups,dc=example,dc=org

    • groupe publisher cn=publisher,ou=groups,dc=example,dc=org

    • groupe reader cn=reader,ou=groups,dc=example,dc=org

  • une organization unit users : ou=users,dc=example,dc=org, contenant la définition des utilisateurs suivants

    • l’utilisateur admin uid=admin,ou=users,dc=example,dc=org

    • l’utilisateur architect uid=architect,ou=users,dc=example,dc=org

    • l’utilisateur publisher uid=publisher,ou=users,dc=example,dc=org

    • l’utilisateur reader uid=reader,ou=users,dc=example,dc=org

INFO: les utilisateurs doivent être liés a leurs groupes via l’attribut memberOf.

Dans mon cas, j’ai sauvegardé ce fichier dans le répertoire /home/bsimard/ldif, ce qui donne la commande suivante pour démarrer le container docker :

docker run \
   --volume /home/bsimard/ldif/:/container/service/slapd/assets/config/bootstrap/ldif/custom \
   osixia/openldap:1.1.10 \
   --copy-service \
   --loglevel debug

La configuration de Neo4j

La configuration de Neo4j est centralisée dans le fichier NEO4J_HOME/conf/neo4j.conf. Ainsi il suffit de l’éditer et d’y ajouter les propriétés suivantes :

#********************************************************************
# Security Configuration
#********************************************************************

# The authentication and authorization provider that contains both users and roles.
# This can be one of the built-in `native` or `ldap` auth providers,
# or it can be an externally provided plugin, with a custom name prefixed by `plugin`,
# i.e. `plugin-<AUTH_PROVIDER_NAME>`.
dbms.security.auth_provider=ldap

# The time to live (TTL) for cached authentication and authorization info when using
# external auth providers (LDAP or plugin). Setting the TTL to 0 will
# disable auth caching.
#dbms.security.auth_cache_ttl=10m

# The maximum capacity for authentication and authorization caches (respectively).
#dbms.security.auth_cache_max_capacity=10000

# Set to log successful authentication events to the security log.
# If this is set to `false` only failed authentication events will be logged, which
# could be useful if you find that the successful events spam the logs too much,
# and you do not require full auditing capability.
#dbms.security.log_successful_authentication=true

#================================================
# LDAP Auth Provider Configuration
#================================================

# URL of LDAP server to use for authentication and authorization.
# The format of the setting is `<protocol>://<hostname>:<port>`, where hostname is the only required field.
# The supported values for protocol are `ldap` (default) and `ldaps`.
# The default port for `ldap` is 389 and for `ldaps` 636.
# For example: `ldaps://ldap.example.com:10389`.
#
# NOTE: You may want to consider using STARTTLS (`dbms.security.ldap.use_starttls`) instead of LDAPS
# for secure connections, in which case the correct protocol is `ldap`.
dbms.security.ldap.host=172.17.0.2

# Use secure communication with the LDAP server using opportunistic TLS.
# First an initial insecure connection will be made with the LDAP server, and then a STARTTLS command
# will be issued to negotiate an upgrade of the connection to TLS before initiating authentication.
#dbms.security.ldap.use_starttls=false

# The LDAP referral behavior when creating a connection. This is one of `follow`, `ignore` or `throw`.
# `follow` automatically follows any referrals
# `ignore` ignores any referrals
# `throw` throws an exception, which will lead to authentication failure
#dbms.security.ldap.referral=follow

# The timeout for establishing an LDAP connection. If a connection with the LDAP server cannot be
# established within the given time the attempt is aborted.
# A value of 0 means to use the network protocol's (i.e., TCP's) timeout value.
#dbms.security.ldap.connection_timeout=30s

# The timeout for an LDAP read request (i.e. search). If the LDAP server does not respond within
# the given time the request will be aborted. A value of 0 means wait for a response indefinitely.
#dbms.security.ldap.read_timeout=30s

#----------------------------------
# LDAP Authentication Configuration
#----------------------------------

# LDAP authentication mechanism. This is one of `simple` or a SASL mechanism supported by JNDI,
# for example `DIGEST-MD5`. `simple` is basic username
# and password authentication and SASL is used for more advanced mechanisms. See RFC 2251 LDAPv3
# documentation for more details.
dbms.security.ldap.authentication.mechanism=simple

# LDAP user DN template. An LDAP object is referenced by its distinguished name (DN), and a user DN is
# an LDAP fully-qualified unique user identifier. This setting is used to generate an LDAP DN that
# conforms with the LDAP directory's schema from the user principal that is submitted with the
# authentication token when logging in.
# The special token {0} is a placeholder where the user principal will be substituted into the DN string.
dbms.security.ldap.authentication.user_dn_template=uid={0},ou=users,dc=example,dc=org

# Determines if the result of authentication via the LDAP server should be cached or not.
# Caching is used to limit the number of LDAP requests that have to be made over the network
# for users that have already been authenticated successfully. A user can be authenticated against
# an existing cache entry (instead of via an LDAP server) as long as it is alive
# (see `dbms.security.auth_cache_ttl`).
# An important consequence of setting this to `true` is that
# Neo4j then needs to cache a hashed version of the credentials in order to perform credentials
# matching. This hashing is done using a cryptographic hash function together with a random salt.
# Preferably a conscious decision should be made if this method is considered acceptable by
# the security standards of the organization in which this Neo4j instance is deployed.
dbms.security.ldap.authentication.cache_enabled=false

#----------------------------------
# LDAP Authorization Configuration
#----------------------------------
# Authorization is performed by searching the directory for the groups that
# the user is a member of, and then map those groups to Neo4j roles.

# Perform LDAP search for authorization info using a system account instead of the user's own account.
#
# If this is set to `false` (default), the search for group membership will be performed
# directly after authentication using the LDAP context bound with the user's own account.
# The mapped roles will be cached for the duration of `dbms.security.auth_cache_ttl`,
# and then expire, requiring re-authentication. To avoid frequently having to re-authenticate
# sessions you may want to set a relatively long auth cache expiration time together with this option.
# NOTE: This option will only work if the users are permitted to search for their
# own group membership attributes in the directory.
#
# If this is set to `true`, the search will be performed using a special system account user
# with read access to all the users in the directory.
# You need to specify the username and password using the settings
# `dbms.security.ldap.authorization.system_username` and
# `dbms.security.ldap.authorization.system_password` with this option.
# Note that this account only needs read access to the relevant parts of the LDAP directory
# and does not need to have access rights to Neo4j, or any other systems.
dbms.security.ldap.authorization.use_system_account=true

# An LDAP system account username to use for authorization searches when
# `dbms.security.ldap.authorization.use_system_account` is `true`.
# Note that the `dbms.security.ldap.authentication.user_dn_template` will not be applied to this username,
# so you may have to specify a full DN.
dbms.security.ldap.authorization.system_username=cn=admin,dc=example,dc=org

# An LDAP system account password to use for authorization searches when
# `dbms.security.ldap.authorization.use_system_account` is `true`.
dbms.security.ldap.authorization.system_password=admin

# The name of the base object or named context to search for user objects when LDAP authorization is enabled.
# A common case is that this matches the last part of `dbms.security.ldap.authentication.user_dn_template`.
dbms.security.ldap.authorization.user_search_base=ou=users,dc=example,dc=org

# The LDAP search filter to search for a user principal when LDAP authorization is
# enabled. The filter should contain the placeholder token {0} which will be substituted for the
# user principal.
dbms.security.ldap.authorization.user_search_filter=(&(objectClass=*)(uid={0}))

# A list of attribute names on a user object that contains groups to be used for mapping to roles
# when LDAP authorization is enabled.
dbms.security.ldap.authorization.group_membership_attributes=memberOf

# An authorization mapping from LDAP group names to Neo4j role names.
# The map should be formatted as a semicolon separated list of key-value pairs, where the
# key is the LDAP group name and the value is a comma separated list of corresponding role names.
# For example: group1=role1;group2=role2;group3=role3,role4,role5
#
# You could also use whitespaces and quotes around group names to make this mapping more readable,
# for example: dbms.security.ldap.authorization.group_to_role_mapping=\
#          "cn=Neo4j Read Only,cn=users,dc=example,dc=com"      = reader;    \
#          "cn=Neo4j Read-Write,cn=users,dc=example,dc=com"     = publisher; \
#          "cn=Neo4j Schema Manager,cn=users,dc=example,dc=com" = architect; \
#          "cn=Neo4j Administrator,cn=users,dc=example,dc=com"  = admin
dbms.security.ldap.authorization.group_to_role_mapping=\
          "cn=reader,ou=groups,dc=example,dc=org"  = reader; \
          "cn=publisher,ou=groups,dc=example,dc=org"  = publisher; \
          "cn=architect,ou=groups,dc=example,dc=org"  = architect; \
          "cn=admin,ou=groups,dc=example,dc=org"  = admin

Après un redémarrage du service, la nouvelle configuration est prise en compte, et on peut tester la connection LDAP directement avec le navigateur Neo4j : http://localhost:7474/browser

Détail du fonctionnement

Il y a deux étapes dans le processus : l’authentification et la gestion des accès

L’authentification

Celle-ci est réalisée directement par un BIND sur le LDAP avec le DN de l’utilisateur. Le DN est calculé par Neo4j grâce à la clef de configuration dbms.security.ldap.authentication.user_dn_template. Le {0} est remplacé par le login de l’utilisateur.

Ainsi dans notre exemple, si on essaye de se connecter avec l’utilisateur admin, le BIND sera effectué avec le DN uid=admin,ou=Users,dc=example,dc=org.

La gestion des accès

Une fois que le couple login/password est validé, on peut passer à la gestion des accès. Celle-ci est déléguée à un utilisateur système qui va effecuter une recherche dans le LDAP pour retrouver l’utilisateur et ses groupes associés.

Ainsi il effectue : * la requête LDAP configurée via la clef dbms.security.ldap.authorization.user_search_filter (dans notre cas (&(objectClass=*)(uid={0}))). Neo4j remplace {0} par le login de l’utilisateur. * dans le sous-arbre définit vi la clef dbms.security.ldap.authorization.user_search_base (dans notre cas ou=users,dc=example,dc=org).

Ceci lui permet de retrouver l’utilisateur dans l’annuaire, puis de récupérer ses groupe via l’attribut configuré par la clef dbms.security.ldap.authorization.group_membership_attributes (donc memberOf dans l’exemple).

Pour finir, la correspondance entre les groupes LDAP et les roles Neo4j est effectué en respectant les règles définie dans la clef de configuration dbms.security.ldap.authorization.group_to_role_mapping.

En bref

  • Le connecteur LDAP est uniquement présent dans l’édition Entreprise de Neo4j

  • La configuratin est simple et permet de gérer à la fois l’authentification et les accès

  • Les utilisateurs doivent impérativement être liés aux groupes via un attribut comme memberOf (relation utilisateur → groupes, et pas l’inverse)

  • L’exemple utilise un utilisateur système, mais ce n’est pas une obligation. Il est possible de modifier la configuration pour que l’utilisateur effectue à la fois l’authenfication et les droits d’accès.