Fix excess memory being used when building schema
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 37s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m23s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 3m29s
Create Docker Image / Final Docker Image Manifest (push) Successful in 11s

This commit is contained in:
Deon George 2025-01-13 22:03:47 +11:00
parent fcec58441f
commit db4b90183f
7 changed files with 110 additions and 116 deletions

View File

@ -2,12 +2,12 @@
namespace App\Classes\LDAP\Schema; namespace App\Classes\LDAP\Schema;
use Config;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use App\Classes\LDAP\Server; use App\Classes\LDAP\Server;
use App\Exceptions\InvalidUsage; use App\Exceptions\InvalidUsage;
use App\Ldap\Entry;
/** /**
* Represents an LDAP Schema objectClass * Represents an LDAP Schema objectClass
@ -15,10 +15,8 @@ use App\Ldap\Entry;
* @package phpLDAPadmin * @package phpLDAPadmin
* @subpackage Schema * @subpackage Schema
*/ */
final class ObjectClass extends Base { final class ObjectClass extends Base
// The server ID that this objectclass belongs to. {
private Server $server;
// Array of objectClass names from which this objectClass inherits // Array of objectClass names from which this objectClass inherits
private Collection $sup_classes; private Collection $sup_classes;
@ -39,15 +37,14 @@ final class ObjectClass extends Base {
private bool $is_obsolete; private bool $is_obsolete;
/* ObjectClass Types */
private const OC_STRUCTURAL = 0x01;
private const OC_ABSTRACT = 0x02;
private const OC_AUXILIARY = 0x03;
/** /**
* Creates a new ObjectClass object given a raw LDAP objectClass string. * Creates a new ObjectClass object given a raw LDAP objectClass string.
* *
* eg: ( 2.5.6.0 NAME 'top' DESC 'top of the superclass chain' ABSTRACT MUST objectClass ) * eg: ( 2.5.6.0 NAME 'top' DESC 'top of the superclass chain' ABSTRACT MUST objectClass )
*
* @param string $line Schema Line
* @param Server $server
* @todo Change $server to $connection, no need to store the server object here
*/ */
public function __construct(string $line,Server $server) public function __construct(string $line,Server $server)
{ {
@ -59,7 +56,6 @@ final class ObjectClass extends Base {
$strings = preg_split('/[\s,]+/',$line,-1,PREG_SPLIT_DELIM_CAPTURE); $strings = preg_split('/[\s,]+/',$line,-1,PREG_SPLIT_DELIM_CAPTURE);
// Init // Init
$this->server = $server;
$this->may_attrs = collect(); $this->may_attrs = collect();
$this->may_force = collect(); $this->may_force = collect();
$this->must_attrs = collect(); $this->must_attrs = collect();
@ -138,21 +134,21 @@ final class ObjectClass extends Base {
break; break;
case 'ABSTRACT': case 'ABSTRACT':
$this->type = self::OC_ABSTRACT; $this->type = Server::OC_ABSTRACT;
if (static::DEBUG_VERBOSE) if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case ABSTRACT returned (%s)',$this->type)); Log::debug(sprintf('- Case ABSTRACT returned (%s)',$this->type));
break; break;
case 'STRUCTURAL': case 'STRUCTURAL':
$this->type = self::OC_STRUCTURAL; $this->type = Server::OC_STRUCTURAL;
if (static::DEBUG_VERBOSE) if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case STRUCTURAL returned (%s)',$this->type)); Log::debug(sprintf('- Case STRUCTURAL returned (%s)',$this->type));
break; break;
case 'AUXILIARY': case 'AUXILIARY':
$this->type = self::OC_AUXILIARY; $this->type = Server::OC_AUXILIARY;
if (static::DEBUG_VERBOSE) if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case AUXILIARY returned (%s)',$this->type)); Log::debug(sprintf('- Case AUXILIARY returned (%s)',$this->type));
@ -212,34 +208,29 @@ final class ObjectClass extends Base {
public function __get(string $key): mixed public function __get(string $key): mixed
{ {
switch ($key) { return match ($key) {
case 'attributes': 'attributes' => $this->getAllAttrs(),
return $this->getAllAttrs(); 'sup' => $this->sup_classes,
'type_name' => match ($this->type) {
case 'sup': Server::OC_STRUCTURAL => 'Structural',
return $this->sup_classes; Server::OC_ABSTRACT => 'Abstract',
Server::OC_AUXILIARY => 'Auxiliary',
case 'type_name': default => throw new InvalidUsage('Unknown ObjectClass Type: ' . $this->type),
switch ($this->type) { },
case self::OC_STRUCTURAL: return 'Structural'; default => parent::__get($key),
case self::OC_ABSTRACT: return 'Abstract'; };
case self::OC_AUXILIARY: return 'Auxiliary';
default:
throw new InvalidUsage('Unknown ObjectClass Type: '.$this->type);
}
default: return parent::__get($key);
}
} }
/** /**
* Return a list of attributes that this objectClass provides * Return a list of attributes that this objectClass provides
* *
* @return Collection * @return Collection
* @throws InvalidUsage
*/ */
public function getAllAttrs(): Collection public function getAllAttrs(): Collection
{ {
return $this->getMustAttrs()->merge($this->getMayAttrs()); return $this->getMustAttrs()
->merge($this->getMayAttrs());
} }
/** /**
@ -250,9 +241,8 @@ final class ObjectClass extends Base {
*/ */
public function addChildObjectClass(string $name): void public function addChildObjectClass(string $name): void
{ {
if ($this->child_objectclasses->search($name) === FALSE) { if (! $this->child_objectclasses->has($name))
$this->child_objectclasses->push($name); $this->child_objectclasses->push($name);
}
} }
/** /**
@ -321,14 +311,13 @@ final class ObjectClass extends Base {
{ {
// If we dont need our parents, then we'll just return ours. // If we dont need our parents, then we'll just return ours.
if (! $parents) if (! $parents)
return $this->may_attrs->sortBy(function($item) { return strtolower($item->name.$item->source); }); return $this->may_attrs
->sortBy(fn($item)=>strtolower($item->name.$item->source));
$attrs = $this->may_attrs; $attrs = $this->may_attrs;
foreach ($this->getParents() as $object_class) { foreach ($this->getParents() as $object_class)
$sc = $this->server->schema('objectclasses',$object_class); $attrs = $attrs->merge($object_class->getMayAttrs($parents));
$attrs = $attrs->merge($sc->getMayAttrs($parents));
}
// Remove any duplicates // Remove any duplicates
$attrs = $attrs->unique(function($item) { return $item->name; }); $attrs = $attrs->unique(function($item) { return $item->name; });
@ -378,10 +367,8 @@ final class ObjectClass extends Base {
$attrs = $this->must_attrs; $attrs = $this->must_attrs;
foreach ($this->getParents() as $object_class) { foreach ($this->getParents() as $object_class)
$sc = $this->server->schema('objectclasses',$object_class); $attrs = $attrs->merge($object_class->getMustAttrs($parents));
$attrs = $attrs->merge($sc->getMustAttrs($parents));
}
// Remove any duplicates // Remove any duplicates
$attrs = $attrs->unique(function($item) { return $item->name; }); $attrs = $attrs->unique(function($item) { return $item->name; });
@ -423,12 +410,13 @@ final class ObjectClass extends Base {
$result = collect(); $result = collect();
foreach ($this->sup_classes as $object_class) { foreach ($this->sup_classes as $object_class) {
$result->push($object_class); $oc = Config::get('server')
->schema('objectclasses',$object_class);
$oc = $this->server->schema('objectclasses',$object_class); if ($oc) {
$result->push($oc);
if ($oc)
$result = $result->merge($oc->getParents()); $result = $result->merge($oc->getParents());
}
} }
return $result; return $result;
@ -476,19 +464,16 @@ final class ObjectClass extends Base {
if (in_array_ignore_case($this->name,$oclass)) if (in_array_ignore_case($this->name,$oclass))
return FALSE; return FALSE;
foreach ($oclass as $object_class) { foreach ($oclass as $object_class)
$oc = $this->server->schema('objectclasses',$object_class); if ($object_class->isStructural() && in_array_ignore_case($this->name,$object_class->getParents()->pluck('name')))
if ($oc->isStructural() && in_array_ignore_case($this->name,$oc->getParents()))
return TRUE; return TRUE;
}
return FALSE; return FALSE;
} }
public function isStructural(): bool public function isStructural(): bool
{ {
return $this->type === self::OC_STRUCTURAL; return $this->type === Server::OC_STRUCTURAL;
} }
/** /**

View File

@ -21,6 +21,9 @@ use App\Ldap\Entry;
final class Server final class Server
{ {
// Connection information used for these object and children
private ?string $connection;
// This servers schema objectclasses // This servers schema objectclasses
private Collection $attributetypes; private Collection $attributetypes;
private Collection $ldapsyntaxes; private Collection $ldapsyntaxes;
@ -28,25 +31,26 @@ final class Server
private Collection $matchingruleuse; private Collection $matchingruleuse;
private Collection $objectclasses; private Collection $objectclasses;
// Valid items that can be fetched /* ObjectClass Types */
public const schema_types = [ public const OC_STRUCTURAL = 0x01;
'objectclasses', public const OC_ABSTRACT = 0x02;
'attributetypes', public const OC_AUXILIARY = 0x03;
'ldapsyntaxes',
'matchingrules', public function __construct(string $connection=NULL)
]; {
$this->connection = $connection;
}
public function __get(string $key): mixed public function __get(string $key): mixed
{ {
switch ($key) { return match($key) {
case 'attributetypes': return $this->attributetypes; 'attributetypes' => $this->attributetypes,
case 'ldapsyntaxes': return $this->ldapsyntaxes; 'connection' => $this->connection,
case 'matchingrules': return $this->matchingrules; 'ldapsyntaxes' => $this->ldapsyntaxes,
case 'objectclasses': return $this->objectclasses; 'matchingrules' => $this->matchingrules,
'objectclasses' => $this->objectclasses,
default: default => throw new Exception('Unknown key:' . $key),
throw new Exception('Unknown key:'.$key); };
}
} }
/* STATIC METHODS */ /* STATIC METHODS */
@ -62,9 +66,10 @@ final class Server
* @testedin GetBaseDNTest::testBaseDNExists(); * @testedin GetBaseDNTest::testBaseDNExists();
* @todo Need to allow for the scenario if the baseDN is not readable by ACLs * @todo Need to allow for the scenario if the baseDN is not readable by ACLs
*/ */
public static function baseDNs($connection=NULL,bool $objects=TRUE): Collection public static function baseDNs(string $connection='default',bool $objects=TRUE): Collection
{ {
$cachetime = Carbon::now()->addSeconds(Config::get('ldap.cache.time')); $cachetime = Carbon::now()
->addSeconds(Config::get('ldap.cache.time'));
try { try {
$base = self::rootDSE($connection,$cachetime); $base = self::rootDSE($connection,$cachetime);
@ -163,7 +168,7 @@ final class Server
*/ */
// If we cannot get to our LDAP server we'll head straight to the error page // If we cannot get to our LDAP server we'll head straight to the error page
} catch (LdapRecordException $e) { } catch (LdapRecordException $e) {
switch ($e->getDetailedError()->getErrorCode()) { switch ($e->getDetailedError()?->getErrorCode()) {
case 49: case 49:
// Since we failed authentication, we should delete our auth cookie // Since we failed authentication, we should delete our auth cookie
if (Cookie::has('password_encrypt')) { if (Cookie::has('password_encrypt')) {
@ -178,7 +183,7 @@ final class Server
abort(401,$e->getDetailedError()->getErrorMessage()); abort(401,$e->getDetailedError()->getErrorMessage());
default: default:
abort(597,$e->getDetailedError()->getErrorMessage()); abort(597,$e->getDetailedError()?->getErrorMessage() ?: $e->getMessage());
} }
} }
@ -192,9 +197,8 @@ final class Server
* @todo Possibly a bug wtih ldaprecord, so need to investigate * @todo Possibly a bug wtih ldaprecord, so need to investigate
*/ */
$result = collect(); $result = collect();
foreach ($base->namingcontexts as $dn) { foreach ($base->namingcontexts as $dn)
$result->push((new Entry)->cache($cachetime)->findOrFail($dn)); $result->push((new Entry)->cache($cachetime)->findOrFail($dn));
}
return $result; return $result;
} }
@ -207,7 +211,7 @@ final class Server
* @throws ObjectNotFoundException * @throws ObjectNotFoundException
* @testedin TranslateOidTest::testRootDSE(); * @testedin TranslateOidTest::testRootDSE();
*/ */
public static function rootDSE($connection=NULL,Carbon $cachetime=NULL): ?Model public static function rootDSE(string $connection=NULL,Carbon $cachetime=NULL): ?Model
{ {
$e = new Entry; $e = new Entry;
@ -227,7 +231,7 @@ final class Server
* @return string * @return string
* @throws ObjectNotFoundException * @throws ObjectNotFoundException
*/ */
public static function schemaDN($connection=NULL): string public static function schemaDN(string $connection=NULL): string
{ {
$cachetime = Carbon::now()->addSeconds(Config::get('ldap.cache.time')); $cachetime = Carbon::now()->addSeconds(Config::get('ldap.cache.time'));
@ -243,7 +247,7 @@ final class Server
public function children(string $dn): ?LDAPCollection public function children(string $dn): ?LDAPCollection
{ {
return ($x=(new Entry) return ($x=(new Entry)
->query() ->on($this->connection)
->cache(Carbon::now()->addSeconds(Config::get('ldap.cache.time'))) ->cache(Carbon::now()->addSeconds(Config::get('ldap.cache.time')))
->select(['*','hassubordinates']) ->select(['*','hassubordinates'])
->setDn($dn) ->setDn($dn)
@ -261,7 +265,7 @@ final class Server
public function fetch(string $dn,array $attrs=['*','+']): ?Entry public function fetch(string $dn,array $attrs=['*','+']): ?Entry
{ {
return ($x=(new Entry) return ($x=(new Entry)
->query() ->on($this->connection)
->cache(Carbon::now()->addSeconds(Config::get('ldap.cache.time'))) ->cache(Carbon::now()->addSeconds(Config::get('ldap.cache.time')))
->select($attrs) ->select($attrs)
->find($dn)) ? $x : NULL; ->find($dn)) ? $x : NULL;
@ -298,17 +302,13 @@ final class Server
* @return Collection|Base|NULL * @return Collection|Base|NULL
* @throws InvalidUsage * @throws InvalidUsage
*/ */
public function schema(string $item,string $key=NULL): Collection|Base|NULL public function schema(string $item,string $key=NULL): Collection|LDAPSyntax|Base|NULL
{ {
// Ensure our item to fetch is lower case // Ensure our item to fetch is lower case
$item = strtolower($item); $item = strtolower($item);
if ($key) if ($key)
$key = strtolower($key); $key = strtolower($key);
// This error message is not localized as only developers should ever see it
if (! in_array($item,self::schema_types))
throw new InvalidUsage('Invalid request to fetch schema: '.$item);
$result = Cache::remember('schema'.$item,config('ldap.cache.time'),function() use ($item) { $result = Cache::remember('schema'.$item,config('ldap.cache.time'),function() use ($item) {
// First pass if we have already retrieved the schema item // First pass if we have already retrieved the schema item
switch ($item) { switch ($item) {
@ -354,13 +354,13 @@ final class Server
break; break;
// Shouldnt get here // This error message is not localized as only developers should ever see it
default: default:
throw new InvalidUsage('Invalid request to fetch schema: '.$item); throw new InvalidUsage('Invalid request to fetch schema: '.$item);
} }
// Try to get the schema DN from the specified entry. // Try to get the schema DN from the specified entry.
$schema_dn = $this->schemaDN(); $schema_dn = $this->schemaDN('default');
$schema = $this->fetch($schema_dn); $schema = $this->fetch($schema_dn);
switch ($item) { switch ($item) {
@ -526,11 +526,16 @@ final class Server
foreach ($this->objectclasses as $o) foreach ($this->objectclasses as $o)
foreach ($o->getSupClasses() as $parent) { foreach ($o->getSupClasses() as $parent) {
$parent = strtolower($parent); $parent = strtolower($parent);
if ($this->objectclasses->has($parent) !== FALSE)
if (! $this->objectclasses->contains($parent))
$this->objectclasses[$parent]->addChildObjectClass($o->name); $this->objectclasses[$parent]->addChildObjectClass($o->name);
} }
return $this->objectclasses; return $this->objectclasses;
// Shouldnt get here
default:
throw new InvalidUsage('Invalid request to fetch schema: '.$item);
} }
}); });

View File

@ -2,9 +2,12 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use Closure;
use Config;
use Illuminate\Http\Request;
use App\Classes\LDAP\Server; use App\Classes\LDAP\Server;
use App\Ldap\User; use App\Ldap\User;
use Closure;
/** /**
* This sets up our application session with any required values, ultimately for cache optimisation reasons * This sets up our application session with any required values, ultimately for cache optimisation reasons
@ -14,13 +17,13 @@ class ApplicationSession
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request * @param Request $request
* @param \Closure $next * @param Closure $next
* @return mixed * @return mixed
*/ */
public function handle($request,Closure $next) public function handle(Request $request,Closure $next): mixed
{ {
\Config::set('server',new Server); Config::set('server',new Server);
view()->share('user', auth()->user() ?: new User); view()->share('user', auth()->user() ?: new User);

View File

@ -3,7 +3,9 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use Closure; use Closure;
use Config;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@ -15,13 +17,13 @@ class CheckUpdate
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request * @param Request $request
* @param \Closure $next * @param Closure $next
* @return mixed * @return mixed
*/ */
public function handle($request, Closure $next) public function handle(Request $request, Closure $next): mixed
{ {
\Config::set('update_available',Cache::get('upstream_version')); Config::set('update_available',Cache::get('upstream_version'));
return $next($request); return $next($request);
} }
@ -31,7 +33,7 @@ class CheckUpdate
* *
* @return void * @return void
*/ */
public function terminate() public function terminate(): void
{ {
Cache::remember('upstream_version',self::UPDATE_TIME,function() { Cache::remember('upstream_version',self::UPDATE_TIME,function() {
// CURL call to URL to see if there is a new version // CURL call to URL to see if there is a new version
@ -40,7 +42,6 @@ class CheckUpdate
$client = new Client; $client = new Client;
try { try {
$response = $client->request('POST',sprintf('%s/%s',self::UPDATE_SERVER,strtolower(config('app.version')))); $response = $client->request('POST',sprintf('%s/%s',self::UPDATE_SERVER,strtolower(config('app.version'))));
if ($response->getStatusCode() === 200) { if ($response->getStatusCode() === 200) {

View File

@ -6,24 +6,23 @@ use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Cookie; use Illuminate\Support\Facades\Cookie;
// use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
// use Illuminate\Support\Facades\Session;
use LdapRecord\Container; use LdapRecord\Container;
use App\Ldap\Connection; use App\Ldap\Connection;
class SwapinAuthUser class SwapinAuthUser
{ {
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next * @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/ * @throws \LdapRecord\Configuration\ConfigurationException
public function handle(Request $request, Closure $next) */
{ public function handle(Request $request,Closure $next): mixed
{
$key = config('ldap.default'); $key = config('ldap.default');
/* /*
@ -35,16 +34,17 @@ class SwapinAuthUser
} else } else
*/ */
// @todo it seems sometimes we have cookies that show the logged in user, but Auth::user() has expired?
if (Cookie::has('username_encrypt') && Cookie::has('password_encrypt')) { if (Cookie::has('username_encrypt') && Cookie::has('password_encrypt')) {
Config::set('ldap.connections.'.$key.'.username',Cookie::get('username_encrypt')); Config::set('ldap.connections.'.$key.'.username',Cookie::get('username_encrypt'));
Config::set('ldap.connections.'.$key.'.password',Cookie::get('password_encrypt')); Config::set('ldap.connections.'.$key.'.password',Cookie::get('password_encrypt'));
Log::debug('Swapping out configured LDAP credentials with the user\'s cookie.',['key'=>$key,'user'=>Cookie::get('username_encrypt')]); Log::debug('Swapping out configured LDAP credentials with the user\'s cookie.',['key'=>$key,'user'=>Cookie::get('username_encrypt')]);
// We need to override our Connection object so that we can store and retrieve the logged in user and swap out the credentials to use them.
Container::getInstance()->addConnection(new Connection(config('ldap.connections.'.$key)),$key);
} }
// We need to override our Connection object so that we can store and retrieve the logged in user and swap out the credentials to use them.
Container::getInstance()->addConnection(new Connection(config('ldap.connections.'.$key)),$key);
return $next($request); return $next($request);
} }
} }

View File

@ -16,8 +16,8 @@ return Application::configure(basePath: dirname(__DIR__))
) )
->withMiddleware(function (Middleware $middleware) { ->withMiddleware(function (Middleware $middleware) {
$middleware->appendToGroup('web', [ $middleware->appendToGroup('web', [
ApplicationSession::class,
SwapinAuthUser::class, SwapinAuthUser::class,
ApplicationSession::class,
CheckUpdate::class, CheckUpdate::class,
]); ]);

View File

@ -9,7 +9,7 @@ RUN install-php-extensions \
memcached memcached
RUN sed -i -e 's/^{$CADDY_EXTRA_CONFIG}$/{$CADDY_EXTRA_CONFIG} /' /etc/caddy/Caddyfile RUN sed -i -e 's/^{$CADDY_EXTRA_CONFIG}$/{$CADDY_EXTRA_CONFIG} /' /etc/caddy/Caddyfile
RUN sed -i -e 's/^memory_limit = 128M/memory_limit = 1G/' /usr/local/etc/php/php.ini-production RUN sed -i -e 's/^memory_limit = 128M/memory_limit = 256M/' /usr/local/etc/php/php.ini-production
RUN cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini RUN cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini
RUN curl -4 https://getcomposer.org/installer|php -- --install-dir=/usr/local/bin --filename=composer RUN curl -4 https://getcomposer.org/installer|php -- --install-dir=/usr/local/bin --filename=composer