DirectoryTree / LdapRecord-Laravel

Multi-domain LDAP Authentication & Management for Laravel.
https://ldaprecord.com/docs/laravel/v3
MIT License
509 stars 54 forks source link

[Support] ldap:test shows successful connection but code says connection doesn't exist #459

Closed kirkaracha closed 2 years ago

kirkaracha commented 2 years ago

I have these settings in my .env file; I do not have a config file:

LDAP_LOGGING=true
LDAP_CONNECTION=default
LDAP_CONNECTIONS=default

LDAP_DEFAULT_HOSTS=eocdc01.coa.domain
LDAP_DEFAULT_USERNAME=COA\******adadmin
LDAP_DEFAULT_PASSWORD=***********************************
LDAP_DEFAULT_PORT=389
LDAP_DEFAULT_BASE_DN="dc=coa,dc=domain"
LDAP_DEFAULT_TIMEOUT=5
LDAP_DEFAULT_SSL=false
LDAP_DEFAULT_TLS=false

Running php artisan ldap:test shows a successful connection:

php artisan ldap:test
Testing LDAP connection [default]...
+------------+------------+-------------------+-------------------------+---------------+
| Connection | Successful | Username          | Message                 | Response Time |
+------------+------------+-------------------+-------------------------+---------------+
| default    | ✔ Yes      | COA\anchoradadmin | Successfully connected. | 37.24ms       |
+------------+------------+-------------------+-------------------------+---------------+

I wrote an LdapSearchService:

<?php

declare(strict_types=1);

namespace App\Services;

use LdapRecord\Connection;
use LdapRecord\Container;
use LdapRecord\Query\Collection;

final class LdapSearchService
{
    protected const LDAP_ATTRIBUTES = [
        'givenname',
        'sn',
        'displayname',
        'title',
        'telephonenumber',
        'mobile',
        'facsimiletelephonenumber',
        'samaccountname',
        'distinguishedname',
        'mail',
        'guid'
    ];

    protected Connection $connection;
    protected Container $container;

    public function __construct(Container $container) {
        $this->container = $container;
        $this->connection = $this->container->getDefaultConnection();
    }

    public function getUsersByTeamLdapName(string $teamLdapName): Collection|array
    {
        $query = $this->connection
            ->query()
            ->setDn("ou=Users,ou=$teamLdapName,ou=ALAMEDA,dc=coa,dc=domain")
            ->select(self::LDAP_ATTRIBUTES);

        return $query->get();
    }
}

I call this service from a command:

<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Jobs\ImportUsersByLdapName;
use App\Repositories\TeamRepositoryInterface;
use App\Services\LdapSearchService;
use Illuminate\Console\Command;

final class ImportUsers extends Command
{
    protected $signature = 'directory:import-users';
    protected $description = 'Import users from Active Directory';

    protected TeamRepositoryInterface $teamRepository;

    public function __construct(TeamRepositoryInterface $teamRepository)
    {
        parent::__construct();

        $this->teamRepository = $teamRepository;
    }

    public function handle(LdapSearchService $ldapSearchService): void
    {
        $users = $ldapSearchService->getUsersByTeamLdapName('Information Technology');

        foreach ($users as $user) {
            $this->info($user['samaccountname'][0]);
        }

//        $ldapNames = $this->teamRepository->getDepartmentLdapNames();

//        foreach ($ldapNames as $ldapName) {
//            dispatch(new ImportUsersByLdapName($ldapName));
//        }
    }
}

When I call my command using php artisan directory:import-users, I get an error saying the default connection doesn't exist, even though the test command shows the default connection does exist.

 LdapRecord\ContainerException

  The LDAP connection [default] does not exist.

  at C:\inetpub\the-anchor\vendor\directorytree\ldaprecord\src\ConnectionManager.php:162
    158▕         if ($this->exists($name = $name ?? $this->default)) {
    159▕             return $this->connections[$name];
    160▕         }
    161▕
  ➜ 162▕         throw new ContainerException("The LDAP connection [$name] does not exist.");
    163▕     }
    164▕
    165▕     /**
    166▕      * Return the default connection.

  1   C:\inetpub\the-anchor\vendor\directorytree\ldaprecord\src\ConnectionManager.php:172
      LdapRecord\ConnectionManager::get()

  2   C:\inetpub\the-anchor\vendor\directorytree\ldaprecord\src\ConnectionManager.php:101
      LdapRecord\ConnectionManager::getDefault()

For now I'm just trying to get a department's users in the search service. I need to import by department to assign roles and permissions, etc.

The same LdapSearchService code worked with v1.7.6 of directorytree/ldaprecord-laravel.

I know LdapRecord-Laravel has an import, but filtering doesn't work (I opened this issue):

 php artisan ldap:import default --filter "(distinguishedName=*ou\\=City Clerk*)"
Provider [default] does not exist.

Running php artisan ldap:import by itself doesn't do anything.

I've been trying to get past this for a couple of weeks off and on and I need to resolve it this week or go to Plan B.

Environment:

stevebauman commented 2 years ago

Hi @kirkaracha! Thanks so much for the sponsorship ❤️

Are you binding LdapSearchService into Laravel's IoC, or are you letting it be resolved automatically? I.e. binding it into the container via:

// ../AppServiceProvider.php

public function bind()
{
    $this->app->bind(LdapSearchService::class, function () {
        return new LdapSearchService(Container::getInstance());
    });
}

The LdapRecord Container is meant to be a singleton. It's self-managed, and its instance can be accessed like so:

use LdapRecord\Container;

$container = Container::getInstance();

What's likely happening here is that your LdapSearchService is getting a new Container instance instead of the singleton instance containing the default connection.

This should resolve your issue:

final class LdapSearchService
{
    // ...

    public function __construct() {
        $this->container = Container::getInstance();
        $this->connection = $this->container->getDefaultConnection();
    }

Let me know if that works! 👍


Note: You can see how the container is utilized and how connections are registered in LdapRecord-Laravel via the LdapServiceProvider here. Only static calls are used, since the container is a singleton:

https://github.com/DirectoryTree/LdapRecord-Laravel/blob/ec537bc3534f874bcade5febd3199cbb0758d9bb/src/LdapServiceProvider.php#L115

kirkaracha commented 2 years ago

Good news, not-so-good news. The Container::getInstance() change searches successfully, but I'm having trouble accessing the values in the results.

The query returns an array of data for each user like this:

Array
(
    [objectclass] => Array
        (
            [count] => 4
            [0] => top
            [1] => person
            [2] => organizationalPerson
            [3] => user
        )

    [0] => objectclass
    [sn] => Array
        (
            [count] => 1
            [0] => Test 5
        )

    [1] => sn
    [givenname] => Array
        (
            [count] => 1
            [0] => Munis
        )

    [2] => givenname
    [distinguishedname] => Array
        (
            [count] => 1
            [0] => CN=Munis Test 5,OU=Users,OU=Information Technology,OU=ALAMEDA,DC=coa,DC=domain
        )

    [3] => distinguishedname
    [displayname] => Array
        (
            [count] => 1
            [0] => Munis Test 5
        )

    [4] => displayname
    [samaccountname] => Array
        (
            [count] => 1
            [0] => munistest5
        )

    [5] => samaccountname
    [mail] => Array
        (
            [count] => 1
            [0] => munistest5@alamedaca.gov
        )

    [6] => mail
    [count] => 7
    [dn] => CN=Munis Test 5,OU=Users,OU=Information Technology,OU=ALAMEDA,DC=coa,DC=domain
)

Doing a foreach on users $this->info($user['sn'][0]); results in a Undefined array key "sn" error.

I was able to access the values that way in the earlier version of LdapRecord.

stevebauman commented 2 years ago

Doing a foreach on users $this->info($user['sn'][0]); results in a Undefined array key "sn" error.

That sounds like you have a user that does not have a surname (sn) attribute. You will have to use the null coalesce operator so PHP doesn't thrown an exception when it's null:

$this->info($user['sn'][0] ?? null);

If you use LdapRecord models, it's much easier to work with query results, rather than running raw queries:

use LdapRecord\Models\Entry;

$objects = Entry::get();

foreach ($objects as $object) {
    // Safely returns `null` if the attribute doesn't exist.
    $object->getFirstAttribute('sn');
}
kirkaracha commented 2 years ago

The user does have a sn attribute (see sample array of data above), I'm just having trouble with the syntax to access it.

I'm not tied to that approach though. What would you recommend for an automated job that gets users by department?

stevebauman commented 2 years ago

The user does have a sn attribute (see sample array of data above), I'm just having trouble with the syntax to access it.

Doing a foreach on users $this->info($user['sn'][0]); results in a Undefined array key "sn" error.

You mentioned you're doing a foreach on a result of many users. One of those users must not have an sn.

I'm not tied to that approach though. What would you recommend for an automated job that gets users by department?

I would recommend using LdapRecord models over raw queries unless you're doing something unsupported by the LdapRecord query builder. Working with results will be much easier. It's the equivalent of using DB::table('users')->... vs User::get() in Laravel.

stevebauman commented 2 years ago

Closing due to inactivity.