Commit a29082c1 authored by stanislav.goldmann's avatar stanislav.goldmann

Refactoring of DCs and more flexible config

parent 1dc208df
......@@ -5,7 +5,8 @@
Very basic **READ ONLY** LDAP authentication driver for [Laravel 5.2+](http://laravel.com/)
Look [**HERE**](https://github.com/krenor/ldap-auth/tree/1.1.0) for the package for Laravel 5.1
Look [**HERE**](https://github.com/krenor/ldap-auth/tree/1.1.0) for the package for Laravel 5.1.
However, only the 5.2 Version will be maintained.
## Installation
......@@ -15,7 +16,7 @@ Add to your root composer.json and install with `composer install` or `composer
{
require: {
"krenor/ldap-auth": "~2.0"
"krenor/ldap-auth": "~2.*"
}
}
......@@ -25,8 +26,13 @@ or use `composer require krenor/ldap-auth` in your console.
Modify your `config/app.php` file and add the service provider to the providers array.
`Krenor\LdapAuth\LdapAuthServiceProvider::class,`
Krenor\LdapAuth\LdapAuthServiceProvider::class,
### Step 3: Publish the configuration file by running:
`php artisan vendor:publish --tag="ldap"`
Now you're all set!
## Configuration
......@@ -57,37 +63,10 @@ Update your `config/auth.php` to use **ldap** as authentication and the **LdapUs
```
### Step 2: Create an LDAP config
Add a **ldap.php** to your config directory.
It should look like this.
### Step 2: Adjust the LDAP config to your needs
```php
<?php
return [
'suffix' => '@example.local',
'domain_controller' => ['dns2.example.local', 'dns1.example.local'],
'base_dn' => 'OU=People,DC=example,DC=local',
// Indicates to use the hostnames sequentially. This means that this package
// will try dns2.example.local first. If it's down, it tries the next one
// If this is set to false, load balancing will be used instead (random domain controller)
'backup_rebind' => true,
// if using TLS this MUST be false
'ssl' => false,
// if using SSL this MUST be false
'tls' => false,
// Prevent anonymous bindings
'admin_user' => 'admin',
// Prevent anonymous bindings
'admin_pass' => 'admin'
];
```
You may use a single domain controller or multiple ones. Enter them as array, not as string!
```php
'domain_controller' => ['dns1.example.local']
```
If you have run `php artisan vendor:publish --tag="ldap"` you should see the
ldap.php file in your config directory. Adjust the values as you need them.
## Usage
......
{
"name": "krenor/ldap-auth",
"description": "Basic readonly authentication via LDAP for Laravel 5.1",
"description": "Basic readonly authentication via LDAP for Laravel 5.2",
"keywords": [
"laravel 5.2", "laravel", "laravel auth", "laravel authentication",
"ldap", "ldap authentication", "active directory",
......
<?php
namespace Krenor\LdapAuth\Connections;
abstract class DomainController
{
/**
* Connection Protocol.
*
* @var string
*/
protected $protocol;
/**
* Collection of domain controllers.
*
* @var array
*/
protected $domain_controller = [];
/**
* DomainController constructor.
*
* @param string $protocol
* @param array $domain_controller
*/
public function __construct($protocol, array $domain_controller)
{
$this->protocol = $protocol;
$this->domain_controller = $domain_controller;
}
/**
* Get the hostname for an LDAP binding.
*
* @return string
*/
abstract public function getHostname();
}
\ No newline at end of file
......@@ -4,26 +4,26 @@ namespace Krenor\LdapAuth\Connections;
use ErrorException;
use Krenor\LdapAuth\Contracts\ConnectionInterface;
use Krenor\LdapAuth\Contracts\DomainController;
use Krenor\LdapAuth\Exceptions\ConnectionException;
class LdapConnection implements ConnectionInterface
{
/**
* Array of domain controller(s) to balance LDAP queries
* Concrete strategy for getting the connection of the domain controllers
*
* @var array
* @var DomainController
*/
protected $domainController = [ ];
protected $domainController;
/**
* Indicates whether or not to use the array of domain controller sequentially
* So on downtime of a server it checks if the next one can be reached.
* If this is set to false load balancing is used instead for multiple dc's
* Indicates whether backup rebinding should be used.
* If this is set to false load balancing is used instead.
*
* @var bool
*/
protected $useBackup = false;
protected $backup = false;
/**
* Indicates whether or not to use SSL
......@@ -34,7 +34,6 @@ class LdapConnection implements ConnectionInterface
/**
* Indicates whether or not to use TLS
* If it's used ensure that ssl is set to false and vice-versa
*
* @var bool
*/
......@@ -49,29 +48,24 @@ class LdapConnection implements ConnectionInterface
/**
* Indicates whether or not the current connection is bound
*
* @var bool
*/
protected $bound = false;
/**
* Constructor
* LdapConnection constructor.
*
* @param array $config
*/
public function __construct(array $config)
{
$this->domainController = $config['domain_controller'];
$this->backup = $config['backup_rebind'];
$this->tls = $config['tls'];
$this->ssl = $config['ssl'];
if ($config['tls']) {
$this->tls = true;
}
if ($config['ssl']) {
$this->ssl = true;
}
if ($config['backup_rebind']) {
$this->useBackup = true;
}
$this->domainController = $this->getDomainControllerStrategy($config['domain_controller']);
}
......@@ -84,7 +78,7 @@ class LdapConnection implements ConnectionInterface
{
$port = $this->ssl ? $this::PORT_SSL : $this::PORT;
$hostname = $this->chooseDomainController();
$hostname = $this->domainController->getHostname();
return $this->connection = ldap_connect($hostname, $port);
}
......@@ -119,40 +113,6 @@ class LdapConnection implements ConnectionInterface
}
/**
* Chooses based on the configuration which domain controller to connect to
*
* @return string
*/
private function chooseDomainController()
{
$protocol = $this->ssl ? $this::PROTOCOL_SSL : $this::PROTOCOL;
$count = count($this->domainController);
if ($count === 1) {
// Single domain controller, so use this one
return $protocol . $this->domainController[0];
}
if ($this->useBackup === true) {
$connectionString = null;
foreach ($this->domainController as $dc) {
$connectionString .= $protocol . $dc . ' ';
}
// In case of using backup_rebind we have to build a string of all
// domain controller which will be walked through sequentially
return $connectionString;
}
$loadBalancedDC = $this->domainController[array_rand($this->domainController)];
// Otherwise use "load balancing" by using a random domain controller
return $protocol . $loadBalancedDC;
}
/**
* @param $option
* @param $value
......@@ -176,14 +136,14 @@ class LdapConnection implements ConnectionInterface
/**
* @param string $dn
* @param string $filter
* @param string $identifier
* @param array $fields
*
* @return resource
*/
public function search($dn, $filter, array $fields)
public function search($dn, $identifier, array $fields)
{
return ldap_search($this->connection, $dn, $filter, $fields);
return ldap_search($this->connection, $dn, $identifier, $fields);
}
......@@ -233,4 +193,27 @@ class LdapConnection implements ConnectionInterface
return $this->connection;
}
/**
* Get the concrete strategy class for retrieving the hostname.
*
* @param array $domain_controller
*
* @return \Krenor\LdapAuth\Connections\DomainController
*/
private function getDomainControllerStrategy(array $domain_controller)
{
$protocol = $this->ssl ? $this::PROTOCOL_SSL : $this::PROTOCOL;
if (count($domain_controller) === 1) {
return new SingleDomainController($protocol, $domain_controller);
}
if ($this->backup === true) {
return new RebindDomainController($protocol, $domain_controller);
} else {
return new LoadBalancingDomainController($protocol, $domain_controller);
}
}
}
\ No newline at end of file
<?php
namespace Krenor\LdapAuth\Connections;
class LoadBalancingDomainController extends DomainController
{
/**
* @return string
*/
public function getHostname()
{
$random_key = array_rand($this->domain_controller);
$random_dc = $this->domain_controller[$random_key];
return $this->protocol . $random_dc;
}
}
\ No newline at end of file
<?php
namespace Krenor\LdapAuth\Connections;
class RebindDomainController extends DomainController
{
/**
* @return string
*/
public function getHostname()
{
$hostname = null;
foreach ($this->domain_controller as $dc) {
$hostname .= $this->protocol . "$dc ";
}
return $hostname;
}
}
\ No newline at end of file
<?php
namespace Krenor\LdapAuth\Connections;
class SingleDomainController extends DomainController
{
/**
* @return string
*/
public function getHostname()
{
return $this->protocol . reset($this->domain_controller);
}
}
\ No newline at end of file
......@@ -84,12 +84,12 @@ interface ConnectionInterface
* Searches in LDAP with the scope of LDAP_SCOPE_SUBTREE
*
* @param string $dn
* @param string $filter
* @param string $identifier
* @param array $fields
*
* @return array
*/
public function search($dn, $filter, array $fields);
public function search($dn, $identifier, array $fields);
/**
* Check if connection is bound
......
<?php
namespace Krenor\LdapAuth\Exceptions;
use Exception;
class EmptySearchResultException extends Exception
{
public function __construct()
{
parent::__construct('The search query returned zero results.');
}
}
\ No newline at end of file
......@@ -9,7 +9,8 @@ class MissingConfigurationException extends Exception
public function __construct()
{
parent::__construct("Please ensure that a ldap.php file is present in the config/ root directory.");
parent::__construct('Please ensure that a ldap.php file is present in the config directory.
Try re-publishing using `php artisan vendor:publish --tag="ldap"`.');
}
}
\ No newline at end of file
......@@ -5,9 +5,11 @@ namespace Krenor\LdapAuth;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;
use Krenor\LdapAuth\Exceptions\MissingConfigurationException;
use Krenor\LdapAuth\Objects\Ldap;
class LdapAuthServiceProvider extends ServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
......@@ -15,6 +17,7 @@ class LdapAuthServiceProvider extends ServiceProvider
*/
protected $defer = false;
/**
* Perform post-registration booting of services.
*
......@@ -22,27 +25,35 @@ class LdapAuthServiceProvider extends ServiceProvider
*/
public function boot()
{
// Register 'ldap' as authentication method
Auth::provider('ldap', function($app){
// Create new LDAP connection based on configuration files
$ldap = new Ldap( $this->getLdapConfig() );
$config = __DIR__ . '/config/ldap.php';
return new LdapAuthUserProvider(
$ldap, $app['config']['auth']['providers']['ldap-users']['model']
);
});
// Add publishable configuration
$this->publishes([
$config => config_path('ldap.php'),
], 'ldap');
}
/**
* Register any package services.
* Register the service provider.
*
* @return void
*/
public function register()
{
//
// Register 'ldap' as authentication method
Auth::provider('ldap', function ($app) {
$model = $app['config']['auth']['providers']['ldap-users']['model'];
// Create a new LDAP connection
$connection = new Ldap($this->getLdapConfig());
return new LdapAuthUserProvider($connection, $model);
});
}
/**
* Get the services provided by the provider.
*
......@@ -50,9 +61,10 @@ class LdapAuthServiceProvider extends ServiceProvider
*/
public function provides()
{
return ['auth'];
return [ 'auth' ];
}
/**
* @return array
*
......@@ -60,7 +72,7 @@ class LdapAuthServiceProvider extends ServiceProvider
*/
private function getLdapConfig()
{
if( is_array($this->app['config']['ldap']) ){
if (is_array($this->app['config']['ldap'])) {
return $this->app['config']['ldap'];
}
......
......@@ -8,14 +8,14 @@ use Illuminate\Contracts\Auth\Authenticatable;
class LdapAuthUserProvider implements UserProvider
{
/**
* LDAP Object
* LDAP Wrapper.
*
* @var object
* @var Ldap
*/
protected $ldap;
/**
* Auth User Class
* LDAP Auth User Class.
*
* @var string
*/
......
<?php
namespace Krenor\LdapAuth;
namespace Krenor\LdapAuth\Objects;
use Krenor\LdapAuth\Connections\LdapConnection;
use Krenor\LdapAuth\Contracts\ConnectionInterface;
use Krenor\LdapAuth\Exceptions\EmptySearchResultException;
use Krenor\LdapAuth\Exceptions\MissingConfigurationException;
class Ldap {
class Ldap
{
/**
* The account suffix for the domain domain
* The current LDAP Connection.
*
* @var string
* @var LdapConnection
*/
protected $suffix;
protected $ldap;
/**
* The base distinguished name for the domain
* The account suffix for the domain.
*
* @var string
*/
protected $base_dn;
protected $suffix;
/**
* If no anonymous login is allowed
* The base distinguished name for the domain.
*
* @var string
*/
private $admin_user;
protected $base_dn;
/**
* If no anonymous login is allowed
* The filter to execute a search query on.
*
* @var string
*/
private $admin_pass;
private $search_filter;
/**
* Current LDAP Connection
* The fields to fetch from a search result.
*
* @var LdapConnection
* @var array
*/
protected $ldap;
protected $search_fields = [ ];
/**
* Default fields to fetch a search or read by
* User with permissions for preventing anonymous bindings.
*
* @var array
* @var string
*/
protected $fields = ['samaccountname', 'displayname', 'memberof'];
private $admin_user;
/**
* Default filter to execute a search query on
* Password of the user with permissions for preventing anonymous bindings.
*
* @var string
*/
private $search_filter = "sAMAccountName";
private $admin_pass;
/**
* Tries to connect and bind to the LDAP
......@@ -75,6 +78,7 @@ class Ldap {
$this->connect($this->ldap);
}
/**
* Initializes the connecting parameters.
* The actual connect happens with $this->ldap->bind()
......@@ -92,41 +96,44 @@ class Ldap {
$this->ldap->option(LDAP_OPT_TIMELIMIT, $connection::TIMELIMIT);
$this->ldap->option(LDAP_OPT_NETWORK_TIMEOUT, $connection::TIMELIMIT);
// For debug purposes only!
// For debug purposes only.
// $this->ldap->option(LDAP_OPT_DEBUG_LEVEL, 7);
$this->ldap->bind($this->admin_user, $this->admin_pass);
}
/**
* Execute a search query in the entire LDAP tree
* Execute a search query in the LDAP Base DN.
*
* @param string $filter msdn.microsoft.com/En-US/library/aa746475.aspx
* @param array $fields specific attributes to be returned. Defaults are set
* as $fields in this class. DN is always returned, no matter what.
* @param string $identifier msdn.microsoft.com/En-US/library/aa746475.aspx
* @param array $fields specific attributes to be returned
*
* @return array $entry|null
* @return array $entry
* @throws EmptySearchResultException
*/
public function find($filter, array $fields = [])
public function find($identifier, array $fields = [ ])
{
$results = $this->ldap->search(
// Get all result entries
$results = $this->ldap->search(
$this->base_dn,
$this->search_filter . '=' . $filter,
($fields ? $fields : $this->fields)
$this->search_filter . '=' . $identifier,
( $fields ?: $this->search_fields )
);
if(count($results) > 0){
if (count($results) > 0) {
$entry = $this->ldap->entry($results);
// Returning a single LDAP entry
if(isset($entry[0]) && !empty($entry[0])) {
if (isset( $entry[0] ) && ! empty( $entry[0] )) {
return $entry[0];
}
}
return null;
throw new EmptySearchResultException;
}
/**
* Rebinds with a given DN and Password
*
......@@ -142,6 +149,7 @@ class Ldap {
return $this->ldap->bind($username, $password);
}
/**
* Bind configuration file to class properties
* as long as these already exist
......@@ -152,11 +160,11 @@ class Ldap {
*/
private function bindConfig(array $config)
{
foreach($config as $key => $value){
if(property_exists($this, $key) ){
foreach ($config as $key => $value) {
if (property_exists($this, $key)) {
$this->{$key} = $value;
// Remove config key
unset($config[$key]);
unset( $config[$key] );
}
}
......
......@@ -149,12 +149,13 @@ class LdapUser implements UserContract, AuthorizableContract, LdapUserContract
private function buildAttributesFromLdap($entry)
{
$this->attributes['display_name'] = $entry['displayname'][0];
$this->attributes['samaccountname'] = $entry['samaccountname'][0];
$this->attributes['dn'] = $entry['dn'];
$this->attributes['member_of'] = $entry['memberof'];
// Just for readability, unsetting count as we only fetch one user
unset( $this->attributes['member_of']['count'] );
// Set the attributes accordingly to the search fields given
foreach($entry as $index => $key){
if(array_key_exists($index, config('ldap.search_fields'))){
$this->attributes[$key] = isset($entry[$key][1]) ? $entry[$key] : $entry[$key][0];
}
};
}
......
<?php
return [
'suffix' => '@dns.example.local',
/*
|--------------------------------------------------
| Domain Controllers
|--------------------------------------------------
|
| The domain controllers option is an array of servers located on your
| network that serve Active Directory. You can insert as many servers or
| as little as you'd like depending on your forest (with a minimum of one).
|
*/
'domain_controller' => [
'dns.example.local',
'dns-2.example.local'
],
/*
|--------------------------------------------------
| Base Distinguished Name
|--------------------------------------------------
|
| The base distinguished name is the base distinguished name you'd like
| to perform operations on. An example base DN would be DC=dns,DC=example,DC=local.
|
| If none defined, then it will try to find it automatically by querying your server.
| It's highly recommended to include it to limit queries executed per request.
|
*/
'base_dn' => 'OU=People,DC=dns,DC=example,DC=local',
/*
|--------------------------------------------------
| Search Filter
|--------------------------------------------------
|
| The filter option defines (you guessed it) on what filter to execute a query on.
| The default filter is "sAMAccountName". For more information please check
| msdn.microsoft.com/En-US/library/aa746475.aspx
|
*/
'search_filter' => 'sAMAccountName',
/*
|--------------------------------------------------
| Search Fields
|--------------------------------------------------
|
| The fields options defined what fields you want the be returned on a successful
| query result. Note: The distinguished name is always returned.
|
*/
'search_fields' => [
'samaccountname',
'displayname',
'memberof'
],
/*
|--------------------------------------------------
| Backup Rebinding
|--------------------------------------------------
|
| This options indicates to use the host names sequentially. This package will try
| to connect to the first domain controller. If it's not reachable the next DC
| will be tried.
|
| If this option is set to false load balancing will be used instead for multiple DC.
|
*/
'backup_rebind' => true,
/*
|--------------------------------------------------
| SSL & TLS
|--------------------------------------------------
|
| One of these options are recommended if you have the ability to connect to your server
| securely. Ensure that only one option can be true. The other one must be false.
|
*/
'ssl' => false,
'tls' => false,
/*
|--------------------------------------------------------------------------
| Administrator Username & Password
|--------------------------------------------------------------------------
|
| When connecting to your AD server, an administrator username and
| password is required to be able to query and run operations on
| your server(s). You can use any user account that has
| these permissions to prevent anonymous bindings.
|
*/
'admin_user' => 'admin',
'admin_pass' => 'admin',
];
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment