├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── feature_request.md
│ └── support-request.md
└── workflows
│ ├── run-cs-fix.yml
│ └── run-tests.yml
├── .gitignore
├── composer.json
├── config
└── ldap.php
├── database
└── migrations
│ └── 2025_01_01_000000_add_ldap_columns_to_users_table.php
├── license.md
├── phpunit.xml
├── readme.md
├── resources
└── lang
│ └── en
│ └── errors.php
├── src
├── Auth
│ ├── AuthenticatesWithLdap.php
│ ├── BindFailureListener.php
│ ├── CreatesUserProvider.php
│ ├── DatabaseUserProvider.php
│ ├── HasLdapUser.php
│ ├── LdapAuthenticatable.php
│ ├── ListensForLdapBindFailure.php
│ ├── NoDatabaseUserProvider.php
│ ├── Rule.php
│ ├── Rules
│ │ └── OnlyImported.php
│ ├── UserProvider.php
│ └── Validator.php
├── Commands
│ ├── BrowseLdapServer.php
│ ├── GetRootDse.php
│ ├── ImportLdapUsers.php
│ ├── MakeLdapModel.php
│ ├── MakeLdapRule.php
│ ├── MakeLdapScope.php
│ ├── TestLdapConnection.php
│ └── stubs
│ │ ├── model.stub
│ │ ├── rule.stub
│ │ └── scope.stub
├── DetectsSoftDeletes.php
├── Events
│ ├── Auth
│ │ ├── BindFailed.php
│ │ ├── Binding.php
│ │ ├── Bound.php
│ │ ├── Completed.php
│ │ ├── CompletedWithWindows.php
│ │ ├── DiscoveredWithCredentials.php
│ │ ├── EloquentUserTrashed.php
│ │ ├── Event.php
│ │ ├── Rejected.php
│ │ ├── RuleEvent.php
│ │ ├── RuleFailed.php
│ │ └── RulePassed.php
│ ├── Import
│ │ ├── Completed.php
│ │ ├── Deleted.php
│ │ ├── DeletedMissing.php
│ │ ├── Event.php
│ │ ├── ImportFailed.php
│ │ ├── Imported.php
│ │ ├── Importing.php
│ │ ├── Restored.php
│ │ ├── Saved.php
│ │ ├── Started.php
│ │ ├── Synchronized.php
│ │ └── Synchronizing.php
│ ├── Loggable.php
│ └── LoggableEvent.php
├── Import
│ ├── EloquentHydrator.php
│ ├── EloquentUserHydrator.php
│ ├── Hydrators
│ │ ├── AttributeHydrator.php
│ │ ├── DomainHydrator.php
│ │ ├── GuidHydrator.php
│ │ ├── Hydrator.php
│ │ └── PasswordHydrator.php
│ ├── ImportException.php
│ ├── Importer.php
│ ├── LdapUserImporter.php
│ ├── Synchronizer.php
│ └── UserSynchronizer.php
├── ImportableFromLdap.php
├── LdapAuthServiceProvider.php
├── LdapImportable.php
├── LdapRecord.php
├── LdapServiceProvider.php
├── LdapUserAuthenticator.php
├── LdapUserRepository.php
├── Middleware
│ ├── UserDomainValidator.php
│ └── WindowsAuthenticate.php
└── Testing
│ ├── DirectoryEmulator.php
│ ├── Emulated
│ ├── ActiveDirectoryBuilder.php
│ ├── EmulatesModelQueries.php
│ ├── ModelBuilder.php
│ └── OpenLdapBuilder.php
│ ├── EmulatedBuilder.php
│ ├── EmulatedConnectionFake.php
│ ├── EmulatesQueries.php
│ ├── LdapDatabaseManager.php
│ ├── LdapObject.php
│ ├── LdapObjectAttribute.php
│ ├── LdapObjectAttributeValue.php
│ ├── ResolvesEmulatedConnection.php
│ ├── UnescapedValue.php
│ ├── VirtualAttributeObserver.php
│ └── VirtualAttributeValueObserver.php
└── tests
├── Feature
├── Commands
│ ├── GetRootDseTest.php
│ ├── ImportLdapUsersTest.php
│ └── TestLdapConnectionTest.php
├── CreatesTestUsers.php
├── DatabaseTestCase.php
├── DatabaseUserProviderTest.php
├── Emulator
│ ├── EmulatedAuthenticationTest.php
│ ├── EmulatedDatabaseAuthenticationTest.php
│ ├── EmulatedImportTest.php
│ ├── EmulatedModelBindingTest.php
│ ├── EmulatedModelQueryTest.php
│ ├── EmulatedQueryTest.php
│ ├── EmulatedUserRepositoryTest.php
│ ├── EmulatedWindowsAuthenticateTest.php
│ └── EmulatorTest.php
├── LdapUserSynchronizerTest.php
├── ListenForLdapBindFailureTest.php
├── NoDatabaseUserProviderTest.php
├── SanctumTest.php
├── SanctumTestUserModelStub.php
├── TestUserModelStub.php
└── WindowsAuthMiddlewareTest.php
├── TestCase.php
└── Unit
├── EloquentHydratorTest.php
├── LdapDatabaseManagerTest.php
├── LdapImporterTest.php
├── LdapServiceProviderTest.php
├── LdapSynchronizerTest.php
├── LdapUserAuthenticatorTest.php
├── LdapUserRepositoryTest.php
├── ListenForLdapBindFailureTest.php
├── ValidatorRuleOnlyImportedTest.php
└── ValidatorTest.php
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [stevebauman]
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help improve LdapRecord-Laravel
4 | assignees: ''
5 |
6 | ---
7 |
8 |
12 | **Environment:**
13 | - LDAP Server Type: [e.g. ActiveDirectory / OpenLDAP / FreeIPA]
14 | - LdapRecord-Laravel Major Version: [e.g. v1, v2]
15 | - PHP Version: [e.g. 7.3 / 7.4 / 8.0]
16 |
17 | **Describe the bug:**
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for LdapRecord-Laravel
4 | title: "[Feature]"
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the feature you'd like:**
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/support-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Support request
3 | about: Request help using LdapRecord-Laravel (requires sponsorship)
4 | title: "[Support]"
5 | labels: question
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
12 |
13 |
14 |
18 | **Environment:**
19 | - LDAP Server Type: [e.g. ActiveDirectory / OpenLDAP / FreeIPA]
20 | - LdapRecord-Laravel Major Version: [e.g. v1, v2]
21 | - PHP Version: [e.g. 7.3 / 7.4 / 8.0]
22 |
--------------------------------------------------------------------------------
/.github/workflows/run-cs-fix.yml:
--------------------------------------------------------------------------------
1 | name: run-cs-fix
2 |
3 | on:
4 | push:
5 |
6 | jobs:
7 | lint:
8 | runs-on: ubuntu-latest
9 | strategy:
10 | fail-fast: true
11 | matrix:
12 | php: [8.3]
13 |
14 | steps:
15 | - name: Checkout code
16 | uses: actions/checkout@v4
17 |
18 | - name: Setup PHP
19 | uses: shivammathur/setup-php@v2
20 | with:
21 | php-version: ${{ matrix.php }}
22 | extensions: json, dom, curl, libxml, mbstring
23 | coverage: none
24 |
25 | - name: Install Pint
26 | run: composer global require laravel/pint
27 |
28 | - name: Run Pint
29 | run: pint
30 |
31 | - name: Commit linted files
32 | uses: stefanzweifel/git-auto-commit-action@v5
33 | with:
34 | commit_message: "Fix code style"
35 |
--------------------------------------------------------------------------------
/.github/workflows/run-tests.yml:
--------------------------------------------------------------------------------
1 | name: run-tests
2 |
3 | on:
4 | push:
5 | pull_request:
6 | schedule:
7 | - cron: "0 0 * * *"
8 |
9 | jobs:
10 | run-tests:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | fail-fast: false
14 | matrix:
15 | include:
16 | - php: 8.4
17 | laravel: 12.*
18 | testbench: 10.*
19 |
20 | - php: 8.4
21 | laravel: 11.*
22 | testbench: 9.*
23 |
24 | - php: 8.3
25 | laravel: 10.*
26 | testbench: 8.*
27 |
28 | - php: 8.2
29 | laravel: 9.*
30 | testbench: 7.*
31 |
32 | name: P${{ matrix.php }} - L${{ matrix.laravel }}
33 |
34 | steps:
35 | - name: Checkout code
36 | uses: actions/checkout@v2
37 |
38 | - name: Cache dependencies
39 | uses: actions/cache@v3
40 | with:
41 | path: ~/.composer/cache/files
42 | key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
43 |
44 | - name: Setup PHP
45 | uses: shivammathur/setup-php@v2
46 | with:
47 | php-version: ${{ matrix.php }}
48 | extensions: ldap, fileinfo, dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, mysql, pdo_mysql, bcmath, intl, gd, exif, iconv
49 | coverage: none
50 |
51 | - name: Install dependencies
52 | run: |
53 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update --dev
54 | composer update --prefer-stable --prefer-dist --no-interaction
55 |
56 | - name: Execute tests
57 | run: vendor/bin/phpunit --testdox
58 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | vendor
3 | composer.lock
4 | .phpunit.cache
5 | .phpunit.result.cache
6 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "directorytree/ldaprecord-laravel",
3 | "description": "LDAP Authentication & Management for Laravel.",
4 | "keywords": [
5 | "ldaprecord",
6 | "adldap2",
7 | "ldap",
8 | "laravel"
9 | ],
10 | "license": "MIT",
11 | "type": "project",
12 | "require": {
13 | "php": ">=8.1",
14 | "ext-ldap": "*",
15 | "ext-json": "*",
16 | "ramsey/uuid": "*",
17 | "directorytree/ldaprecord": "^v3.3",
18 | "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0"
19 | },
20 | "require-dev": {
21 | "mockery/mockery": "^1.0",
22 | "phpunit/phpunit": "^8.0|^9.0|^10.0|^11.0",
23 | "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0|^10.0",
24 | "spatie/ray": "^1.28",
25 | "laravel/sanctum": "*",
26 | "laravel/pint": "^1.9"
27 | },
28 | "archive": {
29 | "exclude": [
30 | "/tests"
31 | ]
32 | },
33 | "autoload": {
34 | "psr-4": {
35 | "LdapRecord\\Laravel\\": "src/"
36 | }
37 | },
38 | "autoload-dev": {
39 | "psr-4": {
40 | "LdapRecord\\Laravel\\Tests\\": "tests/"
41 | }
42 | },
43 | "extra": {
44 | "laravel": {
45 | "providers": [
46 | "LdapRecord\\Laravel\\LdapServiceProvider",
47 | "LdapRecord\\Laravel\\LdapAuthServiceProvider"
48 | ]
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/config/ldap.php:
--------------------------------------------------------------------------------
1 | env('LDAP_CONNECTION', 'default'),
17 |
18 | /*
19 | |--------------------------------------------------------------------------
20 | | LDAP Connections
21 | |--------------------------------------------------------------------------
22 | |
23 | | Below you may configure each LDAP connection your application requires
24 | | access to. Be sure to include a valid base DN - otherwise you may
25 | | not receive any results when performing LDAP search operations.
26 | |
27 | */
28 |
29 | 'connections' => [
30 |
31 | 'default' => [
32 | 'hosts' => [env('LDAP_HOST', '127.0.0.1')],
33 | 'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'),
34 | 'password' => env('LDAP_PASSWORD', 'secret'),
35 | 'port' => env('LDAP_PORT', 389),
36 | 'base_dn' => env('LDAP_BASE_DN', 'dc=local,dc=com'),
37 | 'timeout' => env('LDAP_TIMEOUT', 5),
38 | 'use_ssl' => env('LDAP_SSL', false),
39 | 'use_tls' => env('LDAP_TLS', false),
40 | 'use_sasl' => env('LDAP_SASL', false),
41 | 'sasl_options' => [
42 | // 'mech' => 'GSSAPI',
43 | ],
44 | ],
45 |
46 | ],
47 |
48 | /*
49 | |--------------------------------------------------------------------------
50 | | LDAP Logging
51 | |--------------------------------------------------------------------------
52 | |
53 | | When LDAP logging is enabled, all LDAP search and authentication
54 | | operations are logged using the default application logging
55 | | driver. This can assist in debugging issues and more.
56 | |
57 | */
58 |
59 | 'logging' => [
60 | 'enabled' => env('LDAP_LOGGING', true),
61 | 'channel' => env('LOG_CHANNEL', 'stack'),
62 | 'level' => env('LOG_LEVEL', 'info'),
63 | ],
64 |
65 | /*
66 | |--------------------------------------------------------------------------
67 | | LDAP Cache
68 | |--------------------------------------------------------------------------
69 | |
70 | | LDAP caching enables the ability of caching search results using the
71 | | query builder. This is great for running expensive operations that
72 | | may take many seconds to complete, such as a pagination request.
73 | |
74 | */
75 |
76 | 'cache' => [
77 | 'enabled' => env('LDAP_CACHE', false),
78 | 'driver' => env('CACHE_DRIVER', 'file'),
79 | ],
80 |
81 | ];
82 |
--------------------------------------------------------------------------------
/database/migrations/2025_01_01_000000_add_ldap_columns_to_users_table.php:
--------------------------------------------------------------------------------
1 | getDriverName();
16 |
17 | Schema::table('users', function (Blueprint $table) use ($driver) {
18 | $table->string('guid')->nullable();
19 | $table->string('domain')->nullable();
20 |
21 | if ($driver !== 'sqlsrv') {
22 | $table->unique('guid');
23 | }
24 | });
25 |
26 | if ($driver === 'sqlsrv') {
27 | DB::statement(
28 | $this->compileUniqueSqlServerIndexStatement('users', 'guid')
29 | );
30 | }
31 | }
32 |
33 | /**
34 | * Reverse the migrations.
35 | */
36 | public function down(): void
37 | {
38 | Schema::table('users', function (Blueprint $table) {
39 | $table->dropColumn(['guid', 'domain']);
40 | });
41 | }
42 |
43 | /**
44 | * Compile a compatible "unique" SQL Server index constraint.
45 | */
46 | protected function compileUniqueSqlServerIndexStatement(string $table, string $column): string
47 | {
48 | return sprintf('create unique index %s on %s (%s) where %s is not null',
49 | implode('_', [$table, $column, 'unique']),
50 | $table,
51 | $column,
52 | $column
53 | );
54 | }
55 | };
56 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright © Steve Bauman
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 |
6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 |
8 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ./tests/
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Integrate LDAP into your Laravel application.
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
24 |
25 | ---
26 |
27 | ⭐️ **Developer Experience First**
28 |
29 | LdapRecord focuses on clean, easy to understand syntax along with thorough documentation.
30 |
31 | 🔑 **Authenticate LDAP Users**
32 |
33 | Allow LDAP users to log into your application and control which users can login via [Scopes](https://ldaprecord.com/docs/laravel/v3/usage/#scopes) and [Rules](https://ldaprecord.com/docs/laravel/v1/auth/configuration/#rules).
34 |
35 | 🔄 **Import & Synchronize LDAP users**
36 |
37 | Import users from your directory via [command](https://ldaprecord.com/docs/laravel/v3/auth/database/importing): `php artisan ldap:import`.
38 |
39 | 💼 **Multi-Domain Support**
40 |
41 | Authenticate users from as many LDAP domains as you'd like. Support comes [out of the box](https://ldaprecord.com/docs/laravel/v3/auth/multi-domain).
42 |
43 | 🎩 **Eloquent Query Builder**
44 |
45 | Search for LDAP objects with a [fluent and easy to use interface](https://ldaprecord.com/docs/core/v3/searching) you're used to. You'll feel right at home.
46 |
47 | ✏️ **Active Record LDAP Models**
48 |
49 | LDAP objects are [individual models](https://ldaprecord.com/docs/core/v3/models). Persist them to your LDAP server with a single `save()`.
50 |
51 | 💫 **LDAP Directory Emulator**
52 |
53 | Test [authenticating](https://ldaprecord.com/docs/laravel/v3/auth/testing/#getting-started) and
54 | [querying users](https://ldaprecord.com/docs/laravel/v3/testing/#getting-started) without
55 | changing your application code.
56 |
57 | Create, update, and delete LDAP objects without touching a real LDAP server.
58 |
59 | ---
60 |
61 | LdapRecord-Laravel is Supportware™
62 |
63 | If you require support using LdapRecord-Laravel, a sponsorship is required :pray:
64 |
65 | Thank you for your understanding :heart:
66 |
67 | ---
68 |
69 | Security Vulnerabilities
70 |
71 | If you discover a security vulnerability within LdapRecord-Laravel, please send an e-mail to Steve Bauman via steven_bauman@outlook.com.
72 |
73 | All security vulnerabilities will be promptly addressed.
74 |
--------------------------------------------------------------------------------
/resources/lang/en/errors.php:
--------------------------------------------------------------------------------
1 | 'User not found.',
5 | 'user_not_permitted_at_this_time' => 'Not permitted to logon at this time.',
6 | 'user_not_permitted_to_login' => 'Not permitted to logon at this workstation.',
7 | 'password_expired' => 'Your password has expired.',
8 | 'account_disabled' => 'Your account is disabled.',
9 | 'account_expired' => 'Your account has expired.',
10 | 'user_must_reset_password' => 'You must reset your password before logging in.',
11 | 'user_account_locked' => 'Your account is locked.',
12 | ];
13 |
--------------------------------------------------------------------------------
/src/Auth/AuthenticatesWithLdap.php:
--------------------------------------------------------------------------------
1 | listenForLdapBindFailure();
29 | }
30 | });
31 | }
32 |
33 | /**
34 | * Register the bind failure listener upon resolving the given class.
35 | */
36 | protected static function whenResolving(string $class, ?Closure $callback = null): void
37 | {
38 | if (! class_exists($class)) {
39 | return;
40 | }
41 |
42 | app()->resolving($class, $callback ?? function () {
43 | (new static)->listenForLdapBindFailure();
44 | });
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Auth/CreatesUserProvider.php:
--------------------------------------------------------------------------------
1 | $config) {
18 | if (Auth::guard($guard)->check()) {
19 | return $guard;
20 | }
21 | }
22 |
23 | return null;
24 | }
25 |
26 | /**
27 | * Get the guard's authentication user provider.
28 | */
29 | protected function getCurrentAuthProvider(string $guard): ?UserProvider
30 | {
31 | if ($guard === 'sanctum') {
32 | $guard = Arr::first(
33 | Arr::wrap(Config::get('sanctum.guard', 'web'))
34 | );
35 | }
36 |
37 | return Auth::createUserProvider(
38 | Config::get("auth.guards.$guard.provider")
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Auth/DatabaseUserProvider.php:
--------------------------------------------------------------------------------
1 | synchronizer = $synchronizer;
54 | $this->eloquent = $eloquent;
55 | }
56 |
57 | /**
58 | * Dynamically pass missing methods to the Eloquent user provider.
59 | */
60 | public function __call(string $method, array $parameters): mixed
61 | {
62 | return $this->forwardCallTo($this->eloquent, $method, $parameters);
63 | }
64 |
65 | /**
66 | * Get the Eloquent user provider.
67 | */
68 | public function eloquent(): EloquentUserProvider
69 | {
70 | return $this->eloquent;
71 | }
72 |
73 | /**
74 | * Get the LDAP user importer.
75 | */
76 | public function getLdapUserSynchronizer(): UserSynchronizer
77 | {
78 | return $this->synchronizer;
79 | }
80 |
81 | /**
82 | * Set the authenticating LDAP user.
83 | */
84 | public function setAuthenticatingUser(Model $user): void
85 | {
86 | $this->user = $user;
87 | }
88 |
89 | /**
90 | * Fallback to Eloquent authentication after failing to locate an LDAP user.
91 | */
92 | public function shouldFallback(): void
93 | {
94 | $this->fallback = true;
95 | }
96 |
97 | /**
98 | * Set the callback to be used to resolve LDAP users.
99 | */
100 | public function resolveUsersUsing(Closure $callback): static
101 | {
102 | $this->userResolver = $callback;
103 |
104 | return $this;
105 | }
106 |
107 | /**
108 | * {@inheritdoc}
109 | */
110 | public function retrieveById($identifier): ?Authenticatable
111 | {
112 | return $this->eloquent->retrieveById($identifier);
113 | }
114 |
115 | /**
116 | * {@inheritdoc}
117 | */
118 | public function retrieveByToken($identifier, $token): ?Authenticatable
119 | {
120 | return $this->eloquent->retrieveByToken($identifier, $token);
121 | }
122 |
123 | /**
124 | * {@inheritdoc}
125 | */
126 | public function updateRememberToken(Authenticatable $user, $token): void
127 | {
128 | $this->eloquent->updateRememberToken($user, $token);
129 | }
130 |
131 | /**
132 | * {@inheritdoc}
133 | */
134 | public function retrieveByCredentials(array $credentials): ?Authenticatable
135 | {
136 | $this->fallback = isset($credentials['fallback']);
137 |
138 | $fetch = function () use ($credentials) {
139 | return $this->fetchLdapUserByCredentials($credentials);
140 | };
141 |
142 | // If fallback is enabled, we want to make sure we catch
143 | // any exceptions that may occur so we do not interrupt
144 | // the fallback process and exit authentication early.
145 | $user = $this->fallback ? rescue($fetch) : value($fetch);
146 |
147 | // If the user was unable to be located and fallback is
148 | // enabled, we will attempt to fetch the database user
149 | // and perform validation on their password normally.
150 | if (! $user) {
151 | return $this->fallback
152 | ? $this->retrieveByCredentialsUsingEloquent($credentials['fallback'])
153 | : null;
154 | }
155 |
156 | $this->setAuthenticatingUser($user);
157 |
158 | return $this->synchronizer->run($user, $credentials);
159 | }
160 |
161 | /**
162 | * Retrieve the user from their credentials using Eloquent.
163 | *
164 | * @throws \Exception
165 | */
166 | protected function retrieveByCredentialsUsingEloquent(array $credentials): ?Authenticatable
167 | {
168 | return $this->eloquent->retrieveByCredentials($credentials);
169 | }
170 |
171 | /**
172 | * {@inheritdoc}
173 | */
174 | public function validateCredentials(Authenticatable $user, array $credentials): bool
175 | {
176 | // If an LDAP user has not been located, fallback is enabled, and
177 | // the given Eloquent model exists, we will attempt to validate
178 | // the users password normally via the Eloquent user provider.
179 | if (! $this->user instanceof Model) {
180 | return $this->fallback && $user->exists
181 | ? $this->eloquent->validateCredentials($user, $credentials)
182 | : false;
183 | }
184 |
185 | $this->auth->setEloquentModel($user);
186 |
187 | if (! $this->auth->attempt($this->user, $credentials['password'])) {
188 | return false;
189 | }
190 |
191 | $user->save();
192 |
193 | event(new Saved($this->user, $user));
194 |
195 | if ($user->wasRecentlyCreated) {
196 | event(new Imported($this->user, $user));
197 | }
198 |
199 | event(new Completed($this->user, $user));
200 |
201 | return true;
202 | }
203 |
204 | /**
205 | * {@inheritdoc}
206 | */
207 | public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false): void
208 | {
209 | if (($this->synchronizer->getConfig()['password_column'] ?? 'password') !== false) {
210 | $this->eloquent->rehashPasswordIfRequired($user, $credentials, $force);
211 | }
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/src/Auth/HasLdapUser.php:
--------------------------------------------------------------------------------
1 | getCurrentAuthGuard()) {
27 | return null;
28 | }
29 |
30 | if (! $provider = $this->getCurrentAuthProvider($guard)) {
31 | return null;
32 | }
33 |
34 | if (! $provider instanceof UserProvider) {
35 | return null;
36 | }
37 |
38 | if (! isset($this->ldapUserModel)) {
39 | $this->ldapUserModel = $provider->getLdapUserRepository()->findByModel($this);
40 | }
41 |
42 | return $this->ldapUserModel;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Auth/LdapAuthenticatable.php:
--------------------------------------------------------------------------------
1 | listen(Connecting::class, function (Connecting $event) use (&$isOnLastHost) {
42 | $connection = $event->getConnection();
43 |
44 | $numberOfHosts = count($connection->getConfiguration()->get('hosts'));
45 |
46 | $numberOfHostsAttempted = count($connection->attempted());
47 |
48 | $isOnLastHost = ($numberOfHosts - 1) - $numberOfHostsAttempted === 0;
49 | });
50 |
51 | $dispatcher->listen(BindFailed::class, function (BindFailed $event) use (&$isOnLastHost) {
52 | if (! $isOnLastHost) {
53 | return;
54 | }
55 |
56 | $this->ldapBindFailed(
57 | $event->getConnection()->getLastError(),
58 | $event->getConnection()->getDiagnosticMessage()
59 | );
60 | });
61 | }
62 |
63 | /**
64 | * Generate a human validation error for LDAP bind failures.
65 | *
66 | * @throws ValidationException
67 | */
68 | protected function ldapBindFailed(string $errorMessage, ?string $diagnosticMessage = null): void
69 | {
70 | switch (true) {
71 | case $this->causedByLostConnection($errorMessage):
72 | $this->handleLdapBindError($errorMessage);
73 |
74 | return;
75 |
76 | case is_null($diagnosticMessage):
77 | // If there is no diagnostic message to work with, we
78 | // cannot make any further attempts to determine
79 | // the error. We will bail here in such case.
80 | return;
81 |
82 | case $this->causedByInvalidCredentials($errorMessage, $diagnosticMessage):
83 | // We'll bypass any invalid LDAP credential errors and let
84 | // the login controller handle it. This is so proper
85 | // translation can be done on the validation error.
86 | return;
87 | default:
88 | foreach ($this->ldapDiagnosticCodeErrorMap() as $code => $message) {
89 | if ($this->errorContainsMessage($diagnosticMessage, (string) $code)) {
90 | $this->handleLdapBindError($message, $code);
91 | }
92 | }
93 | }
94 | }
95 |
96 | /**
97 | * Handle the LDAP bind error.
98 | *
99 | * @throws ValidationException
100 | */
101 | protected function handleLdapBindError(string $message, ?string $code = null): void
102 | {
103 | logger()->error($message, compact('code'));
104 |
105 | ($callback = static::$bindErrorHandler)
106 | ? $callback($message, $code)
107 | : $this->throwLoginValidationException($message);
108 | }
109 |
110 | /**
111 | * Throw a login validation exception.
112 | *
113 | * @throws ValidationException
114 | */
115 | protected function throwLoginValidationException(string $message): void
116 | {
117 | $username = 'email';
118 |
119 | if (class_exists($fortify = 'Laravel\Fortify\Fortify')) {
120 | $username = $fortify::username();
121 | } elseif (method_exists($this, 'username')) {
122 | $username = $this->username();
123 | } elseif (property_exists($this, 'username')) {
124 | $username = $this->username;
125 | }
126 |
127 | throw ValidationException::withMessages([
128 | $username => $message,
129 | ]);
130 | }
131 |
132 | /**
133 | * Determine if the LDAP error generated is caused by invalid credentials.
134 | */
135 | protected function causedByInvalidCredentials(string $errorMessage, string $diagnosticMessage): bool
136 | {
137 | return $this->errorContainsMessage($errorMessage, 'Invalid credentials')
138 | && $this->errorContainsMessage($diagnosticMessage, '52e');
139 | }
140 |
141 | /**
142 | * The LDAP diagnostic code error map.
143 | */
144 | protected function ldapDiagnosticCodeErrorMap(): array
145 | {
146 | return [
147 | '525' => trans('ldap::errors.user_not_found'),
148 | '530' => trans('ldap::errors.user_not_permitted_at_this_time'),
149 | '531' => trans('ldap::errors.user_not_permitted_to_login'),
150 | '532' => trans('ldap::errors.password_expired'),
151 | '533' => trans('ldap::errors.account_disabled'),
152 | '701' => trans('ldap::errors.account_expired'),
153 | '773' => trans('ldap::errors.user_must_reset_password'),
154 | '775' => trans('ldap::errors.user_account_locked'),
155 | ];
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/src/Auth/NoDatabaseUserProvider.php:
--------------------------------------------------------------------------------
1 | users->findByGuid($identifier);
17 | }
18 |
19 | /**
20 | * {@inheritdoc}
21 | */
22 | public function retrieveByToken($identifier, $token): void
23 | {
24 | // We can't retrieve LDAP users via remember
25 | // token, as we have nowhere to store them.
26 | }
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | public function updateRememberToken(Authenticatable $user, $token): void
32 | {
33 | // LDAP users cannot contain remember tokens.
34 | }
35 |
36 | /**
37 | * {@inheritdoc}
38 | */
39 | public function retrieveByCredentials(array $credentials): ?Model
40 | {
41 | return $this->fetchLdapUserByCredentials($credentials);
42 | }
43 |
44 | /**
45 | * {@inheritdoc}
46 | */
47 | public function validateCredentials(Authenticatable $user, array $credentials): bool
48 | {
49 | if (! $this->auth->attempt($user, $credentials['password'])) {
50 | return false;
51 | }
52 |
53 | event(new Completed($user));
54 |
55 | return true;
56 | }
57 |
58 | /**
59 | * {@inheritdoc}
60 | */
61 | public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false)
62 | {
63 | // We can't rehash LDAP users passwords.
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Auth/Rule.php:
--------------------------------------------------------------------------------
1 | exists;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Auth/UserProvider.php:
--------------------------------------------------------------------------------
1 | users = $users;
37 | $this->auth = $auth;
38 |
39 | $this->userResolver = function (array $credentials) {
40 | return $this->users->findByCredentials($credentials);
41 | };
42 | }
43 |
44 | /**
45 | * Set the callback to resolve users by.
46 | */
47 | public function resolveUsersUsing(Closure $callback): static
48 | {
49 | $this->userResolver = $callback;
50 |
51 | return $this;
52 | }
53 |
54 | /**
55 | * Attempt to retrieve the user by their credentials.
56 | *
57 | * @throws ValidationException
58 | */
59 | protected function fetchLdapUserByCredentials(array $credentials): ?Model
60 | {
61 | try {
62 | return call_user_func($this->userResolver, $credentials);
63 | } catch (Exception $e) {
64 | $this->handleException($e);
65 |
66 | return null;
67 | }
68 | }
69 |
70 | /**
71 | * Handle exceptions during user resolution.
72 | *
73 | * @throws ValidationException
74 | */
75 | protected function handleException(Exception $e): void
76 | {
77 | if ($e instanceof ValidationException) {
78 | throw $e;
79 | }
80 |
81 | if (! LdapRecord::failingQuietly()) {
82 | throw $e;
83 | }
84 |
85 | report($e);
86 | }
87 |
88 | /**
89 | * Set the LDAP user repository.
90 | */
91 | public function setLdapUserRepository(LdapUserRepository $users): void
92 | {
93 | $this->users = $users;
94 | }
95 |
96 | /**
97 | * Get the LDAP user repository.
98 | */
99 | public function getLdapUserRepository(): LdapUserRepository
100 | {
101 | return $this->users;
102 | }
103 |
104 | /**
105 | * Get the LDAP user authenticator.
106 | */
107 | public function getLdapUserAuthenticator(): LdapUserAuthenticator
108 | {
109 | return $this->auth;
110 | }
111 |
112 | /**
113 | * Set the LDAP user authenticator.
114 | */
115 | public function setLdapUserAuthenticator(LdapUserAuthenticator $auth): void
116 | {
117 | $this->auth = $auth;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/Auth/Validator.php:
--------------------------------------------------------------------------------
1 | addRule($rule);
26 | }
27 | }
28 |
29 | /**
30 | * Determine if all rules pass validation.
31 | */
32 | public function passes(LdapRecord $user, ?Eloquent $model = null): bool
33 | {
34 | foreach ($this->rules as $rule) {
35 | if (! $rule->passes($user, $model)) {
36 | event(new RuleFailed($rule, $user, $model));
37 |
38 | return false;
39 | }
40 |
41 | event(new RulePassed($rule, $user, $model));
42 | }
43 |
44 | return true;
45 | }
46 |
47 | /**
48 | * Adds a rule to the validator.
49 | */
50 | public function addRule(Rule $rule): void
51 | {
52 | $this->rules[] = $rule;
53 | }
54 |
55 | /**
56 | * Get the rules on the validator.
57 | *
58 | * @return Rule[]
59 | */
60 | public function getRules(): array
61 | {
62 | return $this->rules;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Commands/GetRootDse.php:
--------------------------------------------------------------------------------
1 | argument('connection') ?? Container::getDefaultConnectionName();
36 |
37 | $rootDse = Entry::getRootDse($connection);
38 |
39 | if ($selected = $this->option('attributes')) {
40 | $onlyAttributes = array_map('trim', explode(',', $selected));
41 | }
42 |
43 | $attributes = isset($onlyAttributes)
44 | ? Arr::only($rootDse->getAttributes(), $onlyAttributes)
45 | : $rootDse->getAttributes();
46 |
47 | if (! empty($attributes)) {
48 | foreach ($attributes as $attribute => $values) {
49 | $this->line("$attribute:>");
50 |
51 | array_map(function ($value) {
52 | $this->line(" $value");
53 | }, $values);
54 |
55 | $this->line('');
56 | }
57 |
58 | return static::SUCCESS;
59 | }
60 |
61 | if (isset($onlyAttributes)) {
62 | $this->error(
63 | sprintf('Attributes [%s] were not found in the Root DSE record.', implode(', ', $onlyAttributes))
64 | );
65 | } else {
66 | $this->error('No attributes were returned from the Root DSE query.');
67 | }
68 |
69 | return static::FAILURE;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Commands/MakeLdapModel.php:
--------------------------------------------------------------------------------
1 | argument('connection')) {
35 | $connections = [$connection => Container::getConnection($connection)];
36 | } else {
37 | $connections = Container::getInstance()->getConnections();
38 | }
39 |
40 | if (empty($connections)) {
41 | $this->error('No LDAP connections have been defined.');
42 |
43 | return static::INVALID;
44 | }
45 |
46 | $tested = [];
47 | $connected = [];
48 |
49 | foreach ($connections as $name => $connection) {
50 | [,$connected[]] = $tested[] = $this->performTest($name, $connection);
51 | }
52 |
53 | $this->table(['Connection', 'Successful', 'Username', 'Message', 'Response Time'], $tested);
54 |
55 | return in_array('✘ No', $connected) ? static::FAILURE : static::SUCCESS;
56 | }
57 |
58 | /**
59 | * Perform a connectivity test on the given connection.
60 | */
61 | protected function performTest(string $name, Connection $connection): array
62 | {
63 | $this->info("Testing LDAP connection [$name]...");
64 |
65 | $start = microtime(true);
66 |
67 | $message = $this->attempt($connection);
68 |
69 | return [
70 | $name,
71 | $connection->isConnected() ? '✔ Yes' : '✘ No',
72 | $connection->getConfiguration()->get('username'),
73 | $message,
74 | (app()->runningUnitTests() ? '0' : $this->getElapsedTime($start)).'ms',
75 | ];
76 | }
77 |
78 | /**
79 | * Attempt establishing the connection.
80 | */
81 | protected function attempt(Connection $connection): string
82 | {
83 | try {
84 | $connection->connect();
85 |
86 | $message = 'Successfully connected.';
87 | } catch (BindException $e) {
88 | $detailedError = optional($e->getDetailedError());
89 |
90 | $errorCode = $detailedError->getErrorCode();
91 | $diagnosticMessage = $detailedError->getDiagnosticMessage();
92 |
93 | $message = sprintf(
94 | '%s. Error Code: [%s] Diagnostic Message: %s',
95 | $e->getMessage(),
96 | $errorCode ?? 'NULL',
97 | $diagnosticMessage ?? 'NULL'
98 | );
99 | } catch (Exception $e) {
100 | $message = sprintf(
101 | '%s. Error Code: [%s]',
102 | $e->getMessage(),
103 | $e->getCode()
104 | );
105 | }
106 |
107 | return $message;
108 | }
109 |
110 | /**
111 | * Get the elapsed time since a given starting point.
112 | */
113 | protected function getElapsedTime(int $start): int|float
114 | {
115 | return round((microtime(true) - $start) * 1000, 2);
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/Commands/stubs/model.stub:
--------------------------------------------------------------------------------
1 | object->getName()}] has failed LDAP authentication.";
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Events/Auth/Binding.php:
--------------------------------------------------------------------------------
1 | object->getName()}] is authenticating.";
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Events/Auth/Bound.php:
--------------------------------------------------------------------------------
1 | object->getName()}] has successfully passed LDAP authentication.";
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Events/Auth/Completed.php:
--------------------------------------------------------------------------------
1 | object->getName()}] has successfully authenticated.";
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Events/Auth/CompletedWithWindows.php:
--------------------------------------------------------------------------------
1 | object->getName()}] has successfully authenticated via NTLM.";
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Events/Auth/DiscoveredWithCredentials.php:
--------------------------------------------------------------------------------
1 | user = $user;
24 | }
25 |
26 | /**
27 | * {@inheritdoc}
28 | */
29 | public function getLogMessage(): string
30 | {
31 | return "User [{$this->user->getName()}] has been successfully discovered for authentication.";
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Events/Auth/EloquentUserTrashed.php:
--------------------------------------------------------------------------------
1 | object->getName()}] was denied authentication because their model is soft-deleted.";
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Events/Auth/Event.php:
--------------------------------------------------------------------------------
1 | object = $object;
26 | $this->eloquent = $eloquent;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Events/Auth/Rejected.php:
--------------------------------------------------------------------------------
1 | object->getName()}] has failed validation. They have been denied authentication.";
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Events/Auth/RuleEvent.php:
--------------------------------------------------------------------------------
1 | rule = $rule;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Events/Auth/RuleFailed.php:
--------------------------------------------------------------------------------
1 | objects = $objects;
31 | $this->imported = $imported;
32 | }
33 |
34 | /**
35 | * {@inheritdoc}
36 | */
37 | public function getLogMessage(): string
38 | {
39 | $imported = $this->imported->filter(function (Model $eloquent) {
40 | return $eloquent->wasRecentlyCreated;
41 | })->count();
42 |
43 | $synchronized = $this->imported->count() - $imported;
44 |
45 | return "Completed import. Imported [$imported] new LDAP objects. Synchronized [$synchronized] existing LDAP objects.";
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Events/Import/Deleted.php:
--------------------------------------------------------------------------------
1 | ldapModel = $ldapModel;
36 | $this->eloquentModel = $eloquentModel;
37 | $this->deleted = $deleted;
38 | }
39 |
40 | /**
41 | * {@inheritdoc}
42 | */
43 | public function getLogMessage(): string
44 | {
45 | $guids = $this->deleted->values()->implode(', ');
46 |
47 | return "Users with guids [$guids] have been soft-deleted due to being missing from LDAP result.";
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Events/Import/Event.php:
--------------------------------------------------------------------------------
1 | object = $object;
26 | $this->eloquent = $eloquent;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Events/Import/ImportFailed.php:
--------------------------------------------------------------------------------
1 | exception = $exception;
28 | }
29 |
30 | /**
31 | * {@inheritdoc}
32 | */
33 | public function getLogLevel(): string
34 | {
35 | return 'error';
36 | }
37 |
38 | /**
39 | * {@inheritdoc}
40 | */
41 | public function getLogMessage(): string
42 | {
43 | return "Failed importing object [{$this->object->getName()}]. {$this->exception->getMessage()}";
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Events/Import/Imported.php:
--------------------------------------------------------------------------------
1 | object->getName()}] has been successfully imported.";
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Events/Import/Importing.php:
--------------------------------------------------------------------------------
1 | object->getName()}] is being imported.";
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Events/Import/Restored.php:
--------------------------------------------------------------------------------
1 | objects = $objects;
24 | }
25 |
26 | /**
27 | * {@inheritdoc}
28 | */
29 | public function getLogMessage(): string
30 | {
31 | return "Starting import of [{$this->objects->count()}] LDAP objects.";
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Events/Import/Synchronized.php:
--------------------------------------------------------------------------------
1 | object->getName()}] has been successfully synchronized.";
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Events/Import/Synchronizing.php:
--------------------------------------------------------------------------------
1 | object->getName()}] is being synchronized.";
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Events/Loggable.php:
--------------------------------------------------------------------------------
1 | config = $config;
35 | }
36 |
37 | /**
38 | * Extra data to pass to each hydrator.
39 | */
40 | public function with(array $data = []): static
41 | {
42 | $this->data = $data;
43 |
44 | return $this;
45 | }
46 |
47 | /**
48 | * Hydrate the database model with the LDAP user.
49 | */
50 | public function hydrate(LdapModel $user, EloquentModel $database): void
51 | {
52 | foreach ($this->hydrators as $hydrator) {
53 | $hydrator::with($this->config, $this->data)->hydrate($user, $database);
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Import/EloquentUserHydrator.php:
--------------------------------------------------------------------------------
1 | getSyncAttributes() as $eloquentField => $ldapField) {
18 | if (! $this->isAttributeHandler($ldapField)) {
19 | $eloquent->{$eloquentField} = is_string($ldapField)
20 | ? $object->getFirstAttribute($ldapField)
21 | : $ldapField;
22 |
23 | continue;
24 | }
25 |
26 | if ($ldapField instanceof Closure) {
27 | $ldapField($object, $eloquent);
28 |
29 | continue;
30 | }
31 |
32 | if (is_callable($handler = app($ldapField))) {
33 | $handler($object, $eloquent);
34 |
35 | continue;
36 | }
37 |
38 | $handler->handle($object, $eloquent);
39 | }
40 | }
41 |
42 | /**
43 | * Get the database sync attributes.
44 | */
45 | protected function getSyncAttributes(): array
46 | {
47 | return (array) Arr::get(
48 | $this->config,
49 | 'sync_attributes',
50 | ['name' => 'cn', 'email' => 'mail']
51 | );
52 | }
53 |
54 | /**
55 | * Determines if the given value is an attribute handler.
56 | */
57 | protected function isAttributeHandler(mixed $value): bool
58 | {
59 | if ($value instanceof Closure) {
60 | return true;
61 | }
62 |
63 | return class_exists($value) && (method_exists($value, '__invoke') || method_exists($value, 'handle'));
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Import/Hydrators/DomainHydrator.php:
--------------------------------------------------------------------------------
1 | setLdapDomain($object->getConnectionName() ?? Config::get('ldap.default'));
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Import/Hydrators/GuidHydrator.php:
--------------------------------------------------------------------------------
1 | setLdapGuid($object->getConvertedGuid());
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Import/Hydrators/Hydrator.php:
--------------------------------------------------------------------------------
1 | config = $config;
26 | $this->data = $data;
27 | }
28 |
29 | /**
30 | * Create a new hydrator instance.
31 | */
32 | public static function with(array $config = [], array $data = []): static
33 | {
34 | return new static($config, $data);
35 | }
36 |
37 | /**
38 | * Hydrate the database model with the LDAP user.
39 | */
40 | abstract public function hydrate(LdapModel $object, EloquentModel $eloquent): void;
41 | }
42 |
--------------------------------------------------------------------------------
/src/Import/Hydrators/PasswordHydrator.php:
--------------------------------------------------------------------------------
1 | hasPasswordColumn()) {
19 | return;
20 | }
21 |
22 | $password = $this->getPassword() ?? Str::random();
23 |
24 | if (! $this->isSyncingPasswords()) {
25 | $password = Str::random();
26 | }
27 |
28 | $column = method_exists($eloquent, 'getAuthPasswordName')
29 | ? $eloquent->getAuthPasswordName()
30 | : $this->getPasswordColumn();
31 |
32 | if ($this->passwordNeedsUpdate($eloquent, $column, $password)) {
33 | $this->setPassword($eloquent, $column, $password);
34 | }
35 | }
36 |
37 | /**
38 | * Set the password on the users model.
39 | */
40 | protected function setPassword(EloquentModel $model, string $column, string $password): void
41 | {
42 | // If the model has a mutator for the password field, we
43 | // can assume hashing passwords is taken care of.
44 | // Otherwise, we will hash it normally.
45 | $password = $model->hasSetMutator($column)
46 | ? $password
47 | : Hash::make($password);
48 |
49 | $model->setAttribute($column, $password);
50 | }
51 |
52 | /**
53 | * Determine if the current model requires a password update.
54 | *
55 | * This checks if the model does not currently have a
56 | * password, or if the password fails a hash check.
57 | */
58 | protected function passwordNeedsUpdate(EloquentModel $model, string $column, ?string $password = null): bool
59 | {
60 | $current = $this->getCurrentModelPassword($model, $column);
61 |
62 | // If the application is running in console, we will assume the
63 | // import command is being run. In this case, we do not want
64 | // to overwrite a password that's already properly hashed.
65 | if (app()->runningInConsole() && ! Hash::needsRehash((string) $current)) {
66 | return false;
67 | }
68 |
69 | // If the eloquent model contains a password and password sync is
70 | // enabled, we will check the integrity of the given password
71 | // against it to determine if it should be updated.
72 | if (! is_null($current) && $this->isSyncingPasswords()) {
73 | return ! Hash::check($password, $current);
74 | }
75 |
76 | return is_null($current);
77 | }
78 |
79 | /**
80 | * Determines if the developer has configured a password column.
81 | */
82 | protected function hasPasswordColumn(): bool
83 | {
84 | return $this->getPasswordColumn() !== false;
85 | }
86 |
87 | /**
88 | * Get the current models hashed password.
89 | */
90 | protected function getCurrentModelPassword(EloquentModel $model, string $column): ?string
91 | {
92 | return $model->getAttribute($column);
93 | }
94 |
95 | /**
96 | * Get the password from the current data.
97 | */
98 | protected function getPassword(): ?string
99 | {
100 | return Arr::get($this->data, 'password');
101 | }
102 |
103 | /**
104 | * Get the configured database password column to use.
105 | *
106 | * @return string|false
107 | */
108 | protected function getPasswordColumn(): bool|string
109 | {
110 | return Arr::get($this->config, 'password_column', 'password');
111 | }
112 |
113 | /**
114 | * Determine whether password sync is enabled.
115 | */
116 | protected function isSyncingPasswords(): bool
117 | {
118 | return Arr::get($this->config, 'sync_passwords', false);
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/Import/ImportException.php:
--------------------------------------------------------------------------------
1 | getGuidKey(),
19 | get_class($model),
20 | $model->getDn()
21 | )
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Import/LdapUserImporter.php:
--------------------------------------------------------------------------------
1 | object instanceof ActiveDirectory) {
43 | return;
44 | }
45 |
46 | if (! $this->isUsingSoftDeletes($event->eloquent)) {
47 | return;
48 | }
49 |
50 | if ($this->trashDisabledUsers) {
51 | $this->delete($event->object, $event->eloquent);
52 | }
53 |
54 | if ($this->restoreEnabledUsers) {
55 | $this->restore($event->object, $event->eloquent);
56 | }
57 | });
58 | }
59 |
60 | /**
61 | * Set the LDAP user repository to use for importing.
62 | */
63 | public function setLdapUserRepository(LdapUserRepository $repository): static
64 | {
65 | $this->repository = $repository;
66 |
67 | return $this;
68 | }
69 |
70 | /**
71 | * Enable restoring enabled users.
72 | */
73 | public function restoreEnabledUsers(): static
74 | {
75 | $this->restoreEnabledUsers = true;
76 |
77 | return $this;
78 | }
79 |
80 | /**
81 | * Enable trashing disabled users.
82 | */
83 | public function trashDisabledUsers(): static
84 | {
85 | $this->trashDisabledUsers = true;
86 |
87 | return $this;
88 | }
89 |
90 | /**
91 | * Load the import's objects from the LDAP repository.
92 | */
93 | public function loadObjectsFromRepository(?string $username = null): Collection
94 | {
95 | $query = $this->applyLdapQueryConstraints(
96 | $this->repository->query()
97 | );
98 |
99 | if (! $username) {
100 | return $this->objects = $query->paginate();
101 | }
102 |
103 | $users = $query->getModel()->newCollection();
104 |
105 | return $this->objects = ($user = $query->findByAnr($username))
106 | ? $users->add($user)
107 | : $users;
108 | }
109 |
110 | /**
111 | * Load the import's objects from the LDAP repository via chunking.
112 | */
113 | public function chunkObjectsFromRepository(Closure $callback, int $perChunk = 500): void
114 | {
115 | $query = $this->applyLdapQueryConstraints(
116 | $this->repository->query()
117 | );
118 |
119 | $query->chunk($perChunk, function ($objects) use ($callback) {
120 | $callback($this->objects = $objects);
121 | });
122 | }
123 |
124 | /**
125 | * Soft deletes the specified model if their LDAP account is disabled.
126 | */
127 | protected function delete(LdapRecord $object, Eloquent $eloquent): void
128 | {
129 | if ($eloquent->trashed()) {
130 | return;
131 | }
132 |
133 | if (! $this->userIsDisabled($object)) {
134 | return;
135 | }
136 |
137 | $eloquent->delete();
138 |
139 | event(new Deleted($object, $eloquent));
140 | }
141 |
142 | /**
143 | * Restores soft-deleted models if their LDAP account is enabled.
144 | */
145 | protected function restore(LdapRecord $object, Eloquent $eloquent): void
146 | {
147 | if (! $eloquent->trashed()) {
148 | return;
149 | }
150 |
151 | if (! $this->userIsEnabled($object)) {
152 | return;
153 | }
154 |
155 | $eloquent->restore();
156 |
157 | event(new Restored($object, $eloquent));
158 | }
159 |
160 | /**
161 | * Determine whether the user is enabled.
162 | */
163 | protected function userIsEnabled(LdapRecord $object): bool
164 | {
165 | return $this->getUserAccountControl($object) === null ? false : ! $this->userIsDisabled($object);
166 | }
167 |
168 | /**
169 | * Determines whether the user is disabled.
170 | */
171 | protected function userIsDisabled(LdapRecord $object): bool
172 | {
173 | return ($this->getUserAccountControl($object) & AccountControl::ACCOUNTDISABLE) === AccountControl::ACCOUNTDISABLE;
174 | }
175 |
176 | /**
177 | * Get the user account control integer from the user.
178 | */
179 | protected function getUserAccountControl(LdapRecord $object): ?int
180 | {
181 | return $object->getFirstAttribute('userAccountControl');
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/src/Import/UserSynchronizer.php:
--------------------------------------------------------------------------------
1 | {$this->getLdapDomainColumn()};
21 | }
22 |
23 | /**
24 | * Set the models LDAP domain.
25 | */
26 | public function setLdapDomain(?string $domain): void
27 | {
28 | $this->{$this->getLdapDomainColumn()} = $domain;
29 | }
30 |
31 | /**
32 | * Get the models LDAP GUID database column name.
33 | */
34 | public function getLdapGuidColumn(): string
35 | {
36 | return 'guid';
37 | }
38 |
39 | /**
40 | * Get the models LDAP GUID.
41 | */
42 | public function getLdapGuid(): ?string
43 | {
44 | return $this->{$this->getLdapGuidColumn()};
45 | }
46 |
47 | /**
48 | * Set the models LDAP GUID.
49 | */
50 | public function setLdapGuid(?string $guid): void
51 | {
52 | $this->{$this->getLdapGuidColumn()} = $guid;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/LdapAuthServiceProvider.php:
--------------------------------------------------------------------------------
1 | loadTranslationsFrom(__DIR__.'/../resources/lang/', 'ldap');
22 |
23 | $this->registerCommands();
24 | $this->registerMigrations();
25 | $this->registerAuthProvider();
26 | $this->registerLoginControllerListeners();
27 | }
28 |
29 | /**
30 | * Register the LDAP auth commands.
31 | */
32 | protected function registerCommands(): void
33 | {
34 | $this->commands([ImportLdapUsers::class]);
35 | }
36 |
37 | /**
38 | * Register the LDAP auth migrations.
39 | */
40 | protected function registerMigrations(): void
41 | {
42 | if (! $this->app->runningInConsole()) {
43 | return;
44 | }
45 |
46 | $this->publishes([
47 | __DIR__.'/../database/migrations' => database_path('migrations'),
48 | ], 'migrations');
49 | }
50 |
51 | /**
52 | * Register the LDAP auth provider.
53 | */
54 | protected function registerAuthProvider(): void
55 | {
56 | Auth::provider('ldap', function ($app, array $config) {
57 | return array_key_exists('database', $config)
58 | ? $this->makeDatabaseUserProvider($config)
59 | : $this->makePlainUserProvider($config);
60 | });
61 | }
62 |
63 | /**
64 | * Registers the login controller listener to handle LDAP errors.
65 | */
66 | protected function registerLoginControllerListeners(): void
67 | {
68 | BindFailureListener::usingLaravelUi();
69 | BindFailureListener::usingLaravelJetstream();
70 | }
71 |
72 | /**
73 | * Get a new database user provider.
74 | */
75 | protected function makeDatabaseUserProvider(array $config): DatabaseUserProvider
76 | {
77 | return app(DatabaseUserProvider::class, [
78 | 'users' => $this->makeLdapUserRepository($config),
79 | 'auth' => $this->makeLdapUserAuthenticator($config),
80 | 'eloquent' => $this->makeEloquentUserProvider($config),
81 | 'synchronizer' => $this->makeLdapUserSynchronizer($config['database']),
82 | ]);
83 | }
84 |
85 | /**
86 | * Make a new plain LDAP user provider.
87 | */
88 | protected function makePlainUserProvider(array $config): NoDatabaseUserProvider
89 | {
90 | return app(NoDatabaseUserProvider::class, [
91 | 'users' => $this->makeLdapUserRepository($config),
92 | 'auth' => $this->makeLdapUserAuthenticator($config),
93 | ]);
94 | }
95 |
96 | /**
97 | * Make a new Eloquent user provider.
98 | */
99 | protected function makeEloquentUserProvider(array $config): EloquentUserProvider
100 | {
101 | return app(EloquentUserProvider::class, [
102 | 'hasher' => $this->app->make('hash'),
103 | 'model' => $config['database']['model'],
104 | ]);
105 | }
106 |
107 | /**
108 | * Make a new LDAP user authenticator.
109 | */
110 | protected function makeLdapUserAuthenticator(array $config): LdapUserAuthenticator
111 | {
112 | return app(LdapUserAuthenticator::class, ['rules' => $config['rules'] ?? []]);
113 | }
114 |
115 | /**
116 | * Make a new LDAP user repository.
117 | */
118 | protected function makeLdapUserRepository(array $config): LdapUserRepository
119 | {
120 | return app(LdapUserRepository::class, [
121 | 'model' => $config['model'],
122 | 'scopes' => $config['scopes'] ?? [],
123 | ]);
124 | }
125 |
126 | /**
127 | * Make a new LDAP user importer.
128 | */
129 | protected function makeLdapUserSynchronizer(array $config): UserSynchronizer
130 | {
131 | return app(UserSynchronizer::class, [
132 | 'eloquentModel' => $config['model'],
133 | 'config' => $config,
134 | ]);
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/LdapImportable.php:
--------------------------------------------------------------------------------
1 | bind(LdapUserAuthenticator::class, $class);
37 | }
38 |
39 | /**
40 | * Register a class that should be used for locating LDAP users.
41 | */
42 | public static function locateUsersUsing(Closure|string $class): void
43 | {
44 | app()->bind(LdapUserRepository::class, $class);
45 | }
46 |
47 | /**
48 | * Register a class that should be used for synchronizing LDAP users.
49 | */
50 | public static function synchronizeUsersUsing(Closure|string $class): void
51 | {
52 | app()->bind(UserSynchronizer::class, $class);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/LdapUserAuthenticator.php:
--------------------------------------------------------------------------------
1 | rules = $rules;
43 |
44 | $this->authenticator = function (Model $user, $password) {
45 | return $user->getConnection()->auth()->attempt($user->getDn(), $password);
46 | };
47 | }
48 |
49 | /**
50 | * Set the authenticating eloquent model.
51 | */
52 | public function setEloquentModel(?Eloquent $model = null): static
53 | {
54 | $this->eloquentModel = $model;
55 |
56 | return $this;
57 | }
58 |
59 | /**
60 | * Attempt authenticating against the LDAP domain.
61 | */
62 | public function attempt(Model $user, ?string $password = null): bool
63 | {
64 | $this->attempting($user);
65 |
66 | if ($this->databaseModelIsTrashed()) {
67 | $this->trashed($user);
68 |
69 | return false;
70 | }
71 |
72 | // Here we will attempt to bind the authenticating LDAP
73 | // user to our connection to ensure their password is
74 | // correct, using the defined authenticator closure.
75 | if (! call_user_func($this->authenticator, $user, $password)) {
76 | $this->failed($user);
77 |
78 | return false;
79 | }
80 |
81 | // Now we will perform authorization on the LDAP user. If all
82 | // validation rules pass, we will allow the authentication
83 | // attempt. Otherwise, it is automatically rejected.
84 | if (! $this->validate($user)) {
85 | $this->rejected($user);
86 |
87 | return false;
88 | }
89 |
90 | $this->passed($user);
91 |
92 | return true;
93 | }
94 |
95 | /**
96 | * Attempt authentication using the given callback once.
97 | */
98 | public function attemptOnceUsing(Closure $callback, Model $user, ?string $password = null): bool
99 | {
100 | $authenticator = $this->authenticator;
101 |
102 | $result = $this->authenticateUsing($callback)->attempt($user, $password);
103 |
104 | $this->authenticator = $authenticator;
105 |
106 | return $result;
107 | }
108 |
109 | /**
110 | * Set the callback to use for authenticating users.
111 | */
112 | public function authenticateUsing(Closure $authenticator): static
113 | {
114 | $this->authenticator = $authenticator;
115 |
116 | return $this;
117 | }
118 |
119 | /**
120 | * Validate the given user against the authentication rules.
121 | */
122 | protected function validate(Model $user): bool
123 | {
124 | return $this->validator()->passes($user, $this->eloquentModel);
125 | }
126 |
127 | /**
128 | * Create a new user validator.
129 | */
130 | protected function validator(): Validator
131 | {
132 | return app(Validator::class, ['rules' => $this->rules()]);
133 | }
134 |
135 | /**
136 | * Get the authentication rules for the domain.
137 | */
138 | protected function rules(): Collection
139 | {
140 | return collect($this->rules)->map(fn ($rule) => app($rule))->values();
141 | }
142 |
143 | /**
144 | * Fire the "attempting" event.
145 | */
146 | protected function attempting(Model $user): void
147 | {
148 | event(new Binding($user, $this->eloquentModel));
149 | }
150 |
151 | /**
152 | * Fire the "passed" event.
153 | */
154 | protected function passed(Model $user): void
155 | {
156 | event(new Bound($user, $this->eloquentModel));
157 | }
158 |
159 | /**
160 | * Fire the "trashed" event.
161 | */
162 | protected function trashed(Model $user): void
163 | {
164 | event(new EloquentUserTrashed($user, $this->eloquentModel));
165 | }
166 |
167 | /**
168 | * Fire the "failed" event.
169 | */
170 | protected function failed(Model $user): void
171 | {
172 | event(new BindFailed($user, $this->eloquentModel));
173 | }
174 |
175 | /**
176 | * Fire the "rejected" event.
177 | */
178 | protected function rejected(Model $user): void
179 | {
180 | event(new Rejected($user, $this->eloquentModel));
181 | }
182 |
183 | /**
184 | * Determine if the database model is trashed.
185 | */
186 | protected function databaseModelIsTrashed(): bool
187 | {
188 | return isset($this->eloquentModel)
189 | && $this->isUsingSoftDeletes($this->eloquentModel)
190 | && $this->eloquentModel->trashed();
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/src/LdapUserRepository.php:
--------------------------------------------------------------------------------
1 | model = $model;
37 | $this->scopes = $scopes;
38 | }
39 |
40 | /**
41 | * Get a user by the attribute and value.
42 | */
43 | public function findBy(string $attribute, mixed $value): ?Model
44 | {
45 | return $this->query()->findBy($attribute, $value);
46 | }
47 |
48 | /**
49 | * Get an LDAP user by their eloquent model.
50 | */
51 | public function findByModel(LdapAuthenticatable $model): ?Model
52 | {
53 | if (empty($guid = $model->getLdapGuid())) {
54 | return null;
55 | }
56 |
57 | return $this->findByGuid($guid);
58 | }
59 |
60 | /**
61 | * Get a user by their object GUID.
62 | */
63 | public function findByGuid(string $guid): ?Model
64 | {
65 | return $this->query()->findByGuid($guid);
66 | }
67 |
68 | /**
69 | * Retrieve a user by the given credentials.
70 | */
71 | public function findByCredentials(array $credentials = []): ?Model
72 | {
73 | if (empty($credentials)) {
74 | return null;
75 | }
76 |
77 | $query = $this->query();
78 |
79 | foreach ($credentials as $key => $value) {
80 | if (Str::contains($key, $this->bypassCredentialKeys)) {
81 | continue;
82 | }
83 |
84 | if (is_array($value) || $value instanceof Arrayable) {
85 | $query->whereIn($key, $value);
86 | } else {
87 | $query->where($key, $value);
88 | }
89 | }
90 |
91 | if (is_null($user = $query->first())) {
92 | return null;
93 | }
94 |
95 | event(new DiscoveredWithCredentials($user));
96 |
97 | return $user;
98 | }
99 |
100 | /**
101 | * Get a new model query.
102 | */
103 | public function query(): Builder
104 | {
105 | $this->applyScopes(
106 | $query = $this->newModelQuery()
107 | );
108 |
109 | return $query;
110 | }
111 |
112 | /**
113 | * Set the scopes to apply on the query.
114 | */
115 | public function setScopes(array $scopes): static
116 | {
117 | $this->scopes = $scopes;
118 |
119 | return $this;
120 | }
121 |
122 | /**
123 | * Apply the configured scopes to the query.
124 | */
125 | protected function applyScopes(Builder $query): void
126 | {
127 | foreach ($this->scopes as $identifier => $scope) {
128 | match (true) {
129 | is_string($scope) => $query->withGlobalScope($scope, app($scope)),
130 | is_callable($scope) => $query->withGlobalScope($identifier, $scope),
131 | is_object($scope) => $query->withGlobalScope(get_class($scope), $scope),
132 | };
133 | }
134 | }
135 |
136 | /**
137 | * Create a new instance of the LdapRecord model.
138 | */
139 | public function createModel(): Model
140 | {
141 | $class = '\\'.ltrim($this->model, '\\');
142 |
143 | return new $class;
144 | }
145 |
146 | /**
147 | * Gets the name of the LdapRecord user model.
148 | *
149 | * @return class-string
150 | */
151 | public function getModel(): string
152 | {
153 | return $this->model;
154 | }
155 |
156 | /**
157 | * Get a new query builder for the model instance.
158 | */
159 | protected function newModelQuery(?Model $model = null): Builder
160 | {
161 | $model = is_null($model) ? $this->createModel() : $model;
162 |
163 | $query = $model->newQuery();
164 |
165 | // We will ensure our object GUID attribute is always selected
166 | // along will all attributes. Otherwise, if the object GUID
167 | // attribute is virtual, it may not be returned in results.
168 | $selects = array_merge(['*', $model->getGuidKey()], $query->getSelects());
169 |
170 | return $query->select(array_unique($selects));
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/Middleware/UserDomainValidator.php:
--------------------------------------------------------------------------------
1 | getDomainComponents($user->getDn())) {
20 | return false;
21 | }
22 |
23 | return $this->domainExistsInComponents($domain, $components);
24 | }
25 |
26 | /**
27 | * Get the domain components from the Distinguished Name.
28 | */
29 | protected function getDomainComponents(string $dn): array
30 | {
31 | return DistinguishedName::build($dn)->components('dc');
32 | }
33 |
34 | /**
35 | * Determine if the domain exists in the given components.
36 | */
37 | protected function domainExistsInComponents(string $domain, array $components): bool
38 | {
39 | return collect($components)->map(function ($component) {
40 | [,$value] = $component;
41 |
42 | return strtolower($value);
43 | })->contains(strtolower($domain));
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Testing/DirectoryEmulator.php:
--------------------------------------------------------------------------------
1 | name($name);
21 |
22 | app(LdapDatabaseManager::class)->connection($name, $config);
23 | });
24 | }
25 |
26 | /**
27 | * Make a fake connection.
28 | */
29 | public static function makeConnectionFake(array $config = []): EmulatedConnectionFake
30 | {
31 | return EmulatedConnectionFake::make($config)->shouldBeConnected();
32 | }
33 |
34 | /**
35 | * Tear down the fake directory.
36 | */
37 | public static function tearDown(): void
38 | {
39 | app(LdapDatabaseManager::class)->teardown();
40 |
41 | parent::tearDown();
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Testing/Emulated/ActiveDirectoryBuilder.php:
--------------------------------------------------------------------------------
1 | connection))
24 | ->setModel($this->model)
25 | ->setBaseDn($baseDn);
26 | }
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | public function add(string $dn, array $attributes): bool
32 | {
33 | if (! $model = $this->find($dn)) {
34 | return false;
35 | }
36 |
37 | foreach ($attributes as $name => $values) {
38 | $model->{$name} = array_merge($model->{$name} ?? [], Arr::wrap($values));
39 | }
40 |
41 | $model->save();
42 |
43 | return true;
44 | }
45 |
46 | /**
47 | * {@inheritdoc}
48 | */
49 | public function update(string $dn, array $modifications): bool
50 | {
51 | if (! $model = $this->findEloquentModelByDn($dn)) {
52 | return false;
53 | }
54 |
55 | foreach ($modifications as $modification) {
56 | $this->applyBatchModificationToModel($model, $modification);
57 | }
58 |
59 | return true;
60 | }
61 |
62 | /**
63 | * {@inheritdoc}
64 | */
65 | public function replace(string $dn, array $attributes): bool
66 | {
67 | if (! $model = $this->find($dn)) {
68 | return false;
69 | }
70 |
71 | foreach ($attributes as $name => $values) {
72 | $model->{$name} = $values;
73 | }
74 |
75 | $model->save();
76 |
77 | return true;
78 | }
79 |
80 | /**
81 | * {@inheritdoc}
82 | */
83 | public function remove(string $dn, array $attributes): bool
84 | {
85 | if (! $model = $this->find($dn)) {
86 | return false;
87 | }
88 |
89 | foreach ($attributes as $attribute => $value) {
90 | if (empty($value)) {
91 | $model->{$attribute} = null;
92 | } elseif ($model->hasAttribute($attribute)) {
93 | $model->{$attribute} = array_values(
94 | array_diff($model->{$attribute}, (array) $value)
95 | );
96 | }
97 | }
98 |
99 | $model->save();
100 |
101 | return true;
102 | }
103 |
104 | /**
105 | * Parse the database query results.
106 | *
107 | * @param \Illuminate\Database\Eloquent\Collection $resource
108 | */
109 | public function parse(mixed $resource): array
110 | {
111 | return $resource->toArray();
112 | }
113 |
114 | /**
115 | * {@inheritdoc}
116 | */
117 | protected function process(array $results): Collection
118 | {
119 | return $this->model->newCollection($results)->transform(function ($result) {
120 | return $this->resultToModelInstance($result);
121 | });
122 | }
123 |
124 | /**
125 | * Transform the result into a model instance.
126 | */
127 | protected function resultToModelInstance($result): Model
128 | {
129 | if ($result instanceof $this->model) {
130 | return $result;
131 | }
132 |
133 | return $this->model->newInstance()
134 | ->setDn($result['dn'])
135 | ->setRawAttributes(
136 | array_merge(
137 | $this->transform($result),
138 | [$result['guid_key'] => [$result['guid']]]
139 | )
140 | );
141 | }
142 |
143 | /**
144 | * {@inheritdoc}
145 | */
146 | public function findOrFail(string $dn, array|string $columns = ['*']): Model
147 | {
148 | return $this->baseFindOrFail($dn, $columns);
149 | }
150 |
151 | /**
152 | * {@inheritdoc}
153 | */
154 | protected function addFilterToDatabaseQuery(Builder $query, string $field, string $operator, ?string $value): void
155 | {
156 | if ($field === 'anr') {
157 | $query->whereIn('name', $this->model->getAnrAttributes())
158 | ->whereHas('values', function ($query) use ($value) {
159 | $query->where('value', 'like', "%$value%");
160 | });
161 |
162 | return;
163 | }
164 |
165 | $this->baseAddFilterToDatabaseQuery($query, $field, $operator, $value);
166 | }
167 |
168 | /**
169 | * Override the possibility of the underlying LDAP model being compatible with ANR.
170 | */
171 | protected function modelIsCompatibleWithAnr(): bool
172 | {
173 | return true;
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/Testing/Emulated/ModelBuilder.php:
--------------------------------------------------------------------------------
1 | determineBuilderFromModel($model);
24 |
25 | return (new $builder($this->connection))
26 | ->setBaseDn($this->baseDn)
27 | ->setModel($model);
28 | }
29 |
30 | /**
31 | * Determine the query builder to use for the model.
32 | */
33 | protected function determineBuilderFromModel(Model $model): string
34 | {
35 | switch (true) {
36 | case $model instanceof ActiveDirectory:
37 | return Emulated\ActiveDirectoryBuilder::class;
38 | case $model instanceof OpenLDAP:
39 | return Emulated\OpenLdapBuilder::class;
40 | default:
41 | return Emulated\ModelBuilder::class;
42 | }
43 | }
44 |
45 | /**
46 | * Process the database query results into an LDAP result set.
47 | */
48 | protected function process(array $results): array
49 | {
50 | return array_map([$this, 'mergeAttributesAndTransformResult'], $results);
51 | }
52 |
53 | /**
54 | * Merge and transform the result.
55 | */
56 | protected function mergeAttributesAndTransformResult(array $result): array
57 | {
58 | return array_merge(
59 | $this->transform($result),
60 | $this->retrieveExtraAttributes($result)
61 | );
62 | }
63 |
64 | /**
65 | * Retrieve extra attributes that should be merged with the result.
66 | */
67 | protected function retrieveExtraAttributes(array $result): array
68 | {
69 | $attributes = array_filter(['dn', $result['guid_key'] ?? null]);
70 |
71 | return array_map(function ($value) {
72 | return Arr::wrap($value);
73 | }, Arr::only($result, $attributes));
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Testing/EmulatedConnectionFake.php:
--------------------------------------------------------------------------------
1 | name;
21 | }
22 |
23 | $this->name = $name;
24 |
25 | return $this;
26 | }
27 |
28 | /**
29 | * Create a new Eloquent LDAP query builder.
30 | *
31 | * @throws \LdapRecord\Configuration\ConfigurationException
32 | */
33 | public function query(): EmulatedBuilder
34 | {
35 | return (new EmulatedBuilder($this))->setBaseDn(
36 | $this->configuration->get('base_dn')
37 | );
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Testing/LdapDatabaseManager.php:
--------------------------------------------------------------------------------
1 | db = $db;
55 | }
56 |
57 | /**
58 | * Create the eloquent database model.
59 | */
60 | public function createModel(?string $connection = null): Model
61 | {
62 | $class = '\\'.ltrim(static::$model, '\\');
63 |
64 | return tap(new $class)->setConnection($connection);
65 | }
66 |
67 | /**
68 | * Get the LDAP database connection.
69 | */
70 | public function connection(?string $name = null, array $config = []): Connection
71 | {
72 | $name = $name ?? Config::get('ldap.default', 'default');
73 |
74 | $this->connections[$name] = $this->makeConnection(
75 | $this->makeDatabaseConnectionName($name),
76 | Arr::get($config, 'database', ':memory:')
77 | );
78 |
79 | return $this->connections[$name];
80 | }
81 |
82 | /**
83 | * Make the database connection.
84 | */
85 | protected function makeConnection(string $name, string $database): Connection
86 | {
87 | // If we're not working with an in-memory database,
88 | // we'll assume a file path has been given and
89 | // create it before we run the migrations.
90 | if ($database !== ':memory:' && ! file_exists($database)) {
91 | file_put_contents($database, '');
92 | }
93 |
94 | app('config')->set("database.connections.$name", [
95 | 'driver' => 'sqlite',
96 | 'database' => $database,
97 | ]);
98 |
99 | return tap($this->db->connection($name), function (Connection $connection) {
100 | $this->migrate($connection);
101 | });
102 | }
103 |
104 | /**
105 | * Tear down the LDAP database connections.
106 | */
107 | public function teardown(): void
108 | {
109 | foreach ($this->connections as $name => $connection) {
110 | $this->rollback($connection);
111 |
112 | unset($this->connections[$name]);
113 | }
114 | }
115 |
116 | /**
117 | * Return the created connections.
118 | */
119 | public function getConnections(): array
120 | {
121 | return $this->connections;
122 | }
123 |
124 | /**
125 | * Make the unique LDAP database connection name.
126 | */
127 | protected function makeDatabaseConnectionName(string $name): string
128 | {
129 | return Str::start($name, 'ldap_');
130 | }
131 |
132 | /**
133 | * Run the database migrations on the connection.
134 | */
135 | protected function migrate(Connection $connection): void
136 | {
137 | $builder = $connection->getSchemaBuilder();
138 |
139 | if (! $builder->hasTable('ldap_objects')) {
140 | $builder->create('ldap_objects', function (Blueprint $table) {
141 | $table->bigIncrements('id');
142 | $table->timestamps();
143 | $table->string('domain')->nullable();
144 | $table->string('guid')->unique()->index();
145 | $table->string('guid_key')->nullable();
146 | $table->string('name')->nullable();
147 | $table->string('dn')->nullable();
148 | $table->string('parent_dn')->nullable();
149 | });
150 | }
151 |
152 | if (! $builder->hasTable('ldap_object_attributes')) {
153 | $builder->create('ldap_object_attributes', function (Blueprint $table) {
154 | $table->bigIncrements('id');
155 | $table->unsignedBigInteger('ldap_object_id');
156 | $table->string('name');
157 | });
158 | }
159 |
160 | if (! $builder->hasTable('ldap_object_attribute_values')) {
161 | $builder->create('ldap_object_attribute_values', function (Blueprint $table) {
162 | $table->bigIncrements('id');
163 | $table->unsignedBigInteger('ldap_object_attribute_id');
164 | $table->string('value');
165 | });
166 | }
167 | }
168 |
169 | /**
170 | * Rollback the database migrations on the connection.
171 | */
172 | protected function rollback(Connection $connection): void
173 | {
174 | if ($connection->getDatabaseName() === ':memory:') {
175 | tap($connection->getSchemaBuilder(), function (Builder $builder) {
176 | $builder->dropIfExists('ldap_object_attribute_values');
177 | $builder->dropIfExists('ldap_object_attributes');
178 | $builder->dropIfExists('ldap_objects');
179 | });
180 | } elseif (file_exists($dbFilePath = $connection->getDatabaseName())) {
181 | unlink($dbFilePath);
182 | }
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/src/Testing/LdapObject.php:
--------------------------------------------------------------------------------
1 | firstWhere('dn', 'like', $dn);
52 | }
53 |
54 | /**
55 | * Find an object by its object guid.
56 | */
57 | public static function findByGuid(string $guid, ?string $connection = null): ?static
58 | {
59 | return static::on($connection)->firstWhere('guid', '=', $guid);
60 | }
61 |
62 | /**
63 | * The hasMany attributes relation.
64 | */
65 | public function attributes(): HasMany
66 | {
67 | return $this->hasMany(LdapObjectAttribute::class);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Testing/LdapObjectAttribute.php:
--------------------------------------------------------------------------------
1 | values()->each(
51 | fn (LdapObjectAttributeValue $value) => $value->delete()
52 | );
53 | });
54 | }
55 |
56 | /**
57 | * The object relationship.
58 | */
59 | public function object(): BelongsTo
60 | {
61 | return $this->belongsTo(LdapObject::class, 'ldap_object_id');
62 | }
63 |
64 | /**
65 | * The values relationship.
66 | */
67 | public function values(): HasMany
68 | {
69 | return $this->hasMany(LdapObjectAttributeValue::class);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Testing/LdapObjectAttributeValue.php:
--------------------------------------------------------------------------------
1 | belongsTo(LdapObjectAttribute::class, 'ldap_object_attribute_id');
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Testing/ResolvesEmulatedConnection.php:
--------------------------------------------------------------------------------
1 | connection($connection);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Testing/UnescapedValue.php:
--------------------------------------------------------------------------------
1 | get();
15 | }
16 |
17 | /**
18 | * {@inheritdoc}
19 | */
20 | public function get(): string
21 | {
22 | // Don't escape values.
23 | return (string) $this->value;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Testing/VirtualAttributeObserver.php:
--------------------------------------------------------------------------------
1 | ['member', 'memberof'],
14 | ];
15 |
16 | /**
17 | * Handle updating the virtual attribute values in related model(s).
18 | */
19 | public function updated(LdapObject $model): void
20 | {
21 | if (empty($changes = $model->getChanges())) {
22 | return;
23 | }
24 |
25 | foreach (static::$attributes as $property => $attributes) {
26 | if (! array_key_exists($property, $changes)) {
27 | continue;
28 | }
29 |
30 | LdapObjectAttributeValue::query()
31 | ->where('value', $model->getOriginal($property))
32 | ->whereHas('attribute', fn (Builder $query) => $query->whereIn('name', $attributes))
33 | ->each(fn (LdapObjectAttributeValue $value) => $value->update(['value' => $changes[$property]]));
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Testing/VirtualAttributeValueObserver.php:
--------------------------------------------------------------------------------
1 | 'memberof',
12 | ];
13 |
14 | /**
15 | * Handle updating the virtual attribute in the related model(s).
16 | */
17 | public function saved(LdapObjectAttributeValue $model): void
18 | {
19 | if (! $this->isWatchedAttribute($model->attribute->name)) {
20 | return;
21 | }
22 |
23 | /** @var LdapObject|null $object */
24 | if (! $object = LdapObject::findByDn($model->value)) {
25 | return;
26 | }
27 |
28 | /** @var LdapObjectAttribute $attribute */
29 | $attribute = $object->attributes()->firstOrCreate([
30 | 'name' => static::$attributes[$model->attribute->name],
31 | ]);
32 |
33 | $attribute->values()->firstOrCreate([
34 | 'value' => $model->attribute->object->dn,
35 | ]);
36 | }
37 |
38 | /**
39 | * Handle deleting the virtual attribute in the related model(s).
40 | */
41 | public function deleted(LdapObjectAttributeValue $model): void
42 | {
43 | if (! $this->isWatchedAttribute($model->attribute->name)) {
44 | return;
45 | }
46 |
47 | /** @var LdapObject|null $object */
48 | if (! $object = LdapObject::findByDn($model->value)) {
49 | return;
50 | }
51 |
52 | /** @var LdapObjectAttribute|null $attribute */
53 | if (! $attribute = $object->attributes()->firstWhere('name', static::$attributes[$model->attribute->name])) {
54 | return;
55 | }
56 |
57 | $attribute->values()->where('value', $model->attribute->object->dn)->delete();
58 | }
59 |
60 | /**
61 | * Determine if the attribute is a watched attribute.
62 | */
63 | protected function isWatchedAttribute(string $attribute): bool
64 | {
65 | return array_key_exists($attribute, static::$attributes);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/tests/Feature/Commands/GetRootDseTest.php:
--------------------------------------------------------------------------------
1 | withoutMockingConsoleOutput();
20 | }
21 |
22 | public function test_command_displays_root_dse_attributes()
23 | {
24 | RootDse::create([
25 | 'objectclass' => ['*'],
26 | 'foo' => 'bar',
27 | 'baz' => 'zal',
28 | ]);
29 |
30 | $code = $this->artisan('ldap:rootdse');
31 |
32 | $this->assertEquals(0, $code);
33 |
34 | $output = Artisan::output();
35 |
36 | $this->assertStringContainsString('foo', $output);
37 | $this->assertStringContainsString('bar', $output);
38 | $this->assertStringContainsString('baz', $output);
39 | $this->assertStringContainsString('zal', $output);
40 | }
41 |
42 | public function test_command_displays_only_requested_attributes()
43 | {
44 | RootDse::create([
45 | 'objectclass' => ['*'],
46 | 'foo' => 'bar',
47 | 'baz' => 'zal',
48 | 'zee' => 'bur',
49 | ]);
50 |
51 | $code = $this->artisan('ldap:rootdse', ['--attributes' => 'foo,baz']);
52 |
53 | $this->assertEquals(0, $code);
54 |
55 | $output = Artisan::output();
56 |
57 | $this->assertStringContainsString('foo', $output);
58 | $this->assertStringContainsString('bar', $output);
59 | $this->assertStringContainsString('baz', $output);
60 | $this->assertStringContainsString('zal', $output);
61 |
62 | $this->assertStringNotContainsString('zee', $output);
63 | $this->assertStringNotContainsString('bur', $output);
64 | }
65 |
66 | public function test_command_displays_no_attributes_error_when_rootdse_is_empty_and_attributes_were_requested()
67 | {
68 | RootDse::create([
69 | 'objectclass' => ['*'],
70 | ]);
71 |
72 | $code = $this->artisan('ldap:rootdse', ['--attributes' => 'foo,bar']);
73 |
74 | $this->assertEquals(Command::FAILURE, $code);
75 |
76 | $output = Artisan::output();
77 |
78 | $this->assertStringContainsString('Attributes [foo, bar] were not found in the Root DSE record.', $output);
79 | }
80 | }
81 |
82 | class RootDse extends Entry
83 | {
84 | public function getCreatableDn(?string $name = null, ?string $attribute = null): string
85 | {
86 | return '';
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/tests/Feature/Commands/TestLdapConnectionTest.php:
--------------------------------------------------------------------------------
1 | shouldReceive('setDispatcher')->once()->with(DispatcherInterface::class)->andReturn($connection);
27 |
28 | Container::addConnection($connection);
29 |
30 | $connection->shouldReceive('connect')->once()->andReturnNull();
31 | $connection->shouldReceive('isConnected')->once()->andReturnTrue();
32 | $connection->shouldReceive('getConfiguration')->once()->andReturn(new DomainConfiguration(['username' => 'user']));
33 |
34 | $pendingCommand = $this->artisan('ldap:test', $params);
35 |
36 | $table = (new Table($output = new BufferedOutput))
37 | ->setHeaders(['Connection', 'Successful', 'Username', 'Message', 'Response Time'])
38 | ->setRows([
39 | ['default', '✔ Yes', 'user', 'Successfully connected.', '0ms'],
40 | ]);
41 |
42 | $table->render();
43 |
44 | $lines = array_filter(
45 | preg_split("/\n/", $output->fetch())
46 | );
47 |
48 | foreach ($lines as $line) {
49 | $pendingCommand->expectsOutput($line);
50 | }
51 |
52 | $pendingCommand->assertSuccessful();
53 | }
54 |
55 | /**
56 | * @testWith
57 | * [{"connection": "default"}]
58 | * [[]]
59 | */
60 | public function test_command_fails_when_connection_fails($params)
61 | {
62 | $connection = m::mock(Connection::class);
63 |
64 | $connection->shouldReceive('setDispatcher')->once()->with(DispatcherInterface::class)->andReturn($connection);
65 |
66 | Container::addConnection($connection);
67 |
68 | $connection->shouldReceive('connect')->once()->andThrow(new BindException('Unable to connect'));
69 | $connection->shouldReceive('isConnected')->once()->andReturnFalse();
70 | $connection->shouldReceive('getConfiguration')->once()->andReturn(new DomainConfiguration(['username' => 'user']));
71 |
72 | $pendingCommand = $this->artisan('ldap:test', $params);
73 |
74 | $table = (new Table($output = new BufferedOutput))
75 | ->setHeaders(['Connection', 'Successful', 'Username', 'Message', 'Response Time'])
76 | ->setRows([
77 | ['default', '✘ No', 'user', 'Unable to connect. Error Code: [NULL] Diagnostic Message: NULL', '0ms'],
78 | ]);
79 |
80 | $table->render();
81 |
82 | $lines = array_filter(
83 | preg_split("/\n/", $output->fetch())
84 | );
85 |
86 | foreach ($lines as $line) {
87 | $pendingCommand->expectsOutput($line);
88 | }
89 |
90 | $pendingCommand->assertFailed();
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/tests/Feature/CreatesTestUsers.php:
--------------------------------------------------------------------------------
1 | $value) {
18 | $user->{$name} = $value;
19 | }
20 |
21 | $user->save();
22 | });
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/Feature/DatabaseTestCase.php:
--------------------------------------------------------------------------------
1 | softDeletes();
30 | });
31 | }
32 |
33 | protected function defineDatabaseMigrations()
34 | {
35 | $this->loadMigrationsFrom(__DIR__.'/../../database/migrations');
36 | $this->loadMigrationsFrom(__DIR__.'/../../vendor/laravel/sanctum/database/migrations');
37 | }
38 |
39 | protected function getMockLdapModel(array $attributes = [])
40 | {
41 | $ldapModel = m::mock(Model::class);
42 | $ldapModel->shouldReceive('getName')->andReturn('name');
43 | $ldapModel->shouldReceive('getConvertedGuid')->andReturn('guid');
44 | $ldapModel->shouldReceive('getConnectionName')->andReturn('default');
45 |
46 | foreach ($attributes as $name => $value) {
47 | $ldapModel->shouldReceive('getFirstAttribute')->withArgs([$name])->andReturn($value);
48 | }
49 |
50 | return $ldapModel;
51 | }
52 |
53 | protected function createDatabaseUserProvider(
54 | ?LdapUserRepository $repo = null,
55 | ?LdapUserAuthenticator $auth = null,
56 | ?UserSynchronizer $synchronizer = null,
57 | ?EloquentUserProvider $eloquent = null
58 | ) {
59 | return new DatabaseUserProvider(
60 | $repo ?? $this->createLdapUserRepository(),
61 | $auth ?? $this->createLdapUserAuthenticator(),
62 | $synchronizer ?? $this->createLdapUserSynchronizer(),
63 | $eloquent ?? $this->createEloquentUserProvider()
64 | );
65 | }
66 |
67 | protected function createLdapUserRepository($model = null)
68 | {
69 | return new LdapUserRepository($model ?? User::class);
70 | }
71 |
72 | protected function createLdapUserAuthenticator()
73 | {
74 | return new LdapUserAuthenticator;
75 | }
76 |
77 | protected function createLdapUserSynchronizer($eloquentModel = null, array $config = [])
78 | {
79 | return new UserSynchronizer($eloquentModel ?? TestUserModelStub::class, $config);
80 | }
81 |
82 | protected function createEloquentUserProvider($model = null)
83 | {
84 | return new EloquentUserProvider(app('hash'), $model ?? TestUserModelStub::class);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/tests/Feature/DatabaseUserProviderTest.php:
--------------------------------------------------------------------------------
1 | createDatabaseUserProvider(
32 | $this->createLdapUserRepository(),
33 | $this->createLdapUserAuthenticator(),
34 | $synchronizer
35 | );
36 |
37 | $this->assertSame($synchronizer, $provider->getLdapUserSynchronizer());
38 | }
39 |
40 | public function test_retrieve_by_id_uses_eloquent()
41 | {
42 | $model = $this->createTestUser([
43 | 'name' => 'john',
44 | 'email' => 'test@email.com',
45 | 'password' => 'secret',
46 | ]);
47 |
48 | $provider = $this->createDatabaseUserProvider();
49 |
50 | $this->assertTrue($model->is($provider->retrieveById($model->id)));
51 | }
52 |
53 | public function test_retrieve_by_token_uses_eloquent()
54 | {
55 | $model = $this->createTestUser([
56 | 'name' => 'john',
57 | 'email' => 'test@email.com',
58 | 'password' => 'secret',
59 | 'remember_token' => 'token',
60 | ]);
61 |
62 | $provider = $this->createDatabaseUserProvider();
63 |
64 | $this->assertTrue($model->is($provider->retrieveByToken($model->id, $model->remember_token)));
65 | }
66 |
67 | public function test_update_remember_token_uses_eloquent()
68 | {
69 | $model = $this->createTestUser([
70 | 'name' => 'john',
71 | 'email' => 'test@email.com',
72 | 'password' => 'secret',
73 | 'remember_token' => 'token',
74 | ]);
75 |
76 | $provider = $this->createDatabaseUserProvider();
77 |
78 | $provider->updateRememberToken($model, 'new-token');
79 |
80 | $this->assertEquals('new-token', $model->fresh()->remember_token);
81 | }
82 |
83 | public function test_retrieve_by_credentials_returns_unsaved_database_model()
84 | {
85 | $credentials = ['samaccountname' => 'jdoe', 'password' => 'secret'];
86 |
87 | $ldapUser = $this->getMockLdapModel([
88 | 'cn' => 'John Doe',
89 | 'mail' => 'jdoe@test.com',
90 | ]);
91 |
92 | $repo = m::mock(LdapUserRepository::class);
93 | $repo->shouldReceive('findByCredentials')->once()->withArgs([$credentials])->andReturn($ldapUser);
94 |
95 | $provider = $this->createDatabaseUserProvider($repo);
96 |
97 | $databaseModel = $provider->retrieveByCredentials($credentials);
98 |
99 | $this->assertFalse($databaseModel->exists);
100 | $this->assertEquals('John Doe', $databaseModel->name);
101 | $this->assertEquals('jdoe@test.com', $databaseModel->email);
102 | $this->assertFalse(Hash::needsRehash($databaseModel->password));
103 | }
104 |
105 | public function test_validate_credentials_returns_false_when_no_database_model_is_set()
106 | {
107 | $databaseModel = new TestUserModelStub;
108 |
109 | $provider = $this->createDatabaseUserProvider();
110 |
111 | $this->assertFalse($provider->validateCredentials($databaseModel, ['password' => 'secret']));
112 | $this->assertFalse($databaseModel->exists);
113 | }
114 |
115 | public function test_validate_credentials_saves_database_model_after_passing()
116 | {
117 | $credentials = ['samaccountname' => 'jdoe', 'password' => 'secret'];
118 |
119 | $ldapUser = $this->getMockLdapModel([
120 | 'cn' => 'John Doe',
121 | 'mail' => 'jdoe@test.com',
122 | ]);
123 |
124 | $repo = m::mock(LdapUserRepository::class);
125 | $repo->shouldReceive('findByCredentials')->once()->withArgs([$credentials])->andReturn($ldapUser);
126 |
127 | $auth = m::mock(LdapUserAuthenticator::class);
128 | $auth->shouldReceive('setEloquentModel')->once()->withArgs([TestUserModelStub::class]);
129 | $auth->shouldReceive('attempt')->once()->withArgs([$ldapUser, 'secret'])->andReturnTrue();
130 |
131 | $provider = $this->createDatabaseUserProvider($repo, $auth);
132 |
133 | $databaseModel = $provider->retrieveByCredentials($credentials);
134 | $provider->validateCredentials($databaseModel, ['password' => 'secret']);
135 | $this->assertTrue($databaseModel->exists);
136 | $this->assertTrue($databaseModel->wasRecentlyCreated);
137 | }
138 |
139 | public function test_method_calls_are_passed_to_eloquent_user_provider()
140 | {
141 | $provider = $this->createDatabaseUserProvider();
142 |
143 | $model = $provider->getModel();
144 |
145 | $this->assertInstanceOf(Model::class, new $model);
146 | }
147 |
148 | public function test_failing_loudly_throws_exception_when_resolving_users()
149 | {
150 | LdapRecord::failLoudly();
151 |
152 | $provider = m::mock(DatabaseUserProvider::class)->makePartial();
153 |
154 | $provider->resolveUsersUsing(function () {
155 | throw new Exception('Failed');
156 | });
157 |
158 | $this->expectExceptionMessage('Failed');
159 |
160 | $provider->retrieveByCredentials([]);
161 | }
162 |
163 | public function test_rehash_password_if_required_does_nothing_when_password_column_disabled()
164 | {
165 | $synchronizer = new UserSynchronizer(TestUserModelStub::class, [
166 | 'password_column' => false,
167 | ]);
168 |
169 | $provider = $this->createDatabaseUserProvider(synchronizer: $synchronizer);
170 |
171 | $provider->rehashPasswordIfRequired($model = new TestUserModelStub, ['password' => 'secret']);
172 |
173 | $this->assertNull($model->password);
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/tests/Feature/Emulator/EmulatedAuthenticationTest.php:
--------------------------------------------------------------------------------
1 | setupPlainUserProvider();
30 |
31 | $user = LdapUser::create([
32 | 'cn' => 'John',
33 | 'mail' => 'jdoe@email.com',
34 | ]);
35 |
36 | $fake->actingAs($user);
37 |
38 | $this->assertTrue(Auth::attempt([
39 | 'mail' => $user->mail[0],
40 | 'password' => 'secret',
41 | ]));
42 |
43 | $model = Auth::user();
44 |
45 | $this->assertInstanceOf(LdapUser::class, $model);
46 | $this->assertTrue($user->is($model));
47 | $this->assertEquals($user->mail[0], $model->mail[0]);
48 | $this->assertEquals($user->getDn(), $model->getDn());
49 |
50 | Event::assertDispatched(Binding::class);
51 | Event::assertDispatched(Bound::class);
52 | Event::assertDispatched(DiscoveredWithCredentials::class);
53 |
54 | Event::assertNotDispatched(Importing::class);
55 | Event::assertNotDispatched(Imported::class);
56 | Event::assertNotDispatched(Synchronizing::class);
57 | Event::assertNotDispatched(Synchronized::class);
58 | }
59 |
60 | public function test_plain_ldap_authentication_fails()
61 | {
62 | Event::fake();
63 |
64 | DirectoryEmulator::setup()->shouldBeConnected();
65 |
66 | $this->setupPlainUserProvider();
67 |
68 | $user = LdapUser::create([
69 | 'cn' => 'John',
70 | 'mail' => 'jdoe@email.com',
71 | ]);
72 |
73 | $this->assertFalse(Auth::attempt(['mail' => $user->mail[0], 'password' => 'secret']));
74 |
75 | Event::assertDispatched(Binding::class);
76 | Event::assertDispatched(DiscoveredWithCredentials::class);
77 |
78 | Event::assertNotDispatched(Bound::class);
79 | Event::assertNotDispatched(Importing::class);
80 | Event::assertNotDispatched(Imported::class);
81 | Event::assertNotDispatched(Synchronizing::class);
82 | Event::assertNotDispatched(Synchronized::class);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/tests/Feature/Emulator/EmulatedModelBindingTest.php:
--------------------------------------------------------------------------------
1 | setupDatabaseUserProvider([
17 | 'database' => [
18 | 'model' => TestUserModelStub::class,
19 | ],
20 | ]);
21 |
22 | DirectoryEmulator::setup();
23 |
24 | $user = User::create([
25 | 'cn' => 'John',
26 | 'mail' => 'jdoe@federalbridge.ca',
27 | 'objectguid' => Uuid::uuid4()->toString(),
28 | ]);
29 |
30 | $model = TestUserModelStub::create([
31 | 'name' => $user->cn[0],
32 | 'email' => $user->mail[0],
33 | 'guid' => $user->objectguid[0],
34 | 'password' => 'secret',
35 | ]);
36 |
37 | Auth::login($model);
38 |
39 | $this->assertInstanceOf(User::class, $model->ldap);
40 | $this->assertTrue($user->is($model->ldap));
41 | $this->assertTrue(isset($model->ldap));
42 | }
43 |
44 | public function test_ldap_users_are_not_bound_when_model_cannot_be_located()
45 | {
46 | $this->setupDatabaseUserProvider([
47 | 'database' => [
48 | 'model' => TestUserModelStub::class,
49 | ],
50 | ]);
51 |
52 | DirectoryEmulator::setup();
53 |
54 | $model = TestUserModelStub::create([
55 | 'name' => 'John',
56 | 'email' => 'jdoe@email.com',
57 | 'guid' => Uuid::uuid4()->toString(),
58 | 'password' => 'secret',
59 | ]);
60 |
61 | Auth::login($model);
62 |
63 | $this->assertNull($model->ldap);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/tests/Feature/Emulator/EmulatedUserRepositoryTest.php:
--------------------------------------------------------------------------------
1 | 'John']),
20 | User::create(['cn' => 'Jane']),
21 | User::create(['cn' => 'Jen']),
22 | ]);
23 |
24 | $repo = new LdapUserRepository(User::class);
25 | $this->assertNull($repo->findBy('cn', 'Other'));
26 | $this->assertTrue($users->get(1)->is($repo->findBy('cn', 'Jane')));
27 | }
28 |
29 | public function test_find_by_guid()
30 | {
31 | DirectoryEmulator::setup();
32 |
33 | $user = User::create(['cn' => 'John', 'objectguid' => Uuid::uuid4()->toString()]);
34 |
35 | $repo = new LdapUserRepository(User::class);
36 | $this->assertNull($repo->findByGuid(Uuid::uuid4()->toString()));
37 | $this->assertTrue($user->is($repo->findByGuid($user->getConvertedGuid())));
38 | }
39 |
40 | public function test_find_by_model()
41 | {
42 | DirectoryEmulator::setup();
43 |
44 | $guid = Uuid::uuid4()->toString();
45 |
46 | $user = User::create(['cn' => 'John', 'objectguid' => $guid]);
47 |
48 | $model = new TestUserModelStub(['guid' => $guid]);
49 |
50 | $repo = new LdapUserRepository(User::class);
51 | $this->assertNull($repo->findByModel(new TestUserModelStub(['guid' => Uuid::uuid4()])));
52 | $this->assertTrue($user->is($repo->findByModel($model)));
53 | }
54 |
55 | public function test_find_by_credentials()
56 | {
57 | DirectoryEmulator::setup();
58 |
59 | $user = User::create(['cn' => 'John', 'mail' => 'jdoe@email.com']);
60 |
61 | $repo = new LdapUserRepository(User::class);
62 | $this->assertNull($repo->findByCredentials(['mail' => 'invalid@email.com']));
63 | $this->assertTrue($user->is($repo->findByCredentials(
64 | ['mail' => $user->mail[0]]
65 | )));
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/tests/Feature/Emulator/EmulatedWindowsAuthenticateTest.php:
--------------------------------------------------------------------------------
1 | setupDatabaseUserProvider([
49 | 'database' => [
50 | 'model' => TestUserModelStub::class,
51 | ],
52 | ]);
53 |
54 | $user = User::create([
55 | 'cn' => 'John',
56 | 'mail' => 'jdoe@email.com',
57 | 'samaccountname' => 'jdoe',
58 | 'objectguid' => $this->faker->uuid,
59 | ]);
60 |
61 | $request = tap(new Request, function ($request) {
62 | $request->server->set('AUTH_USER', 'LOCAL\\jdoe');
63 | });
64 |
65 | app(WindowsAuthenticate::class)->handle($request, function () use ($user) {
66 | $this->assertTrue(auth()->check());
67 | $this->assertEquals($user->getConvertedGuid(), auth()->user()->guid);
68 | });
69 |
70 | Event::assertDispatched(Importing::class);
71 | Event::assertDispatched(Imported::class);
72 | Event::assertDispatched(Synchronizing::class);
73 | Event::assertDispatched(Synchronized::class);
74 | Event::assertDispatched(Binding::class);
75 | Event::assertDispatched(Bound::class);
76 | Event::assertDispatched(Saved::class);
77 | Event::assertDispatched(CompletedWithWindows::class);
78 | }
79 |
80 | public function test_kerberos_authenticated_user_is_signed_in()
81 | {
82 | Event::fake();
83 |
84 | DirectoryEmulator::setup();
85 |
86 | $this->setupDatabaseUserProvider([
87 | 'database' => [
88 | 'model' => TestUserModelStub::class,
89 | ],
90 | ]);
91 |
92 | WindowsAuthenticate::serverKey('REMOTE_USER');
93 | WindowsAuthenticate::username('userPrincipalName');
94 | WindowsAuthenticate::extractDomainUsing(function ($accountName) {
95 | return $accountName;
96 | });
97 | WindowsAuthenticate::validateDomainUsing(function ($user, $username) {
98 | return $user->getFirstAttribute('userPrincipalName') === $username;
99 | });
100 |
101 | $user = User::create([
102 | 'cn' => 'John',
103 | 'mail' => 'jdoe@email.com',
104 | 'samaccountname' => 'jdoe',
105 | 'userprincipalname' => 'jdoe@local.com',
106 | 'objectguid' => $this->faker->uuid,
107 | ]);
108 |
109 | $request = tap(new Request, function ($request) use ($user) {
110 | $request->server->set('REMOTE_USER', $user->getFirstAttribute('userprincipalname'));
111 | });
112 |
113 | app(WindowsAuthenticate::class)->handle($request, function () use ($user) {
114 | $this->assertTrue(auth()->check());
115 | $this->assertEquals($user->getConvertedGuid(), auth()->user()->guid);
116 | });
117 |
118 | Event::assertDispatched(Importing::class);
119 | Event::assertDispatched(Imported::class);
120 | Event::assertDispatched(Synchronizing::class);
121 | Event::assertDispatched(Synchronized::class);
122 | Event::assertDispatched(Binding::class);
123 | Event::assertDispatched(Bound::class);
124 | Event::assertDispatched(Saved::class);
125 | Event::assertDispatched(CompletedWithWindows::class);
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/tests/Feature/Emulator/EmulatorTest.php:
--------------------------------------------------------------------------------
1 | set('ldap.default', 'default');
17 | $app['config']->set('ldap.connections.default', [
18 | 'base_dn' => 'dc=local,dc=com',
19 | 'use_tls' => true,
20 | ]);
21 | }
22 |
23 | public function test_sqlite_connection_is_setup()
24 | {
25 | DirectoryEmulator::setup('default');
26 |
27 | $db = app(LdapDatabaseManager::class);
28 |
29 | $this->assertCount(1, $db->getConnections());
30 |
31 | $connection = $db->getConnections()['default'];
32 |
33 | $this->assertInstanceOf(Connection::class, $connection);
34 | $this->assertEquals('sqlite', $connection->getConfig('driver'));
35 | $this->assertEquals(':memory:', $connection->getDatabaseName());
36 | }
37 |
38 | public function test_configuration_and_options_are_carried_over_to_emulated_connection()
39 | {
40 | $fake = DirectoryEmulator::setup('default');
41 |
42 | $this->assertTrue($fake->getLdapConnection()->isUsingTLS());
43 | $this->assertEquals('dc=local,dc=com', $fake->getConfiguration()->get('base_dn'));
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/Feature/ListenForLdapBindFailureTest.php:
--------------------------------------------------------------------------------
1 | set('ldap.connections.default', [
19 | 'hosts' => ['one', 'two', 'three'],
20 | 'username' => 'user',
21 | 'password' => 'secret',
22 | 'base_dn' => 'dc=local,dc=com',
23 | ]);
24 | }
25 |
26 | public function test_validation_exception_is_not_thrown_until_all_connection_hosts_are_attempted()
27 | {
28 | $this->setupPlainUserProvider(['model' => User::class]);
29 |
30 | $fake = DirectoryFake::setup('default')->shouldNotBeConnected();
31 |
32 | $expectedSelects = [
33 | 'objectguid',
34 | '*',
35 | ];
36 |
37 | $expectedFilter = $fake->query()
38 | ->where([
39 | ['objectclass', '=', 'top'],
40 | ['objectclass', '=', 'person'],
41 | ['objectclass', '=', 'organizationalperson'],
42 | ['objectclass', '=', 'user'],
43 | ['mail', '=', 'jdoe@local.com'],
44 | ['objectclass', '!=', 'computer'],
45 | ])
46 | ->getQuery();
47 |
48 | $expectedQueryResult = [
49 | [
50 | 'mail' => ['jdoe@local.com'],
51 | 'dn' => ['cn=jdoe,dc=local,dc=com'],
52 | ],
53 | ];
54 |
55 | $fake->getLdapConnection()->expect([
56 | // Two bind attempts fail on hosts "one" and "two" with configured user account.
57 | LdapFake::operation('bind')
58 | ->with('user', 'secret')
59 | ->twice()
60 | ->andReturn(new LdapResultResponse(1)),
61 |
62 | // Third bind attempt passes.
63 | LdapFake::operation('bind')
64 | ->with('user', 'secret')
65 | ->once()
66 | ->andReturn(new LdapResultResponse),
67 |
68 | // Bind is attempted with the authenticating user and passes.
69 | LdapFake::operation('bind')
70 | ->with('cn=jdoe,dc=local,dc=com', 'secret')
71 | ->once()
72 | ->andReturn(new LdapResultResponse),
73 |
74 | // Rebind is attempted with configured user account.
75 | LdapFake::operation('bind')
76 | ->with('user', 'secret')
77 | ->once()
78 | ->andReturn(new LdapResultResponse(0)),
79 |
80 | // Search operation is executed for authenticating user.
81 | LdapFake::operation('search')
82 | ->with(['dc=local,dc=com', $expectedFilter, $expectedSelects, false, 1])
83 | ->once()
84 | ->andReturn($expectedQueryResult),
85 |
86 | LdapFake::operation('parseResult')
87 | ->once()
88 | ->andReturn(new LdapResultResponse),
89 | ])->shouldReturnError("Can't contact LDAP server");
90 |
91 | $result = Auth::attempt([
92 | 'mail' => 'jdoe@local.com',
93 | 'password' => 'secret',
94 | ]);
95 |
96 | $this->assertTrue($result);
97 | $this->assertCount(2, $fake->attempted());
98 | $this->assertInstanceOf(User::class, Auth::user());
99 | $this->assertEquals(['one', 'two'], array_keys($fake->attempted()));
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/tests/Feature/NoDatabaseUserProviderTest.php:
--------------------------------------------------------------------------------
1 | assertSame($repo, $provider->getLdapUserRepository());
32 | }
33 |
34 | public function test_user_authenticator_can_be_retrieved()
35 | {
36 | $auth = new LdapUserAuthenticator;
37 |
38 | $provider = new NoDatabaseUserProvider(new LdapUserRepository(User::class), $auth);
39 |
40 | $this->assertSame($auth, $provider->getLdapUserAuthenticator());
41 | }
42 |
43 | public function test_retrieve_by_id_returns_model_instance()
44 | {
45 | $model = new Entry;
46 |
47 | $repo = m::mock(LdapUserRepository::class);
48 |
49 | $repo->shouldReceive('findByGuid')->once()->withArgs(['id'])->andReturn($model);
50 |
51 | $provider = new NoDatabaseUserProvider($repo, new LdapUserAuthenticator);
52 |
53 | $this->assertSame($model, $provider->retrieveById('id'));
54 | }
55 |
56 | public function test_retrieve_by_credentials_returns_model_instance()
57 | {
58 | $model = new Entry;
59 |
60 | $repo = m::mock(LdapUserRepository::class);
61 |
62 | $repo->shouldReceive('findByCredentials')->once()->withArgs([['username' => 'foo']])->andReturn($model);
63 |
64 | $provider = new NoDatabaseUserProvider($repo, new LdapUserAuthenticator);
65 |
66 | $this->assertSame($model, $provider->retrieveByCredentials(['username' => 'foo']));
67 | }
68 |
69 | public function test_validate_credentials_attempts_authentication()
70 | {
71 | $user = new User;
72 |
73 | $auth = m::mock(LdapUserAuthenticator::class);
74 |
75 | $auth->shouldReceive('attempt')->once()->withArgs([$user, 'secret'])->andReturnTrue();
76 |
77 | $provider = new NoDatabaseUserProvider(new LdapUserRepository(Entry::class), $auth);
78 |
79 | $this->assertTrue($provider->validateCredentials($user, ['password' => 'secret']));
80 | }
81 |
82 | public function test_failing_loudly_throws_exception_when_resolving_users()
83 | {
84 | LdapRecord::failLoudly();
85 |
86 | $provider = m::mock(NoDatabaseUserProvider::class)->makePartial();
87 |
88 | $provider->resolveUsersUsing(function () {
89 | throw new Exception('Failed');
90 | });
91 |
92 | $this->expectExceptionMessage('Failed');
93 |
94 | $provider->retrieveByCredentials([]);
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/tests/Feature/SanctumTest.php:
--------------------------------------------------------------------------------
1 | setupDatabaseUserProvider([
22 | 'database' => [
23 | 'model' => SanctumTestUserModelStub::class,
24 | 'sync_attributes' => [
25 | 'name' => 'cn',
26 | 'email' => 'mail',
27 | ],
28 | ],
29 | ]);
30 |
31 | Route::get('api/user', function (Request $request) {
32 | return $request->user();
33 | })->middleware('auth:sanctum');
34 |
35 | Route::post('api/sanctum/token', function (Request $request) {
36 | if (Auth::validate($request->only('mail', 'password'))) {
37 | return [
38 | 'token' => Auth::getLastAttempted()
39 | ->createToken($request->device_name)
40 | ->plainTextToken,
41 | ];
42 | }
43 |
44 | throw ValidationException::withMessages([
45 | 'email' => 'The provided credentials are incorrect.',
46 | ]);
47 | });
48 | }
49 |
50 | public function test_ldap_user_can_request_sanctum_token()
51 | {
52 | $fake = DirectoryEmulator::setup();
53 |
54 | $user = LdapUser::create([
55 | 'cn' => 'John Doe',
56 | 'mail' => 'john@local.com',
57 | ]);
58 |
59 | $fake->actingAs($user);
60 |
61 | $this->postJson('api/sanctum/token', [
62 | 'mail' => 'john@local.com',
63 | 'password' => 'secret',
64 | 'device_name' => 'browser',
65 | ])->assertJsonStructure(['token']);
66 |
67 | $this->assertDatabaseHas('users', [
68 | 'email' => $user->mail[0],
69 | 'name' => $user->cn[0],
70 | ]);
71 |
72 | $this->assertEquals(1, PersonalAccessToken::count());
73 | }
74 |
75 | public function test_ldap_user_can_fail_requesting_sanctum_token_with_invalid_password()
76 | {
77 | DirectoryEmulator::setup();
78 |
79 | $user = LdapUser::create([
80 | 'cn' => 'John Doe',
81 | 'mail' => 'john@local.com',
82 | ]);
83 |
84 | $this->postJson('api/sanctum/token', [
85 | 'mail' => 'john@local.com',
86 | 'password' => 'secret',
87 | 'device_name' => 'browser',
88 | ])->assertJsonValidationErrors(['email' => 'The provided credentials are incorrect.']);
89 |
90 | $this->assertDatabaseMissing('users', [
91 | 'email' => $user->mail[0],
92 | 'name' => $user->cn[0],
93 | ]);
94 |
95 | $this->assertEquals(0, PersonalAccessToken::count());
96 | }
97 |
98 | public function test_ldap_user_can_use_sanctum_token_for_authentication()
99 | {
100 | $fake = DirectoryEmulator::setup();
101 |
102 | $user = LdapUser::create([
103 | 'cn' => 'John Doe',
104 | 'mail' => 'john@local.com',
105 | ]);
106 |
107 | $fake->actingAs($user);
108 |
109 | $plainTextToken = $this->postJson('api/sanctum/token', [
110 | 'mail' => 'john@local.com',
111 | 'password' => 'secret',
112 | 'device_name' => 'browser',
113 | ])->json('token');
114 |
115 | $this->getJson('api/user', [
116 | 'Authorization' => "Bearer $plainTextToken",
117 | ])->assertJsonFragment([
118 | 'name' => $user->cn[0],
119 | 'email' => $user->mail[0],
120 | ]);
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/tests/Feature/SanctumTestUserModelStub.php:
--------------------------------------------------------------------------------
1 | set('database.default', 'testbench');
46 | $config->set('database.connections.testbench', [
47 | 'driver' => 'sqlite',
48 | 'database' => ':memory:',
49 | ]);
50 |
51 | // LDAP mock setup.
52 | $config->set('ldap.default', 'default');
53 | $config->set('ldap.logging.enabled', false);
54 | $config->set('ldap.connections.default', [
55 | 'hosts' => ['localhost'],
56 | 'username' => 'user',
57 | 'password' => 'secret',
58 | 'base_dn' => 'dc=local,dc=com',
59 | 'port' => 389,
60 | ]);
61 | }
62 |
63 | protected function setupLdapUserProvider($guardName, array $config)
64 | {
65 | config()->set('auth.defaults.guard', $guardName);
66 |
67 | config()->set("auth.guards.$guardName", [
68 | 'driver' => 'session',
69 | 'provider' => $guardName,
70 | ]);
71 |
72 | config()->set("auth.providers.$guardName", array_merge([
73 | 'rules' => [],
74 | 'driver' => 'ldap',
75 | 'model' => User::class,
76 | ], $config));
77 | }
78 |
79 | protected function setupPlainUserProvider(array $config = [])
80 | {
81 | $this->setupLdapUserProvider('ldap-plain', $config);
82 | }
83 |
84 | protected function setupDatabaseUserProvider(array $config = [])
85 | {
86 | $this->setupLdapUserProvider('ldap-database', array_merge([
87 | 'database' => [
88 | 'sync_attributes' => [
89 | 'name' => 'cn',
90 | 'email' => 'mail',
91 | ],
92 | ],
93 | ], $config));
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/tests/Unit/EloquentHydratorTest.php:
--------------------------------------------------------------------------------
1 | 'bf9679e7-0de6-11d0-a285-00aa003049e2']);
22 | $model = new TestModelStub;
23 | $hydrator = new GuidHydrator;
24 |
25 | $hydrator->hydrate($entry, $model);
26 |
27 | $this->assertEquals($entry->getConvertedGuid(), $model->guid);
28 | }
29 |
30 | public function test_domain_hydrator_uses_default_connection_name()
31 | {
32 | $entry = new Entry;
33 | $model = new TestModelStub;
34 | $hydrator = new DomainHydrator;
35 |
36 | $hydrator->hydrate($entry, $model);
37 |
38 | $this->assertEquals('default', $model->domain);
39 | }
40 |
41 | public function test_attribute_hydrator()
42 | {
43 | $entry = new Entry(['bar' => 'baz']);
44 | $model = new TestModelStub;
45 |
46 | AttributeHydrator::with([
47 | 'sync_attributes' => ['foo' => 'bar'],
48 | ])->hydrate($entry, $model);
49 |
50 | $this->assertEquals('baz', $model->foo);
51 | }
52 |
53 | public function test_attribute_hydrator_can_use_handle_function_of_class()
54 | {
55 | $entry = new Entry(['bar' => 'baz']);
56 | $model = new TestModelStub;
57 |
58 | AttributeHydrator::with([
59 | 'sync_attributes' => [TestAttributeHandlerHandleStub::class],
60 | ])->hydrate($entry, $model);
61 |
62 | $this->assertEquals('baz', $model->foo);
63 | }
64 |
65 | public function test_attribute_hydrator_can_use_invokable_class()
66 | {
67 | $entry = new Entry(['bar' => 'baz']);
68 | $model = new TestModelStub;
69 |
70 | AttributeHydrator::with(['sync_attributes' => [
71 | TestAttributeHandlerInvokableStub::class,
72 | ]])->hydrate($entry, $model);
73 |
74 | $this->assertEquals('baz', $model->foo);
75 | }
76 |
77 | public function test_attribute_hydrator_can_use_inline_function()
78 | {
79 | $entry = new Entry(['bar' => 'baz']);
80 | $model = new TestModelStub;
81 |
82 | AttributeHydrator::with(['sync_attributes' => [
83 | function ($object, $eloquent) {
84 | $eloquent->foo = $object->getFirstAttribute('bar');
85 | },
86 | ]])->hydrate($entry, $model);
87 |
88 | $this->assertEquals('baz', $model->foo);
89 | }
90 |
91 | public function test_password_hydrator_uses_random_password()
92 | {
93 | $entry = new Entry;
94 | $model = new TestModelStub;
95 | $hydrator = new PasswordHydrator;
96 |
97 | $hydrator->hydrate($entry, $model);
98 |
99 | $this->assertFalse(Hash::needsRehash($model->password));
100 | }
101 |
102 | public function test_password_hydrator_does_nothing_when_password_column_is_disabled()
103 | {
104 | $entry = new Entry;
105 | $model = new TestModelStub;
106 | $hydrator = new PasswordHydrator(['password_column' => false]);
107 |
108 | $hydrator->hydrate($entry, $model);
109 |
110 | $this->assertNull($model->password);
111 | }
112 |
113 | public function test_password_hydrator_uses_given_password_when_password_sync_is_enabled()
114 | {
115 | $entry = new Entry;
116 | $model = new TestModelStub;
117 | $hydrator = new PasswordHydrator(['sync_passwords' => true], ['password' => 'secret']);
118 |
119 | $hydrator->hydrate($entry, $model);
120 |
121 | $this->assertFalse(Hash::needsRehash($model->password));
122 | $this->assertTrue(Hash::check('secret', $model->password));
123 | }
124 |
125 | public function test_password_hydrator_ignores_password_when_password_sync_is_disabled()
126 | {
127 | $entry = new Entry;
128 | $model = new TestModelStub;
129 | $hydrator = new PasswordHydrator(['sync_passwords' => false], ['password' => 'secret']);
130 |
131 | $hydrator->hydrate($entry, $model);
132 |
133 | $this->assertFalse(Hash::needsRehash($model->password));
134 | $this->assertFalse(Hash::check('secret', $model->password));
135 | }
136 |
137 | public function test_password_hydrator_uses_models_get_auth_password_name_if_available()
138 | {
139 | $entry = new Entry;
140 | $model = new TestModelWithCustomPasswordStub;
141 | $hydrator = new PasswordHydrator;
142 |
143 | $hydrator->hydrate($entry, $model);
144 |
145 | $this->assertFalse(Hash::needsRehash($model->custom_password));
146 | }
147 |
148 | public function test_hydrator_uses_all_hydrators()
149 | {
150 | $entry = new Entry([
151 | 'bar' => 'baz',
152 | 'objectguid' => 'bf9679e7-0de6-11d0-a285-00aa003049e2',
153 | ]);
154 |
155 | $model = new TestModelStub;
156 |
157 | (new EloquentHydrator(['sync_attributes' => ['foo' => 'bar']]))
158 | ->hydrate($entry, $model);
159 |
160 | $this->assertEquals('baz', $model->foo);
161 | $this->assertEquals('default', $model->domain);
162 | $this->assertEquals($entry->getConvertedGuid(), $model->guid);
163 | }
164 | }
165 |
166 | class TestModelStub extends Model implements LdapAuthenticatable
167 | {
168 | use AuthenticatesWithLdap;
169 | }
170 |
171 | class TestModelWithCustomPasswordStub extends Model implements LdapAuthenticatable
172 | {
173 | use AuthenticatesWithLdap;
174 |
175 | public function getAuthPasswordName()
176 | {
177 | return 'custom_password';
178 | }
179 | }
180 |
181 | class TestAttributeHandlerHandleStub
182 | {
183 | public function handle($object, $eloquent)
184 | {
185 | $eloquent->foo = $object->getFirstAttribute('bar');
186 | }
187 | }
188 |
189 | class TestAttributeHandlerInvokableStub
190 | {
191 | public function __invoke($object, $eloquent)
192 | {
193 | $eloquent->foo = $object->getFirstAttribute('bar');
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/tests/Unit/LdapDatabaseManagerTest.php:
--------------------------------------------------------------------------------
1 | connection('default');
15 |
16 | $this->assertInstanceOf(Connection::class, $connection);
17 | $this->assertCount(1, $manager->getConnections());
18 | $this->assertArrayHasKey('default', $manager->getConnections());
19 | }
20 |
21 | public function test_database_connections_are_not_resolved_twice()
22 | {
23 | $manager = app(LdapDatabaseManager::class);
24 |
25 | $manager->connection('default');
26 | $manager->connection('default');
27 |
28 | $this->assertCount(1, $manager->getConnections());
29 | $this->assertArrayHasKey('default', $manager->getConnections());
30 | }
31 |
32 | public function test_initialized_database_sets_up_configuration()
33 | {
34 | app(LdapDatabaseManager::class)->connection('default');
35 |
36 | $this->assertEquals([
37 | 'driver' => 'sqlite',
38 | 'database' => ':memory:',
39 | ], config('database.connections.ldap_default'));
40 | }
41 |
42 | public function test_cached_database_can_be_initialized()
43 | {
44 | $manager = app(LdapDatabaseManager::class);
45 |
46 | $file = storage_path('framework/cache/ldap_directory.sqlite');
47 |
48 | file_put_contents($file, '');
49 |
50 | $manager->connection('default', ['database' => $file]);
51 |
52 | $this->assertFileExists($file);
53 |
54 | $this->assertEquals([
55 | 'driver' => 'sqlite',
56 | 'database' => $file,
57 | ], config('database.connections.ldap_default'));
58 |
59 | unlink($file);
60 | }
61 |
62 | public function test_tear_down_removes_tables()
63 | {
64 | $manager = app(LdapDatabaseManager::class);
65 |
66 | $schema = $manager->connection('default')->getSchemaBuilder();
67 | $this->assertTrue($schema->hasTable('ldap_objects'));
68 |
69 | $manager->teardown();
70 | $this->assertFalse($schema->hasTable('ldap_objects'));
71 | }
72 |
73 | public function test_tear_down_deletes_cached_database()
74 | {
75 | $manager = app(LdapDatabaseManager::class);
76 |
77 | $file = storage_path('framework/cache/ldap_directory.sqlite');
78 |
79 | file_put_contents($file, '');
80 |
81 | $manager->connection('default', ['database' => $file]);
82 |
83 | $this->assertFileExists($file);
84 |
85 | $manager->teardown();
86 |
87 | $this->assertFileDoesNotExist($file);
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/tests/Unit/LdapImporterTest.php:
--------------------------------------------------------------------------------
1 | increments('id');
30 | $table->softDeletes();
31 | $table->string('guid')->unique()->nullable();
32 | $table->string('domain')->nullable();
33 | $table->string('name')->nullable();
34 | });
35 |
36 | DirectoryEmulator::setup();
37 | }
38 |
39 | protected function tearDown(): void
40 | {
41 | Schema::dropIfExists('test_importer_group_model_stubs');
42 |
43 | parent::tearDown();
44 | }
45 |
46 | public function test_class_based_import_works()
47 | {
48 | $object = LdapGroup::create([
49 | 'objectguid' => $this->faker->uuid,
50 | 'cn' => 'Group',
51 | ]);
52 |
53 | $imported = (new Importer)
54 | ->setLdapModel(LdapGroup::class)
55 | ->setEloquentModel(TestImporterGroupModelStub::class)
56 | ->setSyncAttributes(['name' => 'cn'])
57 | ->execute();
58 |
59 | $this->assertCount(1, $imported);
60 | $this->assertTrue($imported->first()->exists);
61 | $this->assertEquals($object->getFirstAttribute('cn'), $imported->first()->name);
62 | }
63 |
64 | public function test_class_based_import_can_have_callable_importer()
65 | {
66 | $object = LdapGroup::create([
67 | 'objectguid' => $this->faker->uuid,
68 | 'cn' => 'Group',
69 | ]);
70 |
71 | $imported = (new Importer)
72 | ->setLdapModel(LdapGroup::class)
73 | ->setEloquentModel(TestImporterGroupModelStub::class)
74 | ->syncAttributesUsing(function ($object, $database) {
75 | $database
76 | ->forceFill(['name' => $object->getFirstAttribute('cn')])
77 | ->save();
78 | })->execute();
79 |
80 | $this->assertCount(1, $imported);
81 | $this->assertEquals($object->getFirstAttribute('cn'), $imported->first()->name);
82 | }
83 |
84 | public function test_scopes_can_be_applied_to_import_query()
85 | {
86 | LdapGroup::create([
87 | 'objectguid' => $this->faker->uuid,
88 | 'cn' => 'First Group',
89 | ]);
90 |
91 | LdapGroup::create([
92 | 'objectguid' => $this->faker->uuid,
93 | 'cn' => 'Second Group',
94 | ]);
95 |
96 | $imported = (new Importer)
97 | ->setLdapModel(LdapGroup::class)
98 | ->setSyncAttributes(['name' => 'cn'])
99 | ->setLdapScopes(TestImporterScopeStub::class)
100 | ->setEloquentModel(TestImporterGroupModelStub::class)
101 | ->execute();
102 |
103 | $this->assertCount(1, $imported);
104 | $this->assertEquals('Second Group', $imported->first()->name);
105 | }
106 | }
107 |
108 | class TestImporterScopeStub implements Scope
109 | {
110 | public function apply(Builder $query, LdapModel $model): void
111 | {
112 | $query->where('cn', 'Second Group');
113 | }
114 | }
115 |
116 | class TestImporterGroupModelStub extends Model implements LdapImportable
117 | {
118 | use ImportableFromLdap, SoftDeletes;
119 |
120 | public $timestamps = false;
121 |
122 | protected $guarded = [];
123 | }
124 |
--------------------------------------------------------------------------------
/tests/Unit/LdapServiceProviderTest.php:
--------------------------------------------------------------------------------
1 | artisan('vendor:publish', ['--provider' => LdapServiceProvider::class, '--no-interaction' => true]);
51 |
52 | $this->assertFileExists(config_path('ldap.php'));
53 | }
54 |
55 | public function test_connections_from_environment_variables_are_setup()
56 | {
57 | tap(Container::getConnection('alpha'), function (Connection $connection) {
58 | $config = $connection->getConfiguration();
59 |
60 | $this->assertEquals(['10.0.0.1', '10.0.0.2'], $config->get('hosts'));
61 | $this->assertEquals('dc=alpha,dc=com', $config->get('base_dn'));
62 | $this->assertEquals('cn=user,dc=alpha,dc=com', $config->get('username'));
63 | $this->assertEquals('alpha-secret', $config->get('password'));
64 | });
65 |
66 | tap(Container::getConnection('bravo'), function (Connection $connection) {
67 | $config = $connection->getConfiguration();
68 |
69 | $this->assertEquals(['172.0.0.1', '172.0.0.2'], $config->get('hosts'));
70 | $this->assertEquals('dc=bravo,dc=com', $config->get('base_dn'));
71 | $this->assertEquals('cn=user,dc=bravo,dc=com', $config->get('username'));
72 | $this->assertEquals('bravo-secret', $config->get('password'));
73 | });
74 | }
75 |
76 | public function test_custom_connection_options_from_env_are_loaded_into_configuration()
77 | {
78 | tap(Container::getConnection('bravo'), function (Connection $connection) {
79 | $config = $connection->getConfiguration();
80 |
81 | $this->assertEquals([
82 | LDAP_OPT_X_TLS_CACERTFILE => '/path',
83 | LDAP_OPT_X_TLS_CERTFILE => '/path',
84 | ], $config->get('options'));
85 | });
86 | }
87 |
88 | public function test_env_config_is_loaded_and_cacheable()
89 | {
90 | $this->assertEquals([
91 | 'default' => 'default',
92 | 'connections' => [
93 | 'default' => [
94 | 'hosts' => ['localhost'],
95 | 'username' => 'user',
96 | 'password' => 'secret',
97 | 'base_dn' => 'dc=local,dc=com',
98 | 'port' => 389,
99 | ],
100 | 'alpha' => [
101 | 'hosts' => ['10.0.0.1', '10.0.0.2'],
102 | 'username' => 'cn=user,dc=alpha,dc=com',
103 | 'password' => 'alpha-secret',
104 | 'base_dn' => 'dc=alpha,dc=com',
105 | 'port' => 389,
106 | 'timeout' => 5,
107 | ],
108 | 'bravo' => [
109 | 'hosts' => ['172.0.0.1', '172.0.0.2'],
110 | 'username' => 'cn=user,dc=bravo,dc=com',
111 | 'password' => 'bravo-secret',
112 | 'base_dn' => 'dc=bravo,dc=com',
113 | 'port' => 389,
114 | 'timeout' => 5,
115 | 'options' => [
116 | 24578 => '/path',
117 | 24580 => '/path',
118 | ],
119 | ],
120 | ],
121 | 'logging' => [
122 | 'enabled' => false,
123 | 'channel' => 'stack',
124 | 'level' => 'info',
125 | ],
126 | 'cache' => [
127 | 'enabled' => false,
128 | 'driver' => 'file',
129 | ],
130 | ], app('config')->all()['ldap']);
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/tests/Unit/LdapSynchronizerTest.php:
--------------------------------------------------------------------------------
1 | increments('id');
28 | $table->softDeletes();
29 | $table->string('guid')->unique()->nullable();
30 | $table->string('domain')->nullable();
31 | $table->string('name')->nullable();
32 | });
33 |
34 | DirectoryEmulator::setup();
35 | }
36 |
37 | protected function tearDown(): void
38 | {
39 | Schema::dropIfExists('test_synchronizer_group_model_stubs');
40 |
41 | parent::tearDown();
42 | }
43 |
44 | public function test_importer_fails_on_object_that_does_not_contain_guid()
45 | {
46 | $object = LdapGroup::create(['cn' => 'Group']);
47 |
48 | $synchronizer = new Synchronizer(TestSynchronizerGroupModelStub::class, [
49 | 'sync_attributes' => ['name' => 'cn'],
50 | ]);
51 |
52 | $this->expectException(LdapRecordException::class);
53 |
54 | $synchronizer->run($object);
55 | }
56 |
57 | public function test_importer_sets_configured_attributes()
58 | {
59 | $object = LdapGroup::create([
60 | 'objectguid' => $this->faker->uuid,
61 | 'cn' => 'Group',
62 | ]);
63 |
64 | $synchronizer = new Synchronizer(TestSynchronizerGroupModelStub::class, [
65 | 'sync_attributes' => ['name' => 'cn'],
66 | ]);
67 |
68 | /** @var TestSynchronizerGroupModelStub $group */
69 | $group = $synchronizer->run($object);
70 |
71 | $this->assertEquals('default', $group->getLdapDomain());
72 | $this->assertEquals($object->getConvertedGuid(), $group->getLdapGuid());
73 | $this->assertEquals($object->getName(), $group->name);
74 | $this->assertFalse($group->exists);
75 | }
76 |
77 | public function test_importer_locates_existing_model()
78 | {
79 | $guid = $this->faker->uuid;
80 |
81 | TestSynchronizerGroupModelStub::create(['guid' => $guid]);
82 |
83 | $object = LdapGroup::create([
84 | 'objectguid' => $guid,
85 | 'cn' => 'Group',
86 | ]);
87 |
88 | $synchronizer = new Synchronizer(TestSynchronizerGroupModelStub::class, [
89 | 'sync_attributes' => ['name' => 'cn'],
90 | ]);
91 |
92 | $group = $synchronizer->run($object);
93 |
94 | $this->assertTrue($group->exists);
95 | $this->assertEquals($guid, $group->guid);
96 | }
97 |
98 | public function test_importer_locates_soft_deleted_model()
99 | {
100 | $guid = $this->faker->uuid;
101 |
102 | $group = TestSynchronizerGroupModelStub::create(['guid' => $guid]);
103 |
104 | $group->delete();
105 |
106 | $object = LdapGroup::create([
107 | 'objectguid' => $guid,
108 | 'cn' => 'Group',
109 | ]);
110 |
111 | $synchronizer = new Synchronizer(TestSynchronizerGroupModelStub::class, [
112 | 'sync_attributes' => ['name' => 'cn'],
113 | ]);
114 |
115 | $imported = $synchronizer->run($object);
116 |
117 | $this->assertEquals($group->id, $imported->id);
118 | $this->assertTrue($imported->trashed());
119 | }
120 | }
121 |
122 | class TestSynchronizerGroupModelStub extends Model implements LdapImportable
123 | {
124 | use ImportableFromLdap, SoftDeletes;
125 |
126 | public $timestamps = false;
127 |
128 | protected $guarded = [];
129 | }
130 |
--------------------------------------------------------------------------------
/tests/Unit/LdapUserAuthenticatorTest.php:
--------------------------------------------------------------------------------
1 | getAuthenticatingModelMock($dn);
28 |
29 | $model->shouldReceive('getConnection')->once()->andReturn(
30 | m::mock(Connection::class, function ($connection) use ($dn) {
31 | $auth = m::mock(Guard::class);
32 | $auth->shouldReceive('attempt')->once()->withArgs([$dn, 'password'])->andReturnTrue();
33 |
34 | $connection->shouldReceive('auth')->once()->andReturn($auth);
35 | })
36 | );
37 |
38 | $auth = new LdapUserAuthenticator;
39 |
40 | Event::fake();
41 |
42 | $this->assertTrue($auth->attempt($model, 'password'));
43 |
44 | Event::assertDispatched(Binding::class);
45 | Event::assertDispatched(Bound::class);
46 | Event::assertNotDispatched(BindFailed::class);
47 | }
48 |
49 | public function test_attempt_failed()
50 | {
51 | $dn = 'cn=John Doe,dc=local,dc=com';
52 |
53 | $model = $this->getAuthenticatingModelMock($dn);
54 |
55 | $model->shouldReceive('getConnection')->once()->andReturn(
56 | m::mock(Connection::class, function ($connection) use ($dn) {
57 | $auth = m::mock(Guard::class);
58 | $auth->shouldReceive('attempt')->once()->withArgs([$dn, 'password'])->andReturnFalse();
59 |
60 | $connection->shouldReceive('auth')->once()->andReturn($auth);
61 | })
62 | );
63 |
64 | $auth = new LdapUserAuthenticator;
65 |
66 | Event::fake();
67 |
68 | $this->assertFalse($auth->attempt($model, 'password'));
69 |
70 | Event::assertDispatched(Binding::class);
71 | Event::assertDispatched(BindFailed::class);
72 | Event::assertNotDispatched(Bound::class);
73 | }
74 |
75 | public function test_auth_fails_due_to_rules()
76 | {
77 | $dn = 'cn=John Doe,dc=local,dc=com';
78 |
79 | $model = $this->getAuthenticatingModelMock($dn);
80 |
81 | $model->shouldReceive('getConnection')->once()->andReturn(
82 | m::mock(Connection::class, function ($connection) use ($dn) {
83 | $auth = m::mock(Guard::class);
84 | $auth->shouldReceive('attempt')->once()->withArgs([$dn, 'password'])->andReturnTrue();
85 |
86 | $connection->shouldReceive('auth')->once()->andReturn($auth);
87 | })
88 | );
89 |
90 | $auth = new LdapUserAuthenticator([TestFailingLdapAuthRule::class]);
91 |
92 | Event::fake();
93 |
94 | $this->assertFalse($auth->attempt($model, 'password'));
95 |
96 | Event::assertDispatched(Binding::class);
97 | Event::assertDispatched(Rejected::class);
98 | Event::assertNotDispatched(Bound::class);
99 | Event::assertNotDispatched(BindFailed::class);
100 | }
101 |
102 | public function test_eloquent_model_can_be_set()
103 | {
104 | $dn = 'cn=John Doe,dc=local,dc=com';
105 |
106 | $model = $this->getAuthenticatingModelMock($dn);
107 |
108 | $model->shouldReceive('getConnection')->once()->andReturn(
109 | m::mock(Connection::class, function ($connection) use ($dn) {
110 | $auth = m::mock(Guard::class);
111 | $auth->shouldReceive('attempt')->once()->withArgs([$dn, 'password'])->andReturnTrue();
112 |
113 | $connection->shouldReceive('auth')->once()->andReturn($auth);
114 | })
115 | );
116 |
117 | $auth = new LdapUserAuthenticator([TestLdapAuthRuleWithEloquentModel::class]);
118 |
119 | $auth->setEloquentModel(new TestLdapUserAuthenticatedModelStub);
120 |
121 | Event::fake();
122 |
123 | $this->assertTrue($auth->attempt($model, 'password'));
124 |
125 | Event::assertDispatched(Binding::class);
126 | Event::assertDispatched(Bound::class);
127 | Event::assertNotDispatched(Rejected::class);
128 | Event::assertNotDispatched(BindFailed::class);
129 | }
130 |
131 | protected function getAuthenticatingModelMock($dn)
132 | {
133 | $model = m::mock(Model::class);
134 | $model->shouldReceive('getDn')->once()->andReturn($dn);
135 |
136 | return $model;
137 | }
138 | }
139 |
140 | class TestLdapUserAuthenticatedModelStub extends EloquentModel
141 | {
142 | //
143 | }
144 |
145 | class TestLdapAuthRuleWithEloquentModel implements Rule
146 | {
147 | public function passes(LdapRecord $user, ?Eloquent $model = null): bool
148 | {
149 | return ! is_null($model);
150 | }
151 | }
152 |
153 | class TestFailingLdapAuthRule implements Rule
154 | {
155 | public function passes(LdapRecord $user, ?Eloquent $model = null): bool
156 | {
157 | return false;
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/tests/Unit/LdapUserRepositoryTest.php:
--------------------------------------------------------------------------------
1 | createModel();
17 |
18 | $this->assertInstanceOf(Entry::class, $model);
19 | }
20 |
21 | public function test_query_can_be_created()
22 | {
23 | $repository = new LdapUserRepository(Entry::class);
24 |
25 | $query = $repository->query();
26 |
27 | $this->assertInstanceOf(Builder::class, $query);
28 | $this->assertInstanceOf(Entry::class, $query->getModel());
29 | }
30 |
31 | public function test_query_selects_are_merged()
32 | {
33 | $repository = new LdapUserRepository(Entry::class);
34 |
35 | $this->assertEquals(['objectguid', '*'], $repository->query()->getSelects());
36 | }
37 |
38 | public function test_find_by_credentials_returns_null_with_empty_array()
39 | {
40 | $repository = new LdapUserRepository(Entry::class);
41 |
42 | $this->assertNull($repository->findByCredentials());
43 | }
44 |
45 | public function test_find_by_credentials_ignores_password()
46 | {
47 | $repository = m::mock(LdapUserRepository::class, function ($repository) {
48 | $query = m::mock(Builder::class);
49 | $query->shouldNotReceive('where');
50 | $query->shouldReceive('first')->once()->andReturnNull();
51 |
52 | $repository->makePartial()->shouldAllowMockingProtectedMethods();
53 | $repository->shouldReceive('newModelQuery')->once()->andReturn($query);
54 | });
55 |
56 | $this->assertNull($repository->findByCredentials(['password' => 'secret']));
57 | }
58 |
59 | public function test_find_by_credentials_returns_model()
60 | {
61 | $user = new Entry;
62 |
63 | $repository = m::mock(LdapUserRepository::class, function ($repository) use ($user) {
64 | $query = m::mock(Builder::class);
65 | $query->shouldReceive('where')->with('username', 'foo')->andReturnSelf();
66 | $query->shouldReceive('first')->once()->andReturn($user);
67 |
68 | $repository->makePartial()->shouldAllowMockingProtectedMethods();
69 | $repository->shouldReceive('newModelQuery')->once()->andReturn($query);
70 | });
71 |
72 | $this->assertSame($user, $repository->findByCredentials(['username' => 'foo']));
73 | }
74 |
75 | public function test_find_by_credentials_with_fallback_returns_model()
76 | {
77 | $user = new Entry;
78 |
79 | $repository = m::mock(LdapUserRepository::class, function ($repository) use ($user) {
80 | $query = m::mock(Builder::class);
81 | $query->shouldReceive('where')->with('username', 'foo')->andReturnSelf();
82 | $query->shouldReceive('first')->once()->andReturn($user);
83 |
84 | $repository->makePartial()->shouldAllowMockingProtectedMethods();
85 | $repository->shouldReceive('newModelQuery')->once()->andReturn($query);
86 | });
87 |
88 | $this->assertSame($user, $repository->findByCredentials(['username' => 'foo', 'fallback' => ['username' => 'foo']]));
89 | }
90 |
91 | public function test_find_by_attribute_and_value_returns_model()
92 | {
93 | $model = new Entry;
94 |
95 | $repository = m::mock(LdapUserRepository::class, function ($repository) use ($model) {
96 | $query = m::mock(Builder::class);
97 | $query->shouldReceive('findBy')->once()->with('foo', 'bar')->andReturn($model);
98 |
99 | $repository->makePartial()->shouldAllowMockingProtectedMethods();
100 | $repository->shouldReceive('newModelQuery')->once()->andReturn($query);
101 | });
102 |
103 | $this->assertSame($model, $repository->findBy('foo', 'bar'));
104 | }
105 |
106 | public function test_find_by_model_returns_model()
107 | {
108 | $model = new Entry;
109 |
110 | $repository = m::mock(LdapUserRepository::class, function ($repository) use ($model) {
111 | $query = m::mock(Builder::class);
112 | $query->shouldReceive('findByGuid')->once()->with('guid')->andReturn($model);
113 |
114 | $repository->makePartial()->shouldAllowMockingProtectedMethods();
115 | $repository->shouldReceive('newModelQuery')->once()->andReturn($query);
116 | });
117 |
118 | $authenticatable = m::mock(LdapAuthenticatable::class);
119 | $authenticatable->shouldReceive('getLdapGuid')->once()->andReturn('guid');
120 |
121 | $this->assertSame($model, $repository->findByModel($authenticatable));
122 | }
123 |
124 | public function test_find_by_model_returns_null_when_no_guid_is_present()
125 | {
126 | $repository = m::mock(LdapUserRepository::class, function ($repository) {
127 | $repository->makePartial()->shouldAllowMockingProtectedMethods();
128 | $repository->shouldNotReceive('newModelQuery');
129 | });
130 |
131 | $authenticatable = m::mock(LdapAuthenticatable::class);
132 | $authenticatable->shouldReceive('getLdapGuid')->once()->andReturnNull();
133 |
134 | $this->assertNull($repository->findByModel($authenticatable));
135 | }
136 |
137 | public function test_find_by_guid_returns_model()
138 | {
139 | $model = new Entry;
140 |
141 | $repository = m::mock(LdapUserRepository::class, function ($repository) use ($model) {
142 | $query = m::mock(Builder::class);
143 | $query->shouldReceive('findByGuid')->once()->with('guid')->andReturn($model);
144 |
145 | $repository->makePartial()->shouldAllowMockingProtectedMethods();
146 | $repository->shouldReceive('newModelQuery')->once()->andReturn($query);
147 | });
148 |
149 | $this->assertSame($model, $repository->findByGuid('guid'));
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/tests/Unit/ListenForLdapBindFailureTest.php:
--------------------------------------------------------------------------------
1 | listenForLdapBindFailure();
19 | }
20 |
21 | protected function username()
22 | {
23 | return 'email';
24 | }
25 |
26 | protected function makeDiagnosticErrorMessage($code)
27 | {
28 | return "80090308: LdapErr: DSID-0C09030B, comment: AcceptSecurityContext error, data {$code}, v893";
29 | }
30 |
31 | public function test_validation_exception_is_not_thrown_when_no_error_is_given()
32 | {
33 | $fake = DirectoryEmulator::setup('default');
34 |
35 | $this->assertFalse($fake->auth()->attempt('user', 'secret'));
36 | }
37 |
38 | public function test_validation_exception_is_not_thrown_when_invalid_credentials_is_returned()
39 | {
40 | $fake = DirectoryEmulator::setup('default');
41 |
42 | /** @var \LdapRecord\Testing\LdapFake $ldap */
43 | $ldap = $fake->getLdapConnection();
44 | $ldap->shouldReturnDiagnosticMessage(null);
45 | $ldap->shouldReturnError('Invalid credentials');
46 |
47 | $this->assertFalse($fake->auth()->attempt('user', 'secret'));
48 | }
49 |
50 | public function test_validation_exception_is_thrown_on_lost_connection()
51 | {
52 | $fake = DirectoryEmulator::setup('default');
53 |
54 | /** @var \LdapRecord\Testing\LdapFake $ldap */
55 | $ldap = $fake->getLdapConnection();
56 | $ldap->shouldReturnError("Can't contact LDAP server");
57 |
58 | $this->expectException(ValidationException::class);
59 |
60 | $fake->auth()->attempt('user', 'secret');
61 | }
62 |
63 | /**
64 | * @testWith
65 | * ["525"]
66 | * ["530"]
67 | * ["531"]
68 | * ["532"]
69 | * ["533"]
70 | * ["701"]
71 | * ["773"]
72 | * ["775"]
73 | */
74 | public function test_directory_throws_validation_error_with_code($code)
75 | {
76 | $fake = DirectoryEmulator::setup('default');
77 |
78 | /** @var \LdapRecord\Testing\LdapFake $ldap */
79 | $ldap = $fake->getLdapConnection();
80 | $ldap->shouldReturnDiagnosticMessage($this->makeDiagnosticErrorMessage($code));
81 |
82 | $this->expectException(ValidationException::class);
83 |
84 | $fake->auth()->attempt('user', 'secret');
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/tests/Unit/ValidatorRuleOnlyImportedTest.php:
--------------------------------------------------------------------------------
1 | assertFalse(
15 | (new OnlyImported)->passes(new Entry, new TestNonExistingOnlyImportedRuleModelStub)
16 | );
17 |
18 | $this->assertTrue(
19 | (new OnlyImported)->passes(new Entry, new TestExistingOnlyImportedRuleModelStub)
20 | );
21 | }
22 | }
23 |
24 | class TestNonExistingOnlyImportedRuleModelStub extends Model
25 | {
26 | public $exists = false;
27 | }
28 |
29 | class TestExistingOnlyImportedRuleModelStub extends Model
30 | {
31 | public $exists = true;
32 | }
33 |
--------------------------------------------------------------------------------
/tests/Unit/ValidatorTest.php:
--------------------------------------------------------------------------------
1 | assertEmpty((new Validator)->getRules());
21 | }
22 |
23 | public function test_rules_can_be_added()
24 | {
25 | $rule = new TestPassingRule;
26 | $validator = new Validator([$rule]);
27 |
28 | $this->assertCount(1, $validator->getRules());
29 | $this->assertSame($rule, $validator->getRules()[0]);
30 | }
31 |
32 | public function test_passing_validation_rule()
33 | {
34 | Event::fake(RulePassed::class);
35 |
36 | $rule = new TestPassingRule;
37 | $this->assertTrue((new Validator([$rule]))->passes(new Entry, new TestRuleModelStub));
38 |
39 | Event::assertDispatched(RulePassed::class);
40 | }
41 |
42 | public function test_failing_validation_rule()
43 | {
44 | Event::fake(RuleFailed::class);
45 |
46 | $rule = new TestFailingRule;
47 | $this->assertFalse((new Validator([$rule]))->passes(new Entry, new TestRuleModelStub));
48 |
49 | Event::assertDispatched(RuleFailed::class);
50 | }
51 |
52 | public function test_all_rules_are_validated()
53 | {
54 | $rule = new TestPassingRule;
55 |
56 | $validator = new Validator([$rule]);
57 |
58 | $this->assertTrue($validator->passes(new Entry, new TestRuleModelStub));
59 |
60 | $validator->addRule(new TestFailingRule);
61 |
62 | $this->assertFalse($validator->passes(new Entry, new TestRuleModelStub));
63 | }
64 | }
65 |
66 | class TestRuleModelStub extends Model
67 | {
68 | //
69 | }
70 |
71 | class TestPassingRule implements Rule
72 | {
73 | public function passes(LdapRecord $user, ?Eloquent $model = null): bool
74 | {
75 | return true;
76 | }
77 | }
78 |
79 | class TestFailingRule implements Rule
80 | {
81 | public function passes(LdapRecord $user, ?Eloquent $model = null): bool
82 | {
83 | return false;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------