Connexion LDAP ou AD

De Wiki de Jordan LE NUFF
Sauter à la navigation Sauter à la recherche

Présentation

Cette page a pour objet de décrire une procédure pour interfacer Symfony avec une authentification LDAP ou AD.

Le contenu de cette procédure est majoritairement inspiré de la documentation officielle de Symfony :

Une autre partie de cette procédure s'est inspirée de la solution de Stanislav Drozdov de ré-écriture du fournisseur d'utilisateurs LDAP :

Objectifs

Cette procédure a deux objectifs :

  1. Rassembler dans une unique page les informations nécessaires à l'interfaçage avec un LDAP ou AD
    • En effet, pour ce type de besoin, les informations sont dispersées au sein de la documentation officielle de Symfony
  2. Aller au-delà des limitations du composant natif LDAP de Symfony
    • Bien que le composant LDAP simplifie énormément la mise en place de l'interfaçage avec Symfony, il n'embarque pas nativement d'option permettant la récupération automatique de certains champs. Il faut donc le surcharger en fonction des besoins

La finalité est de pouvoir réaliser une gestion des droits de l'utilisateur en fonction de ses groupes d'appartenance dans l'Active Directory.

Marche à suivre

La marche à suivre est plutôt bien présentée dans la documentation officielle.

Elle se présente sous la forme suivante :

Installer le module de sécurité

Cette partie est la plus simple de la procédure. Si Symfony a été installé avec l'option --full, elle n'est pas nécessaire.

Pour installer le module de sécurité, lancer la commande suivante à la racine du projet :

Putty icon.png Console SSH

Créer la gestion des utilisateurs

Le module security s'appuie sur des fournisseurs d'utilisateurs pour gérer les utilisateurs.

Comme indiqué dans la documentation à propos des fournisseurs d'utilisateurs, Symfony embarque nativement les fournisseurs suivants :

Bien que la documentation principale relative au module de sécurité laisse entendre qu'il faut créer une classe d'utilisateur, cette étape est inutile pour des utilisateurs LDAP (à moins de vouloir utiliser une classe d'utilisateur personnalisée). En effet, le composant symfony/ldap embarque nativement une classe d'utilisateur LdapUser.

Installer le composant LDAP

Pour installer le composant LDAP, lancer la commande suivante à la racine du projet :

Putty icon.png Console SSH

Configurer le client LDAP

Pour configurer le client LDAP, ajouter la section suivante dans le fichier config/services.yaml :

parameters:
...
    app_ldap_host_default: my-server
    app_ldap_port_default: 389
    app_ldap_encryption_default: tls
...
    env(APP_LDAP_HOST): '%env(default:app_ldap_host_default:LDAP_HOST)%'
    env(APP_LDAP_PORT): '%env(default:app_ldap_port_default:LDAP_PORT)%'
    env(APP_LDAP_ENCRYPTION): '%env(default:app_ldap_encryption_default:LDAP_ENCRYPTION)%'
...
services:
...
    # LDAP configuration
    Symfony\Component\Ldap\Ldap:
        arguments: ['@Symfony\Component\Ldap\Adapter\ExtLdap\Adapter']
    Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
        arguments:
            -   host: '%env(resolve:APP_LDAP_HOST)%'
                port: '%env(resolve:APP_LDAP_PORT)%'
                encryption: '%env(resolve:APP_LDAP_ENCRYPTION)%'
                options:
                    protocol_version: 3
                    referrals: false

Le contenu de cette section diffère par rapport à la documentation officielle au niveau du contenu des variables host, port et encryption. En effet, afin de rendre l'application complètement paramétrable depuis des variables d'environnement du serveur, il a été défini des paramètres par défaut et des variables internes à l'application.

Attention ! Il faut que l'extension LDAP pour PHP soit installée pour que le composant soit fonctionnel.

Par ailleurs, il est recommandé d'ajouter "ext-ldap": "*", dans la section "require" du fichier composer.json afin d'indiquer que l'application Symfony a besoin de cette extension PHP pour fonctionner correctement.

Plus d'information sur : https://symfony.com/doc/current/security/ldap.html#configuring-the-ldap-client

Définir le fournisseur d'utilisateurs de type "LDAP"

Comme indiqué dans la documentation Symfony pour le paramétrage du fournisseur d'utilisateurs de type "LDAP", il est possible d'utiliser le fournisseur natif en suivant l'exemple donné dans la documentation. Toutefois, comme évoqué en introduction de cette procédure, les besoins de l'application peuvent se heurter aux limites de ce fournisseur. Il va donc falloir utiliser un fournisseur d'utilisateurs LDAP personnalisé.

Ainsi, en s'appuyant sur la relative à la mise en place d'un fournisseur d'utilisateur personnalisé, la définition du nouveau fournisseur se fait dans les fichiers config/services.yaml et config/packages/security.yaml.

Dans le fichier config/services.yaml, ajouter la section suivante :

parameters:
...
    app_ldap_basedn_default: dc=example,dc=com
    app_ldap_searchdn_default: "cn=read-only-admin,dc=example,dc=com"
    app_ldap_searchpassword_default: password
...
    env(APP_LDAP_BASEDN): '%env(default:app_ldap_basedn_default:LDAP_BASEDN)%'
    env(APP_LDAP_SEARCHDN): '%env(default:app_ldap_searchdn_default:LDAP_SEARCHDN)%'
    env(APP_LDAP_SEARCHPASSWORD): '%env(default:app_ldap_searchpassword_default:LDAP_SEARCHPASSWORD)%'
...
services:
...
    # Custom LDAP User Provider
    App\Security\CustomLdapUserProvider:
        arguments:
            - '@Symfony\Component\Ldap\Ldap'
            - '%env(resolve:APP_LDAP_BASEDN)%'
            - '%env(resolve:APP_LDAP_SEARCHDN)%'
            - '%env(resolve:APP_LDAP_SEARCHPASSWORD)%'

Explication : Cette section permet de définir un service déclaré dans la classe App\Security\CustomLdapUserProvider (cette classe est ensuite à créer) et s'appuie sur le client LDAP existant (Symfony\Component\Ldap\Ldap). C'est ce service qui va récupérer de façon personnalisée les utilisateurs LDAP ou AD. Les variables APP_LDAP_BASEDN, APP_LDAP_SEARCHDN et APP_LDAP_SEARCHPASSWORD reposent sur l'existence de variables d'environnement du serveur, comme expliqué précédemment.

Dans le fichier config/packages/security.yaml, ajouter la section suivante :

security:
    providers:
        ldap_users:
            id: App\Security\CustomLdapUserProvider
...

Explication : Le paramètre "providers:ldap_users:id" permet de définir un fournisseur d'utilisateur (providers) référencé ldap_users (ldap_users) personnalisé (id) pointant vers le service précédemment défini App\Security\CustomLdapUserProvider.

Surcharger le fournisseur d'utilisateurs de type "LDAP"

Une fois que les groupes d'appartenance de l'utilisateur seront extraits depuis l'Active Directory, il faudra les stockés dans une variable. Le but étant de réaliser un filtrage selon ces derniers, il faudra donc les stocker dans la propriété $roles exigée pour tout utilisateur de Symfony fonctionnant avec le module de sécurité de base.

Ainsi, créer le fichier src/Security/CustomLdapUserProvider.php avec le contenu suivant :

<?php

namespace App\Security;

use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\Security\LdapUser;
use Symfony\Component\Ldap\Security\LdapUserProvider;
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
use Symfony\Component\Security\Core\User\UserInterface;

class CustomLdapUserProvider extends LdapUserProvider
{
    private $defaultRoles;
    private $passwordAttribute;
    private $extraFields = array();

    /**
     * Loads a user from an LDAP entry.
     *
     * @param string $username
     * @param Entry $entry
     * @return UserInterface
     */
    protected function loadUser(string $username, Entry $entry)
    {
        $password = null;
        $extraFields = [];

        if (null !== $this->passwordAttribute) {
            $password = $this->getAttributeValue($entry, $this->passwordAttribute);
        }

        foreach ($this->extraFields as $field) {
            $extraFields[$field] = $this->getAttributeValue($entry, $field);
        }

        $results=array();
        foreach ($entry->getAttribute("memberOf") as $LdapGroupDn)
        {
            $results[]= "ROLE_".ldap_explode_dn($LdapGroupDn,1)[0];
        }

        if (!empty($results))
            $roles=$results;
        else
            $roles=$this->defaultRoles;

        return new LdapUser($entry, $username, $password, $roles, $extraFields);
    }

    private function getAttributeValue(Entry $entry, string $attribute)
    {
        if (!$entry->hasAttribute($attribute)) {
            throw new InvalidArgumentException(sprintf('Missing attribute "%s" for user "%s".', $attribute, $entry->getDn()));
        }

        $values = $entry->getAttribute($attribute);

        if (1 !== \count($values)) {
            throw new InvalidArgumentException(sprintf('Attribute "%s" has multiple values.', $attribute));
        }

        return $values[0];
    }

}

Explications :

  • class CustomLdapUserProvider extends LdapUserProvider
    • Création d'une classe enfant CustomLdapUserProvider qui hérite de toutes les propriétés et méthodes de la classe parente LdapUserProvider
  • Propriétés privées $defaultRoles, $passwordAttribute et $extraFields
    • Déclaration des propriétés nécessaires à l'utilisation de classe CustomLdapUserProvider
    • Obligation de surcharger la propriété $extraFields avec un = array() pour assurer la compatibilité avec la classe parente (problème d'incompatibilité encore inexpliqué)
  • protected function loadUser(string $username, Entry $entry)
    • C'est cette fonction loadUser qu'il est nécessaire de ré-écrire. En effet, c'est à cette étape qu'il faudra alimenter l'utilisateur Symfony avec les données désirées extraites depuis le LDAP
  • $results=array();
    • Variable qui contiendra les groupes au format attendu par Symfony
  • foreach ($entry->getAttribute("memberOf") as $LdapGroupDn)
    • Boucle pour récupérer chaque contenu de l'attribut memberOf depuis l'Active Directory dans une variable $LdapGroupDn
  • $results[]= "ROLE_".ldap_explode_dn($LdapGroupDn,1)[0];
    • Utilisation de la fonction PHP ldap_explode_dn pour éclater la variable $LdapGroupDn et récupérer la ligne 0 du tableau PHP (correspondant au CN du groupe AD)
    • Ajout de la chaîne de caractère "ROLE_" au début du CN pour que cela corresponde au format attendu par Symfony
    • Ajout du résultat du traitement dans le tableau de variable $results[]
  • if (!empty($results)) $roles=$results; else $roles=$this->defaultRoles;
    • Si la variable $results[] n'est pas vide, on affecte les résultats aux rôles de l'utilisateur Symfony
    • Si la variable $results[] est vide, on affecte à l'utilisateur Symfony les rôles par défaut
  • private function getAttributeValue(Entry $entry, string $attribute)
    • Simple copier/coller de la fonction privée getAttributeValue de la classe parente (car utilisée dans dans la fonction loadUser)

Activer le fournisseur d'utilisateurs de type "LDAP"

Pour activer le fournisseur d'utilisateurs de type "LDAP" précédemment créé, définir dans le fichier config/packages/security.yaml le paramètre "security:firewalls:main:provider" à ldap_users (par défaut users_in_memory). Cela donner le code suivant :

security:
...
    firewalls:
...
        main:
...
            provider: ldap_users
...

Mise en place de l'authentification

Pare-feux

Le principe du système d'authentification du module de sécurité de Symfony repose sur la définition de "pare-feux" (firewalls) dans le fichier config/packages/security.yaml.

Un seul pare-feu est actif à chaque requête. Le nom du pare-feu importe peu. Cela est seulement utile pour savoir quel chemin a pris la requête d'un utilisateur pour s'authentifier.

Les paramètres pattern, host et methods du pare-feu permettent de restreindre l'accès aux ressources de l'application. C'est la première restriction qui correspond à la requête qui activera le pare-feu Symfony correspondant. Voir la documentation sur les restrictions dans un pare-feu Symfony.

Dans la suite de cette procédure, le nom du pare-feu utilisé est main.

Plus d'informations sur https://symfony.com/doc/current/security.html#a-authentication-firewalls.

Authentification

A moins de définir le paramètre "security:firewalls:myfirewall:security" à false, l'authentification est activée par défaut. Cela implique que l'utilisateur doit être authentifié pour accéder à la ressource demandée.

Or, un utilisateur qui arrive sur l'application pour la première fois est, par définition, non authentifié. De ce fait, sans aucun autre paramètre supplémentaire, cet utilisateur ne pourra jamais accéder à l'application.

Pour pouvoir accorder au minimum un accès aux pages de base de l'application, il faut ajouter l'option "security:firewalls:myfirewall:anonymous" et la définir à "lazy". Ainsi, techniquement, du point de vue de Symfony, les utilisateurs non identifiés seront considérés authentifiés comme anonyme (anonymous).

Mise en place du filtrage et des rôles

Résumé