Server::class optimisations, minimal functional changes - basically caching/performance improvements

This commit is contained in:
Deon George 2025-04-27 12:07:48 +10:00
parent ea46cf36d0
commit 53880121b6
8 changed files with 113 additions and 94 deletions

View File

@ -9,8 +9,10 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use LdapRecord\LdapRecordException; use LdapRecord\LdapRecordException;
use LdapRecord\Models\Model; use LdapRecord\Models\Model;
use LdapRecord\Query\Builder;
use LdapRecord\Query\Collection as LDAPCollection; use LdapRecord\Query\Collection as LDAPCollection;
use LdapRecord\Query\ObjectNotFoundException; use LdapRecord\Query\ObjectNotFoundException;
@ -20,8 +22,7 @@ use App\Ldap\Entry;
final class Server final class Server
{ {
// Connection information used for these object and children private const LOGKEY = 'SVR';
private ?string $connection;
// This servers schema objectclasses // This servers schema objectclasses
private Collection $attributetypes; private Collection $attributetypes;
@ -35,22 +36,24 @@ final class Server
public const OC_ABSTRACT = 0x02; public const OC_ABSTRACT = 0x02;
public const OC_AUXILIARY = 0x03; public const OC_AUXILIARY = 0x03;
public function __construct(?string $connection=NULL) public function __construct()
{ {
$this->connection = $connection; $this->attributetypes = collect();
$this->ldapsyntaxes = collect();
$this->matchingrules = collect();
$this->objectclasses = collect();
} }
public function __get(string $key): mixed public function __get(string $key): mixed
{ {
return match($key) { return match($key) {
'attributetypes' => $this->attributetypes, 'attributetypes' => $this->attributetypes,
'connection' => $this->connection,
'ldapsyntaxes' => $this->ldapsyntaxes, 'ldapsyntaxes' => $this->ldapsyntaxes,
'matchingrules' => $this->matchingrules, 'matchingrules' => $this->matchingrules,
'objectclasses' => $this->objectclasses, 'objectclasses' => $this->objectclasses,
'config' => config('ldap.connections.'.config('ldap.default')), 'config' => config(sprintf('ldap.connections.%s',config('ldap.default'))),
'name' => Arr::get($this->config,'name',__('No Server Name Yet')), 'name' => Arr::get($this->config,'name',__('No Server Name Yet')),
default => throw new Exception('Unknown key:' . $key), default => throw new Exception('Unknown key:'.$key),
}; };
} }
@ -60,20 +63,14 @@ final class Server
* Gets the root DN of the specified LDAPServer, or throws an exception if it * Gets the root DN of the specified LDAPServer, or throws an exception if it
* can't find it. * can't find it.
* *
* @param string|null $connection Return a collection of baseDNs
* @param bool $objects Return a collection of Entry Models * @param bool $objects Return a collection of Entry Models
* @return Collection * @return Collection
* @throws ObjectNotFoundException
* @testedin GetBaseDNTest::testBaseDNExists(); * @testedin GetBaseDNTest::testBaseDNExists();
* @todo Need to allow for the scenario if the baseDN is not readable by ACLs
*/ */
public static function baseDNs(?string $connection=NULL,bool $objects=TRUE): Collection public static function baseDNs(bool $objects=FALSE): Collection
{ {
$cachetime = Carbon::now()
->addSeconds(Config::get('ldap.cache.time'));
try { try {
$base = self::rootDSE($connection,$cachetime); $rootdse = self::rootDSE();
/** /**
* LDAP Error Codes: * LDAP Error Codes:
@ -179,51 +176,100 @@ final class Server
} }
if (! $objects) if (! $objects)
return collect($base->namingcontexts); return collect($rootdse->namingcontexts);
return Cache::remember('basedns'.Session::id(),config('ldap.cache.time'),function() use ($rootdse) {
$result = collect(); $result = collect();
foreach ($base->namingcontexts as $dn)
$result->push((new Entry)->cache($cachetime)->findOrFail($dn));
return $result; // @note: Incase our rootDSE didnt return a namingcontext, we'll have no base DNs
foreach (($rootdse->namingcontexts ?: []) as $dn)
$result->push(self::get($dn)->read()->find($dn));
return $result->filter();
});
}
/**
* Work out if we should flush the cache when retrieving an entry
*
* @param string $dn
* @return bool
* @note: We dont need to flush the cache for internal LDAP attributes, as we dont change them
*/
private static function cacheflush(string $dn): bool
{
$cache = (! config('ldap.cache.enabled'))
|| match (strtolower($dn)) {
'','cn=schema','cn=subschema' => FALSE,
default => TRUE,
};
Log::debug(sprintf('%s:%s - %s',self::LOGKEY,$cache ? 'Caching' : 'Not Cached',$dn));
return $cache;
}
/**
* Return our cache time as per the configuration
*
* @return Carbon
*/
private static function cachetime(): Carbon
{
return Carbon::now()
->addSeconds(Config::get('ldap.cache.time'));
}
/**
* Generic Builder method to setup our queries consistently - mainly to ensure we cache results
*
* @param string $dn
* @param array $attrs
* @return Builder
*/
private static function get(string $dn,array $attrs=['*','+']): Builder
{
return Entry::query()
->setDN($dn)
->cache(
until: self::cachetime(),
flush: self::cacheflush($dn))
->select($attrs);
} }
/** /**
* Obtain the rootDSE for the server, that gives us server information * Obtain the rootDSE for the server, that gives us server information
* *
* @param string|null $connection * @return Model
* @param Carbon|null $cachetime
* @return Entry|null
* @throws ObjectNotFoundException * @throws ObjectNotFoundException
* @testedin TranslateOidTest::testRootDSE(); * @testedin TranslateOidTest::testRootDSE();
* @note While we are using a static variable for in session performance, we'll also cache the result normally
*/ */
public static function rootDSE(?string $connection=NULL,?Carbon $cachetime=NULL): ?Model public static function rootDSE(): Model
{ {
$e = new Entry; static $rootdse = NULL;
return Entry::on($connection ?? $e->getConnectionName()) if (is_null($rootdse))
->cache($cachetime) $rootdse = self::get('',['+','*'])
->in(NULL)
->read() ->read()
->select(['+'])
->whereHas('objectclass')
->firstOrFail(); ->firstOrFail();
return $rootdse;
} }
/** /**
* Get the Schema DN * Get the Schema DN
* *
* @param string|null $connection
* @return string * @return string
* @throws ObjectNotFoundException * @throws ObjectNotFoundException
*/ */
public static function schemaDN(?string $connection=NULL): string public static function schemaDN(): string
{ {
$cachetime = Carbon::now()->addSeconds(Config::get('ldap.cache.time')); return collect(self::rootDSE()->subschemasubentry)
->first();
return collect(self::rootDSE($connection,$cachetime)->subschemasubentry)->first();
} }
/* METHODS */
/** /**
* Query the server for a DN and return its children and if those children have children. * Query the server for a DN and return its children and if those children have children.
* *
@ -233,17 +279,16 @@ final class Server
*/ */
public function children(string $dn,array $attrs=['dn']): ?LDAPCollection public function children(string $dn,array $attrs=['dn']): ?LDAPCollection
{ {
return ($x=(new Entry) return $this
->on($this->connection) ->get(
->cache(Carbon::now()->addSeconds(Config::get('ldap.cache.time'))) dn: $dn,
->select(array_merge($attrs,[ attrs: array_merge($attrs,[
'hassubordinates', // Needed for the tree to know if an entry has children 'hassubordinates', // Needed for the tree to know if an entry has children
'c' // Needed for the tree to show icons for countries 'c' // Needed for the tree to show icons for countries
])) ]))
->setDn($dn)
->list() ->list()
->orderBy('dn') ->orderBy('dn')
->get()) ? $x : NULL; ->get() ?: NULL;
} }
/** /**
@ -251,15 +296,13 @@ final class Server
* *
* @param string $dn * @param string $dn
* @param array $attrs * @param array $attrs
* @return Entry|null * @return Model|null
*/ */
public function fetch(string $dn,array $attrs=['*','+']): ?Entry public function fetch(string $dn,array $attrs=['*','+']): ?Model
{ {
return ($x=(new Entry) return $this->get($dn,$attrs)
->on($this->connection) ->read()
->cache(Carbon::now()->addSeconds(Config::get('ldap.cache.time'))) ->first() ?: NULL;
->select($attrs)
->find($dn)) ? $x : NULL;
} }
/** /**
@ -303,34 +346,11 @@ final class Server
// First pass if we have already retrieved the schema item // First pass if we have already retrieved the schema item
switch ($item) { switch ($item) {
case 'attributetypes': case 'attributetypes':
if (isset($this->attributetypes))
return $this->attributetypes;
else
$this->attributetypes = collect();
break;
case 'ldapsyntaxes': case 'ldapsyntaxes':
if (isset($this->ldapsyntaxes))
return $this->ldapsyntaxes;
else
$this->ldapsyntaxes = collect();
break;
case 'matchingrules': case 'matchingrules':
if (isset($this->matchingrules))
return $this->matchingrules;
else
$this->matchingrules = collect();
break;
case 'objectclasses': case 'objectclasses':
if (isset($this->objectclasses)) if ($this->{$item}->count())
return $this->objectclasses; return $this->{$item};
else
$this->objectclasses = collect();
break; break;
@ -340,7 +360,7 @@ final class Server
} }
// Try to get the schema DN from the specified entry. // Try to get the schema DN from the specified entry.
$schema_dn = $this->schemaDN($this->connection); $schema_dn = $this->schemaDN();
$schema = $this->fetch($schema_dn); $schema = $this->fetch($schema_dn);
// If our schema's null, we didnt find it. // If our schema's null, we didnt find it.
@ -349,7 +369,7 @@ final class Server
switch ($item) { switch ($item) {
case 'attributetypes': case 'attributetypes':
Log::debug('Attribute Types'); Log::debug(sprintf('%s:Attribute Types',self::LOGKEY));
// build the array of attribueTypes // build the array of attribueTypes
//$syntaxes = $this->SchemaSyntaxes($dn); //$syntaxes = $this->SchemaSyntaxes($dn);
@ -367,7 +387,7 @@ final class Server
* with its name set to the alias name, and all other data copied.*/ * with its name set to the alias name, and all other data copied.*/
if ($o->aliases->count()) { if ($o->aliases->count()) {
Log::debug(sprintf('\ Attribute [%s] has the following aliases [%s]',$o->name,$o->aliases->join(','))); Log::debug(sprintf('%s:\ Attribute [%s] has the following aliases [%s]',self::LOGKEY,$o->name,$o->aliases->join(',')));
foreach ($o->aliases as $alias) { foreach ($o->aliases as $alias) {
$new_attr = clone $o; $new_attr = clone $o;
@ -427,7 +447,7 @@ final class Server
return $this->attributetypes; return $this->attributetypes;
case 'ldapsyntaxes': case 'ldapsyntaxes':
Log::debug('LDAP Syntaxes'); Log::debug(sprintf('%s:LDAP Syntaxes',self::LOGKEY));
foreach ($schema->{$item} as $line) { foreach ($schema->{$item} as $line) {
if (is_null($line) || ! strlen($line)) if (is_null($line) || ! strlen($line))
@ -440,7 +460,7 @@ final class Server
return $this->ldapsyntaxes; return $this->ldapsyntaxes;
case 'matchingrules': case 'matchingrules':
Log::debug('Matching Rules'); Log::debug(sprintf('%s:Matching Rules',self::LOGKEY));
$this->matchingruleuse = collect(); $this->matchingruleuse = collect();
foreach ($schema->{$item} as $line) { foreach ($schema->{$item} as $line) {
@ -481,7 +501,7 @@ final class Server
return $this->matchingrules; return $this->matchingrules;
case 'objectclasses': case 'objectclasses':
Log::debug('Object Classes'); Log::debug(sprintf('%s:Object Classes',self::LOGKEY));
foreach ($schema->{$item} as $line) { foreach ($schema->{$item} as $line) {
if (is_null($line) || ! strlen($line)) if (is_null($line) || ! strlen($line))

View File

@ -17,10 +17,11 @@ class AjaxController extends Controller
* *
* @return Collection * @return Collection
* @throws \LdapRecord\Query\ObjectNotFoundException * @throws \LdapRecord\Query\ObjectNotFoundException
* @todo This should be consolidated with HomeController
*/ */
public function bases(): Collection public function bases(): Collection
{ {
$base = Server::baseDNs() ?: collect(); $base = Server::baseDNs(TRUE) ?: collect();
return $base return $base
->transform(fn($item)=> ->transform(fn($item)=>

View File

@ -28,7 +28,7 @@ class HomeController extends Controller
{ {
private function bases(): Collection private function bases(): Collection
{ {
$base = Server::baseDNs() ?: collect(); $base = Server::baseDNs(TRUE) ?: collect();
return $base->transform(function($item) { return $base->transform(function($item) {
return [ return [

View File

@ -31,7 +31,7 @@ class LdapUserRepository extends LdapUserRepositoryBase
return $this->query()->find($credentials['dn']); return $this->query()->find($credentials['dn']);
// Look for a user using all our baseDNs // Look for a user using all our baseDNs
foreach (Server::baseDNs() as $base) { foreach (Server::baseDNs(FALSE) as $base) {
$query = $this->query()->setBaseDn($base); $query = $this->query()->setBaseDn($base);
foreach ($credentials as $key => $value) { foreach ($credentials as $key => $value) {
@ -67,7 +67,7 @@ class LdapUserRepository extends LdapUserRepositoryBase
public function findByGuid($guid): ?Model public function findByGuid($guid): ?Model
{ {
// Look for a user using all our baseDNs // Look for a user using all our baseDNs
foreach (Server::baseDNs() as $base) { foreach (Server::baseDNs(FALSE) as $base) {
$user = $this->query()->setBaseDn($base)->findByGuid($guid); $user = $this->query()->setBaseDn($base)->findByGuid($guid);
if ($user) if ($user)

View File

@ -24,7 +24,7 @@
<td>BaseDN(s)</td> <td>BaseDN(s)</td>
<td> <td>
<table class="table table-sm table-borderless"> <table class="table table-sm table-borderless">
@foreach($server->baseDNs()->sort(fn($item)=>$item->sort_key) as $item) @foreach($server->baseDNs(TRUE)->sort(fn($item)=>$item->sort_key) as $item)
<tr> <tr>
<td class="ps-0">{{ $item->getDn() }}</td> <td class="ps-0">{{ $item->getDn() }}</td>
</tr> </tr>

View File

@ -1,11 +1,10 @@
@use(App\Classes\LDAP\Server)
@extends('layouts.dn') @extends('layouts.dn')
@section('page_title') @section('page_title')
<table class="table table-borderless"> <table class="table table-borderless">
<tr> <tr>
<td style="border-radius: 5px;"><div class="page-title-icon f32"><i class="fas fa-fingerprint"></i></div></td> <td style="border-radius: 5px;"><div class="page-title-icon f32"><i class="fas fa-fingerprint"></i></div></td>
<td class="top text-end align-text-top p-2"><strong>{{ Server::schemaDN() }}</strong></td> <td class="top text-end align-text-top p-2"><strong>{{ $server->schemaDN() }}</strong></td>
</tr> </tr>
</table> </table>
@endsection @endsection

View File

@ -12,7 +12,7 @@ class ExampleTest extends TestCase
*/ */
public function test_the_application_returns_a_successful_response(): void public function test_the_application_returns_a_successful_response(): void
{ {
$response = $this->get('/'); $response = $this->get('/login');
$response->assertStatus(200); $response->assertStatus(200);
} }

View File

@ -13,11 +13,10 @@ class GetBaseDNTest extends TestCase
* *
* @return void * @return void
* @throws \LdapRecord\Query\ObjectNotFoundException * @throws \LdapRecord\Query\ObjectNotFoundException
* @covers \App\Classes\LDAP\Server::baseDNs()
*/ */
public function testBaseDnExists() public function testBaseDnExists()
{ {
$o = Server::baseDNs(); $o = Server::baseDNs(TRUE);
$this->assertIsObject($o); $this->assertIsObject($o);
$this->assertCount(6,$o->toArray()); $this->assertCount(6,$o->toArray());