Compare commits

...

12 Commits

Author SHA1 Message Date
0a268fb653 Revert version to 2.1.2-dev
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 4m23s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 2m24s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m31s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-04-27 14:12:24 +10:00
6954b09089 @todo udpates 2025-04-27 14:12:24 +10:00
a336e58b7a Fixes for 389 Directory Server - addresses recursion issue #314. The primary issue was that 389DS doesnt render the subschemaSubentry attribute unless it is specifically requested. 2025-04-27 14:12:24 +10:00
53880121b6 Server::class optimisations, minimal functional changes - basically caching/performance improvements 2025-04-27 14:12:24 +10:00
ea46cf36d0 Remove deprecteated Entry::query() override and associated noObjectAttributes() it wasnt used 2025-04-27 14:12:24 +10:00
36f8f57b77 When opening the export modal, limit selection to inside the modal. Generally when opening modals disable selection.
When selecting a DN on a DN fragment, autoselect the whole DN.
2025-04-27 14:12:24 +10:00
3604f1498c Update existing LDAP instance configuration instead of replacing it. Caching was not enabled as per the configuration, so this fixes this. 2025-04-27 14:12:24 +10:00
808934ebfe Change we now store logged in user details in session, instead of cookies.
This is so when the session expires, the logged in user details are expired as well, which wasnt happening with cookies.
2025-04-27 14:12:24 +10:00
21a690c6dd Move our /api routes into /ajax under web.php. The /api routes werent authenticated and may not have been using the logged in users details 2025-04-27 14:12:24 +10:00
0083e9158b Move out view variables until after our session has been setup. This was needed so that auth()->user() could be resolved correctly and needed to be done after we have started the session and swapped in the users cookies 2025-04-27 14:12:24 +10:00
f4cc559931 Dynamically work out objectclasses on the current entry, this fixes usage issues between adding objectclasses and adding attribute that are now available from new objectclasses, as well as determining that they are not dynamic 2025-04-27 14:12:24 +10:00
3de46ac28e Fix when rendering changes to 2 or more attributes, the update confirmation table had one too many rowspan values for the Attribute.
Fix updating an entry by adding an new objectclass
2025-04-27 14:12:24 +10:00
35 changed files with 257 additions and 290 deletions

View File

@ -15,4 +15,4 @@ LDAP_HOST=
LDAP_BASE_DN= LDAP_BASE_DN=
LDAP_USERNAME= LDAP_USERNAME=
LDAP_PASSWORD= LDAP_PASSWORD=
LDAP_CACHE=true LDAP_CACHE=false

View File

@ -61,7 +61,7 @@ Support is known for these LDAP servers:
- [X] OpenLDAP - [X] OpenLDAP
- [X] OpenDJ - [X] OpenDJ
- [ ] Microsoft Active Directory - [ ] Microsoft Active Directory
- [ ] 389 Directory Server - [X] 389 Directory Server
If there is an LDAP server that you have that you would like to have supported, please open an issue to request it. If there is an LDAP server that you have that you would like to have supported, please open an issue to request it.
You might need to provide access, provide a copy or instructions to get an environment for testing. If you have enabled You might need to provide access, provide a copy or instructions to get an environment for testing. If you have enabled

View File

@ -311,7 +311,7 @@ class Attribute implements \Countable, \ArrayAccess
*/ */
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
{ {
$view = match ($this->schema->syntax_oid) { $view = match ($this->schema?->syntax_oid) {
self::SYNTAX_CERTIFICATE => view('components.syntax.certificate'), self::SYNTAX_CERTIFICATE => view('components.syntax.certificate'),
self::SYNTAX_CERTIFICATE_LIST => view('components.syntax.certificatelist'), self::SYNTAX_CERTIFICATE_LIST => view('components.syntax.certificatelist'),

View File

@ -20,7 +20,7 @@ abstract class Base {
protected string $name = ''; protected string $name = '';
// The OID of this schema item. // The OID of this schema item.
protected string $oid; protected string $oid = '';
# The description of this schema item. # The description of this schema item.
protected string $description = ''; protected string $description = '';

View File

@ -43,7 +43,7 @@ final class ObjectClass extends Base
* *
* @param string $line Schema Line * @param string $line Schema Line
* @param Server $server * @param Server $server
* @todo Change $server to $connection, no need to store the server object here * @todo Deprecate this $server variable? It is only used for isForceMay() determination, and that might be better done elsewhere?
*/ */
public function __construct(string $line,Server $server) public function __construct(string $line,Server $server)
{ {

View File

@ -8,11 +8,11 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Collection; 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\Cookie;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session; 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;
@ -22,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;
@ -37,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),
}; };
} }
@ -62,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:
@ -173,16 +168,6 @@ final class Server
} 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
if (Cookie::has('password_encrypt')) {
Log::alert('Clearing user credentials and logging out');
Cookie::queue(Cookie::forget('password_encrypt'));
Cookie::queue(Cookie::forget('username_encrypt'));
Session::invalidate();
}
abort(401,$e->getDetailedError()->getErrorMessage()); abort(401,$e->getDetailedError()->getErrorMessage());
default: default:
@ -191,57 +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) {
* @note While we are caching our baseDNs, it seems if we have more than 1, $result = collect();
* our caching doesnt generate a hit on a subsequent call to this function (before the cache expires).
* IE: If we have 5 baseDNs, it takes 5 calls to this function to case them all.
* @todo Possibly a bug wtih ldaprecord, so need to investigate
*/
$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() ->firstOrFail();
->select(['+'])
->whereHas('objectclass') return $rootdse;
->firstOrFail();
} }
/** /**
* 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.
* *
@ -251,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;
} }
/** /**
@ -269,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;
} }
/** /**
@ -285,6 +310,7 @@ final class Server
* as configured in config.php. * as configured in config.php.
* *
* @return boolean True if the specified attribute is configured to be force as a may attribute * @return boolean True if the specified attribute is configured to be force as a may attribute
* @todo There are 3 isForceMay() functions - we only need one
*/ */
public function isForceMay($attr_name): bool public function isForceMay($attr_name): bool
{ {
@ -321,34 +347,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;
@ -358,8 +361,9 @@ 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); // @note: 389DS does not return subschemaSubentry unless it is requested
$schema = $this->fetch($schema_dn,['*','+','subschemaSubentry']);
// If our schema's null, we didnt find it. // If our schema's null, we didnt find it.
if (! $schema) if (! $schema)
@ -367,7 +371,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);
@ -385,7 +389,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;
@ -445,7 +449,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))
@ -458,7 +462,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) {
@ -499,7 +503,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

@ -10,17 +10,18 @@ use Illuminate\Support\Collection;
use App\Classes\LDAP\Server; use App\Classes\LDAP\Server;
class APIController extends Controller class AjaxController extends Controller
{ {
/** /**
* Get the LDAP server BASE DNs * Get the LDAP server BASE DNs
* *
* @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

@ -5,41 +5,43 @@ namespace App\Http\Controllers\Auth;
use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cookie; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
class LoginController extends Controller class LoginController extends Controller
{ {
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Login Controller | Login Controller
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| This controller handles authenticating users for the application and | This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait | redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications. | to conveniently provide its functionality to your applications.
| |
*/ */
use AuthenticatesUsers; use AuthenticatesUsers;
/** /**
* Where to redirect users after login. * Where to redirect users after login.
* *
* @var string * @var string
*/ */
protected $redirectTo = '/'; protected $redirectTo = '/';
/** /**
* Create a new controller instance. * Create a new controller instance.
* *
* @return void * @return void
*/ */
public function __construct() public function __construct()
{ {
$this->middleware('guest')->except('logout'); $this->middleware('guest')
} ->except('logout');
}
protected function credentials(Request $request): array protected function credentials(Request $request): array
{ {
@ -58,17 +60,14 @@ class LoginController extends Controller
*/ */
public function logout(Request $request) public function logout(Request $request)
{ {
// Delete our LDAP authentication cookies $user = Auth::user();
Cookie::queue(Cookie::forget('username_encrypt'));
Cookie::queue(Cookie::forget('password_encrypt'));
$this->guard()->logout(); $this->guard()->logout();
$request->session()->invalidate(); $request->session()->invalidate();
$request->session()->regenerateToken(); $request->session()->regenerateToken();
if ($response = $this->loggedOut($request)) { if ($response = $this->loggedOut($request)) {
Log::info(sprintf('Logged out [%s]',$user->dn));
return $response; return $response;
} }
@ -100,4 +99,4 @@ class LoginController extends Controller
{ {
return login_attr_name(); return login_attr_name();
} }
} }

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 [
@ -45,7 +45,7 @@ class HomeController extends Controller
* Create a new object in the LDAP server * Create a new object in the LDAP server
* *
* @param EntryAddRequest $request * @param EntryAddRequest $request
* @return View * @return \Illuminate\View\View
* @throws InvalidUsage * @throws InvalidUsage
*/ */
public function entry_add(EntryAddRequest $request): \Illuminate\View\View public function entry_add(EntryAddRequest $request): \Illuminate\View\View
@ -189,8 +189,7 @@ class HomeController extends Controller
{ {
$dn = Crypt::decryptString($id); $dn = Crypt::decryptString($id);
$result = (new Entry) $result = Entry::query()
->query()
->setDn($dn) ->setDn($dn)
->recursive() ->recursive()
->get(); ->get();

View File

@ -4,7 +4,7 @@ namespace App\Http\Middleware;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cookie; use Illuminate\Support\Facades\Session;
class AllowAnonymous class AllowAnonymous
{ {
@ -17,7 +17,9 @@ class AllowAnonymous
*/ */
public function handle(Request $request,Closure $next): mixed public function handle(Request $request,Closure $next): mixed
{ {
if (((! Cookie::has('username_encrypt')) || (! Cookie::has('password_encrypt'))) && (! config('pla.allow_guest',FALSE))) if ((! config('pla.allow_guest',FALSE))
&& ($request->path() !== 'login')
&& ((! Session::has('username_encrypt')) || (! Session::has('password_encrypt'))))
return redirect() return redirect()
->to('/login'); ->to('/login');

View File

@ -7,7 +7,6 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Config;
use App\Classes\LDAP\Server; use App\Classes\LDAP\Server;
use App\Ldap\User;
/** /**
* 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
@ -25,9 +24,6 @@ class ApplicationSession
{ {
Config::set('server',new Server); Config::set('server',new Server);
view()->share('server', Config::get('server'));
view()->share('user', auth()->user() ?: new User);
return $next($request); return $next($request);
} }
} }

View File

@ -5,11 +5,12 @@ namespace App\Http\Middleware;
use Closure; 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\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\Guard;
class SwapinAuthUser class SwapinAuthUser
{ {
@ -28,25 +29,19 @@ class SwapinAuthUser
if (! array_key_exists($key,config('ldap.connections'))) if (! array_key_exists($key,config('ldap.connections')))
abort(599,sprintf('LDAP default server [%s] configuration doesnt exist?',$key)); abort(599,sprintf('LDAP default server [%s] configuration doesnt exist?',$key));
/*
// Rebuild our connection with the authenticated user.
if (Session::has('username_encrypt') && Session::has('password_encrypt')) { if (Session::has('username_encrypt') && Session::has('password_encrypt')) {
Config::set('ldap.connections.'.$key.'.username',Crypt::decryptString(Session::get('username_encrypt'))); Config::set('ldap.connections.'.$key.'.username',Crypt::decryptString(Session::get('username_encrypt')));
Config::set('ldap.connections.'.$key.'.password',Crypt::decryptString(Session::get('password_encrypt'))); Config::set('ldap.connections.'.$key.'.password',Crypt::decryptString(Session::get('password_encrypt')));
} else Log::debug('Swapping out configured LDAP credentials with the user\'s session.',['key'=>$key]);
*/
// @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')) {
Config::set('ldap.connections.'.$key.'.username',Cookie::get('username_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')]);
} }
// 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. // 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); $c = Container::getInstance()
->getConnection($key);
$c->setConfiguration(config('ldap.connections.'.$key));
$c->setGuardResolver(fn()=>new Guard($c->getLdapConnection(),$c->getConfiguration()));
return $next($request); return $next($request);
} }

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use App\Ldap\User;
/**
* This sets up our application session with any required values, ultimately for cache optimisation reasons
*/
class ViewVariables
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle(Request $request,Closure $next): mixed
{
view()->share('server',Config::get('server'));
view()->share('user',auth()->user() ?: new User);
return $next($request);
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace App\Ldap;
use LdapRecord\Configuration\DomainConfiguration;
use LdapRecord\Connection as ConnectionBase;
use LdapRecord\LdapInterface;
class Connection extends ConnectionBase
{
public function __construct(DomainConfiguration|array $config=[],?LdapInterface $ldap=NULL)
{
parent::__construct($config,$ldap);
// We need to override this so that we use our own Guard, that stores the users credentials in the session
$this->authGuardResolver = function () {
return new Guard($this->ldap, $this->configuration);
};
}
}

View File

@ -27,8 +27,6 @@ class Entry extends Model
// Our Attribute objects // Our Attribute objects
private Collection $objects; private Collection $objects;
/* @deprecated */
private bool $noObjectAttributes = FALSE;
// For new entries, this is the container that this entry will be stored in // For new entries, this is the container that this entry will be stored in
private string $rdnbase; private string $rdnbase;
@ -71,8 +69,6 @@ class Entry extends Model
/** /**
* Determine if the new and old values for a given key are equivalent. * Determine if the new and old values for a given key are equivalent.
*
* @todo This function barfs on language tags, eg: key = givenname;lang-ja
*/ */
protected function originalIsEquivalent(string $key): bool protected function originalIsEquivalent(string $key): bool
{ {
@ -84,16 +80,6 @@ class Entry extends Model
|| (! $this->getObject($attribute)->isDirty()); || (! $this->getObject($attribute)->isDirty());
} }
public static function query(bool $noattrs=false): Builder
{
$o = new static;
if ($noattrs)
$o->noObjectAttributes();
return $o->newQuery();
}
/** /**
* As attribute values are updated, or new ones created, we need to mirror that * As attribute values are updated, or new ones created, we need to mirror that
* into our $objects. This is called when we $o->key = $value * into our $objects. This is called when we $o->key = $value
@ -539,19 +525,6 @@ class Entry extends Model
return [$attribute,$tags]; return [$attribute,$tags];
} }
/**
* Dont convert our $this->attributes to $this->objects when creating a new Entry::class
*
* @return $this
* @deprecated
*/
public function noObjectAttributes(): static
{
$this->noObjectAttributes = TRUE;
return $this;
}
public function setRDNBase(string $bdn): void public function setRDNBase(string $bdn): void
{ {
if ($this->exists) if ($this->exists)

View File

@ -2,26 +2,20 @@
namespace App\Ldap; namespace App\Ldap;
use Illuminate\Support\Facades\Cookie; use Illuminate\Support\Facades\Crypt;
// use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Log;
use LdapRecord\Auth\Guard as GuardBase; use LdapRecord\Auth\Guard as GuardBase;
class Guard extends GuardBase class Guard extends GuardBase
{ {
public function attempt(string $username, string $password, bool $stayBound = false): bool public function attempt(string $username, string $password, bool $stayBound = false): bool
{ {
if ($result = parent::attempt($username,$password,$stayBound)) { Log::info(sprintf('Attempting login for [%s] with password [%s]',$username,($password ? str_repeat('*',16) : str_repeat('?',16))));
/*
* We can either use our session or cookies to store this. If using session, then Http/Kernel needs to be
* updated to start a session for API calls.
// We need to store our password so that we can swap in the user in during SwapinAuthUser::class middleware
request()->session()->put('username_encrypt',Crypt::encryptString($username));
request()->session()->put('password_encrypt',Crypt::encryptString($password));
*/
// For our API calls, we store the cookie - which our cookies are already encrypted if ($result = parent::attempt($username,$password,$stayBound)) {
Cookie::queue('username_encrypt',$username); // Store user details so we can swap in auth details in SwapinAuthUser
Cookie::queue('password_encrypt',$password); session()->put('username_encrypt',Crypt::encryptString($username));
session()->put('password_encrypt',Crypt::encryptString($password));
} }
return $result; return $result;

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

@ -1,32 +1,27 @@
<?php <?php
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Foundation\Configuration\Middleware;
use App\Http\Middleware\{AllowAnonymous,ApplicationSession,CheckUpdate,SwapinAuthUser}; use App\Http\Middleware\{AllowAnonymous,ApplicationSession,CheckUpdate,SwapinAuthUser,ViewVariables};
return Application::configure(basePath: dirname(__DIR__)) return Application::configure(basePath: dirname(__DIR__))
->withRouting( ->withRouting(
web: __DIR__.'/../routes/web.php', web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php', commands: __DIR__.'/../routes/console.php',
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware) { ->withMiddleware(function (Middleware $middleware) {
$middleware->appendToGroup('web', [ $middleware->appendToGroup(
SwapinAuthUser::class, group: 'web',
ApplicationSession::class, middleware: [
CheckUpdate::class, AllowAnonymous::class,
]); ApplicationSession::class,
SwapinAuthUser::class,
$middleware->prependToGroup('api', [ ViewVariables::class,
EncryptCookies::class, CheckUpdate::class,
SwapinAuthUser::class, ]);
ApplicationSession::class,
AllowAnonymous::class,
]);
$middleware->trustProxies(at: [ $middleware->trustProxies(at: [
'10.0.0.0/8', '10.0.0.0/8',

View File

@ -1,5 +1,5 @@
<?php <?php
return [ return [
App\Providers\AppServiceProvider::class, App\Providers\AppServiceProvider::class,
]; ];

View File

@ -68,7 +68,7 @@ return [
'daily' => [ 'daily' => [
'driver' => 'daily', 'driver' => 'daily',
'path' => storage_path('logs/laravel.log'), 'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'), 'level' => env('LOG_LEVEL', 'info'),
'days' => env('LOG_DAILY_DAYS', 14), 'days' => env('LOG_DAILY_DAYS', 14),
'replace_placeholders' => true, 'replace_placeholders' => true,
], ],

View File

@ -1 +1 @@
v2.1.1-rel v2.1.2-dev

View File

@ -252,4 +252,9 @@ select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__
/* Stop showing a border on our user's drop down menu when open */ /* Stop showing a border on our user's drop down menu when open */
.btn-check:checked+.btn, .btn.active, .btn.show, .btn:first-child:active, :not(.btn-check)+.btn:active { .btn-check:checked+.btn, .btn.active, .btn.show, .btn:first-child:active, :not(.btn-check)+.btn:active {
border-color: var(--bs-btn-bg); border-color: var(--bs-btn-bg);
}
/* limit selection to inside the modal */
body.modal-open {
user-select: none;
} }

4
public/js/custom.js vendored
View File

@ -59,7 +59,7 @@ $(document).ready(function() {
if (typeof basedn !== 'undefined') { if (typeof basedn !== 'undefined') {
sources = basedn; sources = basedn;
} else { } else {
sources = { url: 'api/bases' }; sources = { url: 'ajax/bases' };
} }
// Attach the fancytree widget to an existing <div id="tree"> element // Attach the fancytree widget to an existing <div id="tree"> element
@ -95,7 +95,7 @@ $(document).ready(function() {
source: sources, source: sources,
lazyLoad: function(event,data) { lazyLoad: function(event,data) {
data.result = { data.result = {
url: '/api/children', url: '/ajax/children',
data: {key: data.node.data.item,depth: 1} data: {key: data.node.data.item,depth: 1}
}; };

View File

@ -54,6 +54,9 @@
var rendered = false; var rendered = false;
var newadded = []; var newadded = [];
var oc = $('attribute#objectClass input[type=text]')
.map((key,item)=>{return $(item).val()}).toArray();
if (newadded.length) if (newadded.length)
process_oc(); process_oc();
@ -88,7 +91,7 @@
// Get a list of attributes already on the page, so we dont double up // Get a list of attributes already on the page, so we dont double up
$.ajax({ $.ajax({
method: 'POST', method: 'POST',
url: '{{ url('api/schema/objectclass/attrs') }}/'+item, url: '{{ url('ajax/schema/objectclass/attrs') }}/'+item,
cache: false, cache: false,
success: function(data) { success: function(data) {
// Render any must attributes // Render any must attributes
@ -153,7 +156,7 @@
$.ajax({ $.ajax({
method: 'POST', method: 'POST',
url: '{{ url('api/schema/objectclass/attrs') }}/'+item, url: '{{ url('ajax/schema/objectclass/attrs') }}/'+item,
cache: false, cache: false,
success: function(data) { success: function(data) {
var attrs = []; var attrs = [];

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

@ -3,7 +3,7 @@
<td class="p-1 pt-0" rowspan="2"> <td class="p-1 pt-0" rowspan="2">
{!! ($x=$o->getObject('jpegphoto')) ? $x->render(FALSE,TRUE) : sprintf('<div class="page-title-icon f32 m-2"><i class="%s"></i></div>',$o->icon() ?? "fas fa-info") !!} {!! ($x=$o->getObject('jpegphoto')) ? $x->render(FALSE,TRUE) : sprintf('<div class="page-title-icon f32 m-2"><i class="%s"></i></div>',$o->icon() ?? "fas fa-info") !!}
</td> </td>
<td class="text-end align-bottom pb-0 mb-0 pt-2 pe-3 {{ $x ? 'ps-3' : '' }}"><strong>{{ $o->getDn() }}</strong></td> <td class="text-end align-bottom pb-0 mb-0 pt-2 pe-3 {{ $x ? 'ps-3' : '' }}"><strong class="user-select-all">{{ $o->getDn() }}</strong></td>
</tr> </tr>
<tr> <tr>
<td class="align-bottom" style="font-size: 55%" colspan="2"> <td class="align-bottom" style="font-size: 55%" colspan="2">

View File

@ -103,9 +103,10 @@
$(document).ready(function() { $(document).ready(function() {
@if($step === 2) @if($step === 2)
var oc = {!! $o->getObject('objectclass')->values !!};
$('#newattr').on('change',function(item) { $('#newattr').on('change',function(item) {
var oc = $('attribute#objectClass input[type=text]')
.map((key,item)=>{return $(item).val()}).toArray();
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: '{{ url('entry/attr/add') }}/'+item.target.value, url: '{{ url('entry/attr/add') }}/'+item.target.value,

View File

@ -174,7 +174,6 @@
@section('page-scripts') @section('page-scripts')
<script type="text/javascript"> <script type="text/javascript">
var dn = '{{ $o->getDNSecure() }}'; var dn = '{{ $o->getDNSecure() }}';
var oc = {!! $o->getObject('objectclass')->values !!};
function editmode() { function editmode() {
$('#dn-edit input[name="dn"]').val(dn); $('#dn-edit input[name="dn"]').val(dn);
@ -225,6 +224,9 @@
}); });
$('#newattr').on('change',function(item) { $('#newattr').on('change',function(item) {
var oc = $('attribute#objectClass input[type=text]')
.map((key,item)=>{return $(item).val()}).toArray();
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
beforeSend: function() {}, beforeSend: function() {},
@ -334,6 +336,11 @@
} }
}); });
$('#page-modal').on('hide.bs.modal',function() {
// Clear any select ranges that occurred while the modal was open
document.getSelection().removeAllRanges();
});
@if(old()) @if(old())
editmode(); editmode();
@endif @endif

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
@ -58,7 +57,7 @@
return false; return false;
$.ajax({ $.ajax({
url: '{{ url('api/schema/view') }}', url: '{{ url('ajax/schema/view') }}',
method: 'POST', method: 'POST',
data: { type: type }, data: { type: type },
dataType: 'html', dataType: 'html',

View File

@ -5,7 +5,7 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div id="entry_export"></div> <div id="entry_export" style="user-select: text;"></div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">

View File

@ -37,7 +37,7 @@
<tbody> <tbody>
@foreach ($o->getObjects()->filter(fn($item)=>$item->isDirty()) as $key => $oo) @foreach ($o->getObjects()->filter(fn($item)=>$item->isDirty()) as $key => $oo)
<tr> <tr>
<th rowspan="{{ $x=max($oo->values->dot()->keys()->count(),$oo->values_old->dot()->keys()->count())+1}}"> <th rowspan="{{ $x=max($oo->values->dot()->keys()->count(),$oo->values_old->dot()->keys()->count())}}">
<abbr title="{{ $oo->description }}">{{ $oo->name }}</abbr> <abbr title="{{ $oo->description }}">{{ $oo->name }}</abbr>
</th> </th>
@ -54,7 +54,7 @@
<td colspan="2" class="text-center">@lang('Ignoring blank value')</td> <td colspan="2" class="text-center">@lang('Ignoring blank value')</td>
@else @else
<td>{{ (($r=$oo->render_item_old($dotkey)) !== NULL) ? $r : '['.strtoupper(__('New Value')).']' }}</td> <td>{{ (($r=$oo->render_item_old($dotkey)) !== NULL) ? $r : '['.strtoupper(__('New Value')).']' }}</td>
<td>{{ (($r=$oo->render_item_new($dotkey)) !== NULL) ? $r : '['.strtoupper(__('Deleted')).']' }}<input type="hidden" name="{{ $key }}[{{ collect(explode('.',$dotkey))->first() }}][]" value="{{ Arr::get($oo,$dotkey) }}"></td> <td>{{ (($r=$oo->render_item_new($dotkey)) !== NULL) ? $r : '['.strtoupper(__('Deleted')).']' }}<input type="hidden" name="{{ $key }}[{{ $oo->no_attr_tags ? \App\Ldap\Entry::TAG_NOTAG : collect(explode('.',$dotkey))->first() }}][]" value="{{ Arr::get($oo->values->dot(),$dotkey) }}"></td>
@endif @endif
@endforeach @endforeach
</tr> </tr>

View File

@ -1,23 +0,0 @@
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\APIController;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
Route::controller(APIController::class)->group(function() {
Route::get('bases','bases');
Route::get('children','children');
Route::post('schema/view','schema_view');
Route::post('schema/objectclass/attrs/{id}','schema_objectclass_attrs');
});

View File

@ -2,7 +2,7 @@
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\Http\Controllers\HomeController; use App\Http\Controllers\{AjaxController,HomeController};
use App\Http\Controllers\Auth\LoginController; use App\Http\Controllers\Auth\LoginController;
use App\Http\Middleware\AllowAnonymous; use App\Http\Middleware\AllowAnonymous;
@ -57,4 +57,13 @@ Route::controller(HomeController::class)->group(function() {
Route::view('modal/export/{dn}','modals.entry-export'); Route::view('modal/export/{dn}','modals.entry-export');
Route::view('modal/userpassword-check/{dn}','modals.entry-userpassword-check'); Route::view('modal/userpassword-check/{dn}','modals.entry-userpassword-check');
}); });
}); });
Route::controller(AjaxController::class)
->prefix('ajax')
->group(function() {
Route::get('bases','bases');
Route::get('children','children');
Route::post('schema/view','schema_view');
Route::post('schema/objectclass/attrs/{id}','schema_objectclass_attrs');
});

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());