Skip to content

[Security] Ability to add roles in form_login_ldap by ldap group #51225

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

Closed
RTUnreal opened this issue Aug 2, 2023 · 2 comments · Fixed by #52181
Closed

[Security] Ability to add roles in form_login_ldap by ldap group #51225

RTUnreal opened this issue Aug 2, 2023 · 2 comments · Fixed by #52181

Comments

@RTUnreal
Copy link
Contributor

RTUnreal commented Aug 2, 2023

Description

When a user logs in with a ldap dn, we need the ability to add roles by groups the user is a member of.

Example

security:
  providers:
    some_ldap:
      ldap:
        service: Symfony\Component\Ldap\Ldap
        base_dn: cn=Users,dc=example,dc=com
        search_dn: "cn=MyService,ou=Services,dc=example,dc=com"
        search_password: '%env(resolve:LDAP_PW)%'
        default_roles: ROLE_USER
        roles:
          'CN=Administrators,CN=Builtin,DC=example,DC=com': ROLE_ADMIN
        extra_fields: ['mail']

Which would result in a user with the a CN=Administrators,CN=Builtin,DC=example,DC=com membership to be assigned the ROLE_ADMIN role.

@ili101
Copy link

ili101 commented Sep 12, 2023

Combining and modifying some solution online:
https://stackoverflow.com/questions/71114617/symfony-5-4-ldap-and-user-entity-password-mixed
https://medium.com/@xobb/map-ldap-groups-to-symfony-3-security-roles-eb3ff09934a9
https://pastebin.com/srUTqw1Z
#18803
https://stackoverflow.com/questions/60053721/ldap-role-in-symfony-4

I came with this solution:
RolesLdapUserProvider.php extend loadUser() with a version that add the roles

<?php

namespace App\Security;

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

/**
 * LdapUserProvider is a simple user provider on top of LDAP.
 * RolesLdapUserProvider handles the mapping of ldap groups to security roles.
 */
class RolesLdapUserProvider extends LdapUserProvider
{
    public function __construct(LdapInterface $ldap,
        string $baseDn,
        string $searchDn = null,
        #[\SensitiveParameter] string $searchPassword = null,
        array $defaultRoles = [],
        string $uidKey = null,
        string $filter = null,
        string $passwordAttribute = null,
        array $extraFields = [],
        // Key is the group name, value is the role name.
        array $groupMapping = [],
        // Extracts group name from dn string.
        // You might want to change it to match your ldap server.
        private string $groupNameRegExp = '/^CN=(?P<group>[^,]+),ou.*$/i',
    ) {
        parent::__construct($ldap, $baseDn, $searchDn, $searchPassword, $defaultRoles, $uidKey, $filter, $passwordAttribute, $extraFields);
        $this->groupMapping = array_change_key_case($groupMapping, CASE_LOWER);
    }

    /** Maps ldap groups to roles */
    private array $groupMapping;

    /**
     * Loads a user from an LDAP entry.
     */
    protected function loadUser(string $identifier, Entry $entry): UserInterface
    {
        $ldapUser = parent::loadUser($identifier, $entry);

        if (!$ldapUser instanceof LdapUser) {
            throw new \Exception(sprintf('Instances of "%s" are not supported.', \get_class($ldapUser)));
        }

        if (!$entry->hasAttribute('memberOf')) { // Check if the entry has attribute with the group
            return new $ldapUser;
        }
        $roles = $ldapUser->getRoles();
        foreach ($entry->getAttribute('memberOf') as $groupLine) { // Iterate through each group entry line
            $groupName = strtolower($this->getGroupName($groupLine)); // Extract and normalize the group name from the line
            if (array_key_exists($groupName, $this->groupMapping)) { // Check if the group is in the mapping
                $roles[] = $this->groupMapping[$groupName]; // Map the group to the role the user will have
            }
        }
        return new LdapUser($entry, $identifier, $ldapUser->getPassword(), $roles, $ldapUser->getExtraFields()); // Create and return the user object
    }

    /**
     * Get the group name from the DN
     */
    private function getGroupName(string $dn): string
    {
        $matches = [];
        return preg_match($this->groupNameRegExp, $dn, $matches) ? $matches['group'] : '';
    }
}

services.yaml override "LdapUserProvider" with "RolesLdapUserProvider" and add the roles mapping

services:
    ...
    security.user.provider.ldap:
        class: App\Security\RolesLdapUserProvider
        arguments:
            [
                ~,
                ~,
                ~,
                ~,
                ~,
                ~,
                ~,
                ~,
                ~,
                { 'LDAP-User': 'ROLE_USER', 'LDAP-Admin': 'ROLE_ADMIN' },
            ]

If needed I can make a PR so all you need to do is add the "roles:" as in @RTUnreal example.

@Spomky
Copy link
Contributor

Spomky commented Oct 18, 2023

Hi,

What about something we could call a RoleFetcher? (better ideas are welcome)
This service could be in charge of fetching roles from a given DN.
default_roles could be deprecated in favor of this option that could also return the default role if needed.

security:
  providers:
    some_ldap:
      ldap:
        service: Symfony\Component\Ldap\Ldap
        base_dn: cn=Users,dc=example,dc=com
        search_dn: "cn=MyService,ou=Services,dc=example,dc=com"
        search_password: '%env(resolve:LDAP_PW)%'
        role_fetcher: my.custom.ldap.role.fetcher
interface RoleFetcherInterface
{
    /**
     * return string[] A list of roles.
     */
    public function fetchRoles(string $dn): array;
}

final class CustomRoleFetcher implements RoleFetcherInterface
{
    /**
     * return string[] A list of roles.
     */
    public function fetchRoles(string $dn): array
    {
          // Do what you want with de parameter
         if (...) {
            return ['ROLE_ADMIN'];
        }
        // Return default role or an empty list depending on the security policy
        return ['ROLE_USER'];
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants