Compare commits

...

15 Commits

Author SHA1 Message Date
4494154879 Fix regression introduced in 56fcd729. Server was added to the configuration before SwapinAuthUser::class resulting in the configured LDAP user being used for all queries and not the logged in user. Fixes #348
Some checks failed
Create Docker Image / Test Application (x86_64) (push) Has been cancelled
Create Docker Image / Build Docker Image (arm64) (push) Has been cancelled
Create Docker Image / Build Docker Image (x86_64) (push) Has been cancelled
Create Docker Image / Final Docker Image Manifest (push) Has been cancelled
2025-06-30 20:35:33 +08:00
b22c9505bc Fix rendering of objectclass in server info, consistent use of true/false/null in view blades
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 31s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m33s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 2m48s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-06-27 18:44:25 +10:00
29a659ff69 Fix typo in 553368c that stopped configuration defaults from loading
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m28s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 2m45s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2025-06-27 16:53:04 +10:00
2348da36c4 Fix hasing password on entry create. Fixes #353
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 2m46s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 15m7s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-06-27 14:18:52 +10:00
6f58f5db36 Fix bug introduced with 553368c, when clearing session _auto_number when need to allow for edits that doesnt have this set
Some checks failed
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 2m41s
Create Docker Image / Final Docker Image Manifest (push) Has been cancelled
Create Docker Image / Build Docker Image (x86_64) (push) Has been cancelled
2025-06-27 14:03:35 +10:00
553368c7b9 Implement getNextNumber() to populate template->values for attributes, where the attribute is determined after evaulating whats in the directory
Some checks failed
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 3m1s
Create Docker Image / Final Docker Image Manifest (push) Has been cancelled
Create Docker Image / Build Docker Image (x86_64) (push) Has been cancelled
2025-06-27 13:50:01 +10:00
c8d1122ff6 Fix validation on existing entries, missed in 88db4cc 2025-06-26 23:13:46 +10:00
2320445dfb Fix regression introduce in 31e3c7, x-form.select wasnt rendering the current value of the select list. Also fix validation redirect where the password encryption method was changed, but the new encryption method was not set. 2025-06-26 22:49:06 +10:00
6d2c9d1354 Specifying a comma delimited list for LDAP_BASE_DN was never going to work. Use a colon instead. Fixes #351 2025-06-26 22:04:37 +10:00
6f20d426ad Dont sort by DN, problematic when sssvlv overlay is used in openldap. Seems DN's are sorted anyway. Fixes #350 2025-06-26 21:55:10 +10:00
7b1b4f4e50 Rename and group schema modification files to better identify global and specific database changes 2025-06-26 21:53:16 +10:00
543250e1fb Fix entry-userpassword-check when entry is rendered with a template
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 30s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m27s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 2m51s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2025-06-22 22:10:21 +10:00
3bf97fc0d1 Add the ability to use a select list for template attributes 2025-06-22 22:08:38 +10:00
3ad4c446ea Change our template attribute processing, to be collections, so we can find attributes using anycase keys 2025-06-22 17:27:56 +10:00
ee3cb395c2 Enhancement to 8fd2a43, validating authentication before rendering the DN doesnt exist error (otherwise it is an authentication issue) 2025-06-22 14:07:33 +10:00
29 changed files with 468 additions and 141 deletions

View File

@@ -275,7 +275,6 @@ final class Server
'c' // Needed for the tree to show icons for countries
]))
->list()
->orderBy('dn')
->get() ?: NULL;
}

View File

@@ -4,16 +4,21 @@ namespace App\Classes;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use App\Ldap\Entry;
class Template
{
private const LOGKEY = 'T--';
private const LOCK_TIME = 600;
private(set) string $file;
private array $template;
private Collection $template;
private(set) bool $invalid = FALSE;
private(set) string $reason = '';
private Collection $on_change_target;
@@ -31,7 +36,7 @@ class Template
try {
// @todo Load in the proper attribute objects and objectclass objects
// @todo Make sure we have a structural objectclass, or make the template invalid
$this->template = json_decode($td->get($file),null,512,JSON_OBJECT_AS_ARRAY|JSON_THROW_ON_ERROR);
$this->template = collect(json_decode($td->get($file),null,512,JSON_OBJECT_AS_ARRAY|JSON_THROW_ON_ERROR));
} catch (\JsonException $e) {
$this->invalid = TRUE;
@@ -42,12 +47,11 @@ class Template
public function __get(string $key): mixed
{
return match ($key) {
'attributes' => collect(Arr::get($this->template,$key))->keys(),
'enabled' => Arr::get($this->template,$key,FALSE) && (! $this->invalid),
'icon','regexp','title' => Arr::get($this->template,$key),
'attributes','objectclasses' => collect($this->template->get($key)),
'enabled' => $this->template->get($key,FALSE) && (! $this->invalid),
'icon','regexp','title' => $this->template->get($key),
'name' => Str::replaceEnd('.json','',$this->file),
'objectclasses' => collect(Arr::get($this->template,$key)),
'order' => collect(Arr::get($this->template,'attributes'))->map(fn($item)=>$item['order']),
'order' => $this->attributes->map(fn($item)=>Arr::get($item,'order')),
default => throw new \Exception('Unknown key: '.$key),
};
@@ -55,7 +59,32 @@ class Template
public function __isset(string $key): bool
{
return array_key_exists($key,$this->template);
return $this->template->has($key);
}
/**
* Return the configuration for an attribute
*
* @param string $attribute
* @return array|NULL
*/
public function attribute(string $attribute): Collection|NULL
{
$key = $this->attributes->search(fn($item,$key)=>! strcasecmp($key,$attribute));
return collect($this->attributes->get($key));
}
/**
* Return an template attributes select options
*
* @param string $attribute
* @return Collection|NULL
*/
public function attributeOptions(string $attribute): Collection|NULL
{
return ($x=$this->attribute($attribute)?->get('options'))
? collect($x)->map(fn($item,$key)=>['id'=>$key,'value'=>$item])
: NULL;
}
/**
@@ -66,7 +95,7 @@ class Template
*/
public function attributeReadOnly(string $attribute): bool
{
return ($x=Arr::get($this->template,'attributes.'.$attribute.'.readonly')) && $x;
return ($x=$this->attribute($attribute)?->get('readonly')) && $x;
}
/**
@@ -77,7 +106,166 @@ class Template
*/
public function attributeTitle(string $attribute): string|NULL
{
return Arr::get($this->template,'attributes.'.$attribute.'.display');
return $this->attribute($attribute)?->get('display');
}
/**
* Return the title we should use for an attribute
*
* @param string $attribute
* @return string|NULL
*/
public function attributeType(string $attribute): string|NULL
{
return $this->attribute($attribute)?->get('type');
}
public function attributeValue(string $attribute): string|NULL
{
if ($x=$this->attribute($attribute)->get('value')) {
list($command,$args) = preg_split('/^=([a-zA-Z]+)\((.+)\)$/',$x,-1,PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
return match ($command) {
'getNextNumber' => $this->getNextNumber($args),
default => NULL,
};
}
return NULL;
}
/**
* Get next number for an attribute
*
* As part of getting the next number, we'll use a lock to avoid any potential clashes. The lock is obtained by
* two lock files:
* a: Read a session lock (our session id), use that number if it exists, otherwise,
* b: Query the ldap server for the attribute, sort by number
* c: Read a system lock, if it exists, and use that as our start base (otherwise use a config() base)
* d: Starting at base, find the next free number
* e: When number identified, put it in the system lock with our session id
* f: Put the number in our session lock, with a timeout
* g: Read the system lock, make sure our session id is still in it, if not, go to (d) with our number as the base
* h: Remove our session id from the system lock (our number is unique)
*
* When using the number to create an entry:
* + Read our session lock, confirm the number is still in it, if not fail validation and bounce back
* + Create the entry
* + Delete our session lock
*
* @param string $arg
* @return int|NULL
*/
private function getNextNumber(string $arg): int|NULL
{
if (! preg_match('/;/',$arg)) {
Log::alert(sprintf('%s:Invalid argument given to getNextNumber [%s]',self::LOGKEY,$arg));
return NULL;
}
list($start,$attr) = preg_split('(([^,]+);(\w+))',$arg,-1,PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
$attr = strtolower($attr);
// If we recently got a number, return it
if ($number=Cache::get($attr.':'.Session::id()))
return $number;
$cache = Cache::get($attr.':system');
Log::debug(sprintf('%s:System Cache has',self::LOGKEY),['cache'=>$cache]);
if (! Arr::get($cache,'number'))
$number = config('pla.template.getnextnumber.'.$attr,0);
else
$number = Arr::get($cache,'number')+1;
Log::debug(sprintf('%s:Starting with [%d] for [%s]',self::LOGKEY,$number,$attr));
$o = config('server');
$bases = ($start === '/') ? $o->baseDNs() : collect($start);
$result = collect();
$complete = [];
do {
$sizelimit = FALSE;
// Get the current numbers
foreach ($bases as $base) {
if (Arr::get($complete,$dn=$base->getDN()))
continue;
$query = Entry::query()
->setDN($base)
->select([$attr])
->where($attr,'*')
->notFilter(fn($q)=>$q->where($attr,'<=',$number-1));
if ($result->count())
$query->notFilter(fn($q)=>$q->where($attr,'>=',$result->min()));
$result = $result->merge(($x=$query
->search()
->orderBy($attr)
->get())
->pluck($attr)
->flatten());
// If we hit a sizelimit on this run
$base_sizelimit = $query->getModel()->hasMore();
Log::debug(sprintf('%s:Query in [%s] returned [%d] entries and has more [%s]',self::LOGKEY,$base,$x->count(),$base_sizelimit ? 'TRUE' : 'FALSE'));
if (! $base_sizelimit)
$complete[$dn] = TRUE;
else
Log::info(sprintf('%s:Size Limit alert for [%s]',self::LOGKEY,$dn));
$sizelimit = $sizelimit || $base_sizelimit;
}
$result = $result
->sort()
->unique();
Log::debug(sprintf('%s:Result has [%s]',self::LOGKEY,$result->join('|')));
if ($result->count())
foreach ($result as $item) {
Log::debug(sprintf('%s:Checking [%d] against [%s]',self::LOGKEY,$number,$item));
if ($number < $item)
break;
$number += 1;
}
else
$number += 1;
// Remove redundant entries
$result = $result->filter(fn($item)=>$item>$number);
if ($sizelimit)
Log::debug(sprintf('%s:We got a sizelimit.',self::LOGKEY),['number'=>$number,'result_min'=>$result->min(),'result_count'=>$result->count()]);
/*
* @todo This might need some additional work:
* EG: if sizelimit is 5
* if result has 1,2,3,4,20 [size limit]
* we re-enquire (4=>20) and get 7,8,9,10,11 [size limit]
* we re-enquire (4=>7) and get 5,6 [no size limit]
* we calculate 12, and accept it because no size limit, but we didnt test for 12
*/
} while ($sizelimit);
// We found our number
Log::debug(sprintf('%s:Autovalue for Attribute [%s] in Session [%s] Storing [%d]',self::LOGKEY,$attr,Session::id(),$number));
Cache::put($attr.':system',['number'=>$number,'session'=>Session::id(),self::LOCK_TIME*2]);
Cache::put($attr.':'.Session::id(),$number,self::LOCK_TIME);
sleep(1);
// If the session still has our session ID, then our number is ours
return (Arr::get(Cache::get($attr.':system'),'session') === Session::id())
? $number
: NULL;
}
/**
@@ -207,7 +395,7 @@ class Template
$m = [];
// MATCH : 0 = highlevel match, 1 = attr, 2 = subst, 3 = mod, 4 = delimiter
preg_match_all('/%(\w+)(?:\|([0-9]*-[0-9])+)?(?:\/([klTUA]+))?(?:\|(.)?)?%/U',$string,$m);
preg_match_all('/%(\w+)(?:\|(\d*-\d)+)?(?:\/([klTUA]+))?(?:\|(.)?)?%/U',$string,$m);
foreach ($m[0] as $index => $null) {
$match_attr = strtolower($m[1][$index]);
@@ -220,14 +408,14 @@ class Template
$result .= sprintf("var %s;\n",$match_attr);
if (str_contains($match_mod,'k')) {
preg_match_all('/([0-9]+)/',trim($match_subst),$substrarray);
preg_match_all('/(\d+)/',trim($match_subst),$substrarray);
$delimiter = ($match_delim === '') ? ' ' : preg_quote($match_delim);
$result .= sprintf(" %s = %s.split('%s')[%s];\n",$match_attr,$match_attr,$delimiter,$substrarray[1][0] ?? '0');
} else {
// Work out the start and end chars needed from this value if we have a range specifier
preg_match_all('/([0-9]*)-([0-9]+)/',$match_subst,$substrarray);
preg_match_all('/(\d*)-(\d+)/',$match_subst,$substrarray);
if ((isset($substrarray[1][0]) && $substrarray[1][0]) || (isset($substrarray[2][0]) && $substrarray[2][0])) {
$result .= sprintf("%s = get_attribute('%s',%d,%s);\n",
$match_attr,$match_attr,

View File

@@ -7,8 +7,9 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use LdapRecord\Auth\BindException;
use LdapRecord\Container;
use App\Exceptions\InvalidUsage;
use App\Http\Controllers\Controller;
use App\Ldap\Entry;
@@ -57,8 +58,9 @@ class LoginController extends Controller
* When attempt to login
*
* @param Request $request
* @return void
* @throws InvalidUsage
* @return bool
* @throws \LdapRecord\ConnectionException
* @throws \LdapRecord\ContainerException
*/
public function attemptLogin(Request $request)
{
@@ -69,12 +71,26 @@ class LoginController extends Controller
// If the login failed, and PLA is set to use DN login, check if the entry exists.
// If the entry doesnt exist, it might be the root DN, which cannot be used to login
if ((! $attempt) && $request->dn && config('pla.login.alert_rootdn',TRUE)) {
// Double check our credentials, and see if they authenticate
try {
Container::getInstance()
->getConnection()
->auth()
->bind($request->get(login_attr_name()),$request->get('password'));
} catch (BindException $e) {
// Password incorrect, fail anyway
return FALSE;
}
$dn = config('server')->fetch($request->dn);
$o = new Entry;
if (! $dn && $o->getConnection()->getLdapConnection()->errNo() === 32)
abort(501,'Authentication set to DN, but the DN doesnt exist');
abort(501,'Authentication succeeded, but the DN doesnt exist');
}
return $attempt;
}
/**

View File

@@ -6,17 +6,18 @@ use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Session;
use LdapRecord\Exceptions\InsufficientAccessException;
use LdapRecord\LdapRecordException;
use LdapRecord\Query\ObjectNotFoundException;
use Nette\NotImplementedException;
use App\Classes\LDAP\Attribute\{Factory,Password};
use App\Classes\LDAP\Server;
use App\Classes\LDAP\Import\LDIF as LDIFImport;
use App\Classes\LDAP\Export\LDIF as LDIFExport;
use App\Exceptions\Import\{GeneralException,VersionException};
@@ -28,7 +29,7 @@ class HomeController extends Controller
{
private const LOGKEY = 'CHc';
private const INTERNAL_POST = ['_key','_rdn','_rdn_value','_step','_template','_token','_userpassword_hash'];
private const INTERNAL_POST = ['_auto_value','_key','_rdn','_rdn_value','_step','_template','_token','_userpassword_hash'];
/**
* Create a new object in the LDAP server
@@ -57,7 +58,7 @@ class HomeController extends Controller
$o->objectclass = [Entry::TAG_NOTAG=>$template->objectclasses->toArray()];
foreach ($o->getAvailableAttributes()
->filter(fn($item)=>$item->names_lc->intersect($template->attributes->map('strtolower'))->count())
->filter(fn($item)=>$item->names_lc->intersect($template->attributes->keys()->map('strtolower'))->count())
->sortBy(fn($item)=>Arr::get($template->order,$item->name)) as $ao)
{
$o->{$ao->name} = [Entry::TAG_NOTAG=>''];
@@ -123,6 +124,13 @@ class HomeController extends Controller
foreach ($request->except(self::INTERNAL_POST) as $key => $value)
$o->{$key} = array_filter($value);
// We need to process and encrypt the password
if ($request->userpassword)
$o->userpassword = $this->password(
$o->getObject('userpassword'),
$request->userpassword,
$request->get('_userpassword_hash'));
try {
$o->save();
@@ -151,6 +159,12 @@ class HomeController extends Controller
));
}
// If there are an _auto_value attributes, we need to invalid those
foreach ($request->get('_auto_value',[]) as $attr => $value) {
Log::debug(sprintf('%s:Removing auto_value attr [%s]',self::LOGKEY,$attr));
Cache::delete($attr.':'.Session::id());
}
return Redirect::to('/')
->withFragment($o->getDNSecure());
}
@@ -277,25 +291,11 @@ class HomeController extends Controller
// @todo Need to handle incoming attributes that were modified by MD5Updates Trait (eg: jpegphoto)
// We need to process and encrypt the password
if ($request->userpassword) {
$passwords = [];
$po = $o->getObject('userpassword');
foreach (Arr::dot($request->userpassword) as $dotkey => $value) {
// If the password is still the MD5 of the old password, then it hasnt changed
if (($old=Arr::get($po,$dotkey)) && ($value === md5($old))) {
$passwords[$dotkey] = $value;
continue;
}
if ($value) {
$type = Arr::get($request->get('_userpassword_hash'),$dotkey);
$passwords[$dotkey] = Password::hash_id($type)
->encode($value);
}
}
$o->userpassword = Arr::undot($passwords);
}
if ($request->userpassword)
$o->userpassword = $this->password(
$o->getObject('userpassword'),
$request->userpassword,
$request->get('_userpassword_hash'));
if (! $o->getDirty())
return back()
@@ -471,6 +471,28 @@ class HomeController extends Controller
->with('ldif',htmlspecialchars($x));
}
private function password(Password $po,array $values,array $hash): array
{
// We need to process and encrypt the password
$passwords = [];
foreach (Arr::dot($values) as $dotkey => $value) {
// If the password is still the MD5 of the old password, then it hasnt changed
if (($old=Arr::get($po,$dotkey)) && ($value === md5($old))) {
$passwords[$dotkey] = $value;
continue;
}
if ($value) {
$type = Arr::get($hash,$dotkey);
$passwords[$dotkey] = Password::hash_id($type)
->encode($value);
}
}
return Arr::undot($passwords);
}
/**
* For any incoming request, work out the command and DN involved
*

View File

@@ -1,29 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use App\Classes\LDAP\Server;
/**
* This sets up our application session with any required values, ultimately for cache optimisation reasons
*/
class ApplicationSession
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle(Request $request,Closure $next): mixed
{
Config::set('server',new Server);
return $next($request);
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Http\Middleware;
use App\Classes\LDAP\Server;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
@@ -43,6 +44,8 @@ class SwapinAuthUser
$c->setConfiguration(config('ldap.connections.'.$key));
$c->setGuardResolver(fn()=>new Guard($c->getLdapConnection(),$c->getConfiguration()));
Config::set('server',new Server);
return $next($request);
}
}

View File

@@ -3,12 +3,17 @@
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use App\Rules\{DNExists,HasStructuralObjectClass};
class EntryAddRequest extends FormRequest
{
private const LOGKEY = 'EAR';
/**
* Get the error messages for the defined validation rules.
*
@@ -112,6 +117,23 @@ class EntryAddRequest extends FormRequest
$fail(__('You cannot select a template and an objectclass'));
},
],
'_auto_value' => 'nullable|array|min:1',
'_auto_value.*' => [
'nullable',
function (string $attribute,mixed $value,\Closure $fail) {
$attr = preg_replace('/^_auto_value\./','',$attribute);
// If the value has been overritten, then our auto_value is invalid
if (! collect(request()->get($attr))->dot()->contains($value))
return;
$cache = Cache::get($attr.':'.Session::id());
Log::debug(sprintf('%s:Autovalue for Attribute [%s] in Session [%s] Retrieved [%d](%d)',self::LOGKEY,$attr,Session::id(),$cache,$value));
if ($cache !== (int)$value)
$fail(__('Lock expired, please re-submit.'));
}
]
])
->toArray();
}

View File

@@ -10,14 +10,24 @@ class EntryRequest extends FormRequest
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*/
public function rules(): array
{
$r = request() ?: collect();
$rk = array_keys($r->all());
return config('server')
->schema('attributetypes')
->intersectByKeys($r->all())
->filter(fn($item)=>$item->names_lc->intersect($rk)->count())
->transform(function($item) use ($rk) {
// Set the attributetype name
if (($x=$item->names_lc->intersect($rk))->count() === 1)
$item->setName($x->pop());
return $item;
})
->map(fn($item)=>$item->validation($r->get('objectclass',[])))
->filter()
->flatMap(fn($item)=>$item)

View File

@@ -49,7 +49,7 @@ class Entry extends Model
$this->objects = collect();
// Load any templates
$this->templates = Cache::remember('templates'.Session::id(),config('ldap.cache.time'),function() {
$this->templates = Cache::remember('templates'.Session::id(),config('ldap.cache.time'),function() {
$template_dir = Storage::disk(config('pla.template.dir'));
$templates = collect();
@@ -465,6 +465,21 @@ class Entry extends Model
->has($key);
}
/**
* Did this query generate a size limit exception
*
* @return bool
* @throws \LdapRecord\ContainerException
*/
public function hasMore(): bool
{
return $this->getConnectionContainer()
->getConnection()
->getLdapConnection()
->getDetailedError()
?->getErrorCode() === 4;
}
/**
* Return an icon for a DN based on objectClass
*

View File

@@ -2,7 +2,6 @@
namespace App\Providers;
use Illuminate\Support\Collection;
use Illuminate\Support\ServiceProvider;
use LdapRecord\Configuration\DomainConfiguration;
use LdapRecord\Laravel\LdapRecord;

View File

@@ -4,7 +4,7 @@ use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use App\Http\Middleware\{AcceptLanguage,AllowAnonymous,ApplicationSession,CheckUpdate,SwapinAuthUser,ViewVariables};
use App\Http\Middleware\{AcceptLanguage,AllowAnonymous,CheckUpdate,SwapinAuthUser,ViewVariables};
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
@@ -16,7 +16,6 @@ return Application::configure(basePath: dirname(__DIR__))
$middleware->appendToGroup(
group: 'web',
middleware: [
ApplicationSession::class,
AcceptLanguage::class,
AllowAnonymous::class,
SwapinAuthUser::class,

View File

@@ -52,7 +52,7 @@ return [
| entry. Instead of using that, you can define your own base DNs to use.
|
*/
'base_dns' => ($x=env('LDAP_BASE_DN', NULL)) ? explode(',',$x) : NULL,
'base_dns' => ($x=env('LDAP_BASE_DN', NULL)) ? explode(':',$x) : NULL,
/*
|--------------------------------------------------------------------------
@@ -95,5 +95,9 @@ return [
'template' => [
'dir' => env('LDAP_TEMPLATE_DRIVER','templates'),
'exclude_system' => env('LDAP_TEMPLATE_EXCLUDE_SYSTEM',FALSE),
'getnextnumber' => [
'gidnumber' => env('LDAP_TEMPLATE_GIDNUMBER_START', 1000),
'uidnumber' => env('LDAP_TEMPLATE_UIDNUMBER_START', 1000),
],
],
];

11
public/css/custom.css vendored
View File

@@ -14,11 +14,20 @@ attribute#objectclass .input-group-end:not(input.form-control) {
z-index: 5;
}
.input-group:first-child .select2-container--bootstrap-5 .select2-selection {
/* select forms that have nothing next to them */
.select-group:first-child .select2-container--bootstrap-5 .select2-selection {
border-radius: 4px !important;
}
.input-group:first-child:not(.select-group) .select2-container--bootstrap-5 .select2-selection {
border-bottom-right-radius: unset;
border-top-right-radius: unset;
}
.select2-container .select2-selection--single .select2-selection__rendered {
font-size: 0.88em;
}
input.form-control.input-group-end {
border-bottom-right-radius: 4px !important;
border-top-right-radius: 4px !important;

View File

@@ -9,13 +9,16 @@
<strong class="align-middle"><abbr title="{{ (($x=$template?->attributeTitle($o->name)) ? $o->name.': ' : '').$o->description }}">{{ $x ?: $o->name }}</abbr></strong>
@if($new)
@if($template?->attributeReadOnly($o->name_lc))
<sup data-bs-toggle="tooltip" title="@lang('Input disabled by template')"><i class="fas fa-ban"></i></sup>
<sup data-bs-toggle="tooltip" title="@lang('Input disabled')"><i class="fas fa-ban"></i></sup>
@endif
@if($template?->onChangeAttribute($o->name_lc))
<sup data-bs-toggle="tooltip" title="@lang('Value triggers an update to another attribute by template')"><i class="fas fa-keyboard"></i></sup>
@if($ca=$template?->onChangeAttribute($o->name_lc))
<sup data-bs-toggle="tooltip" title="@lang('Value triggers an update to another attribute')"><i class="fas fa-keyboard"></i></sup>
@endif
@if ($template?->onChangeTarget($o->name_lc))
<sup data-bs-toggle="tooltip" title="@lang('Value calculated by template')"><i class="fas fa-wand-magic-sparkles"></i></sup>
@if ($ct=$template?->onChangeTarget($o->name_lc))
<sup data-bs-toggle="tooltip" title="@lang('Value calculated from another attribute')"><i class="fas fa-wand-magic-sparkles"></i></sup>
@endif
@if((! $ca) && (! $ct) && $template?->attribute($o->name_lc))
<sup data-bs-toggle="tooltip" title="@lang('Value controlled by template')"><i class="fas fa-wand-magic"></i></sup>
@endif
@endif
@@ -59,7 +62,14 @@
</div>
</div>
<x-attribute :o="$o" :edit="(! $template?->attributeReadOnly($o->name)) && $edit" :new="$new" :updated="$updated"/>
@switch($template?->attributeType($o->name))
@case('select')
<x-attribute.template.select :o="$o" :template="$template" :edit="(! $template?->attributeReadOnly($o->name)) && $edit" :new="$new"/>
@break;
@default
<x-attribute :o="$o" :edit="(! $template?->attributeReadOnly($o->name)) && $edit" :new="$new" :updated="$updated"/>
@endswitch
</div>
</div>

View File

@@ -5,12 +5,25 @@
@foreach($o->langtags as $langtag)
<span @class(['tab-pane','active'=>$loop->index === 0]) id="langtag-{{ $o->name_lc }}-{{ $langtag }}" role="tabpanel">
@foreach(Arr::get(old($o->name_lc,[$langtag=>$new ? [NULL] : $o->tagValues($langtag)]),$langtag,[]) as $key => $value)
<!-- AutoValue Lock -->
@if($new && $template && ($av=$template->attributeValue($o->name_lc)))
<input type="hidden" name="_auto_value[{{ $o->name_lc }}]" value="{{ $av }}">
@endif
<div class="input-group has-validation">
@if($edit && (! $o->is_rdn))
<input type="text" @class(['form-control','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$langtag.'.'.$loop->index)),'mb-1','border-focus'=>! ($tv=$o->tagValuesOld($langtag))->contains($value),'bg-success-subtle'=>$updated]) name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ $value }}" placeholder="{{ ! is_null($x=$tv->get($loop->index)) ? $x : '['.__('NEW').']' }}" @readonly(! $new) @disabled($o->isDynamic())>
@else
<input type="text" @class(['form-control','noedit','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$langtag.'.'.$loop->index)),'mb-1','bg-success-subtle'=>$updated]) name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ $value }}" readonly>
@endif
<input type="text"
@class([
'form-control',
'noedit'=>(! $edit) || ($o->is_rdn),
'is-invalid'=>($e=$errors->get($o->name_lc.'.'.$langtag.'.'.$loop->index)) || ($e=$errors->get('_auto_value.'.$o->name_lc)),
'mb-1',
'border-focus'=>! ($tv=$o->tagValuesOld($langtag))->contains($value),
'bg-success-subtle'=>$updated])
name="{{ $o->name_lc }}[{{ $langtag }}][]"
value="{{ $value ?: ($av ?? '') }}"
placeholder="{{ ! is_null($x=$tv->get($loop->index)) ? $x : '['.__('NEW').']' }}"
readonly
@disabled($o->isDynamic())>
<div class="invalid-feedback pb-2">
@if($e)

View File

@@ -4,7 +4,7 @@
@foreach(($o->tagValues($langtag)->count() ? $o->tagValues($langtag) : [$langtag => NULL]) as $key => $value)
@if($edit)
<div class="input-group has-validation">
<x-form.select id="userpassword_hash_{{$loop->index}}{{$template?->name ?? ''}}" name="_userpassword_hash[{{ $langtag }}][]" :value="$o->hash($new ? '' : ($value ?? ''))->id()" :options="$helpers" allowclear="false" :disabled="! $new"/>
<x-form.select id="userpassword_hash_{{$loop->index}}{{$template?->name ?: ''}}" name="_userpassword_hash[{{ $langtag }}][]" :value="old('_userpassword_hash.'.$langtag.'.0',$o->hash($new ? '' : ($value ?? ''))->id())" :options="$helpers" allowclear="false" :disabled="! $new"/>
<input type="password" @class(['form-control','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$langtag.'.'.$loop->index)),'mb-1','border-focus'=>! $o->tagValuesOld($langtag)->contains($value),'bg-success-subtle'=>$updated]) name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ Arr::get(old($o->name_lc),$langtag.'.'.$loop->index,$value ? md5($value) : '') }}" @readonly(! $new)>
<div class="invalid-feedback pb-2">
@@ -24,7 +24,7 @@
<div class="row">
<div class="offset-1 col-4">
<span class="p-0 m-0">
<button id="entry-userpassword-check" type="button" class="btn btn-sm btn-outline-dark mt-3" data-bs-toggle="modal" data-bs-target="#page-modal"><i class="fas fa-user-check"></i> @lang('Check Password')</button>
<button name="entry-userpassword-check" type="button" class="btn btn-sm btn-outline-dark mt-3" data-bs-toggle="modal" data-bs-target="#page-modal"><i class="fas fa-user-check"></i> @lang('Check Password')</button>
</span>
</div>
</div>

View File

@@ -0,0 +1,28 @@
<!-- $o=Attribute::class -->
<x-attribute.layout :edit="$edit" :new="$new" :o="$o">
@foreach($o->langtags as $langtag)
@foreach(($o->tagValues($langtag)->count() ? $o->tagValues($langtag) : [$langtag => NULL]) as $key => $value)
@if($edit)
<div class="select-group">
<x-form.select
@class(['is-invalid'=>($e=$errors->get($o->name_lc.'.'.$langtag.'.'.$loop->index)),'mb-1','border-focus'=>! $o->tagValuesOld($langtag)->contains($value)])
id="{{ $o->name_lc }}_{{$loop->index}}{{$template?->name ?: ''}}"
name="{{ $o->name_lc }}[{{ $langtag }}][]"
:value="$value"
:options="$template->attributeOptions($o->name_lc)"
allowclear="true"
:disabled="! $new"
:readonly="false"/>
<div class="invalid-feedback pb-2">
@if($e=$errors->get($o->name_lc.'.'.$langtag.'.'.$loop->index))
{{ join('|',$e) }}
@endif
</div>
</div>
@else
{{ $o->render_item_old($langtag.'.'.$key) }}
@endif
@endforeach
@endforeach
</x-attribute.layout>

View File

@@ -15,7 +15,7 @@
@empty($groupby)
@foreach($options as $option)
@continue(! Arr::get($option,'value'))
<option value="{{ Arr::get($option,'id') }}" @selected(Arr::get($option,'id') == collect(old())->dot()->get(isset($old) ? $old.'.0' : ($id ?? $name)))>{{ Arr::get($option,'value') }}</option>
<option value="{{ Arr::get($option,'id') }}" @selected(Arr::get($option,'id') == collect(old())->dot()->get(isset($old) ? $old.'.0' : ($id ?? $name),$value ?? ''))>{{ Arr::get($option,'value') }}</option>
@endforeach
@else
@@ -23,7 +23,7 @@
<optgroup label="{{ Arr::get($group->first(),$groupby) }}">
@foreach($group as $option)
@continue(! Arr::get($option,'value'))
<option value="{{ Arr::get($option,'id') }}" @selected(Arr::get($option,'id') == collect(old())->dot()->get(isset($old) ? $old.'.0' : ($id ?? $name)))>{{ Arr::get($option,'value') }}</option>
<option value="{{ Arr::get($option,'id') }}" @selected(Arr::get($option,'id') == collect(old())->dot()->get(isset($old) ? $old.'.0' : ($id ?? $name),$value ?? ''))>{{ Arr::get($option,'value') }}</option>
@endforeach
</optgroup>
@endforeach

View File

@@ -1,7 +1,7 @@
@extends('architect::layouts.error')
@section('error')
501: @lang('LDAP Authentication Error')
501: @lang('LDAP User Error')
@endsection
@section('content')

View File

@@ -8,8 +8,8 @@
<div class="tab-content">
@php($up=(session()->get('updated') ?: collect()))
@foreach($o->getVisibleAttributes()->filter(fn($item)=>$template->attributes->map('strtolower')->contains($item->name_lc)) as $ao)
<x-attribute-type :o="$ao" :edit="TRUE" :new="FALSE" :template="$template" :updated="$up->contains($ao->name)"/>
@foreach($o->getVisibleAttributes()->filter(fn($item)=>$template->attributes->keys()->map('strtolower')->contains($item->name_lc)) as $ao)
<x-attribute-type :o="$ao" :edit="true" :new="false" :template="$template" :updated="$up->contains($ao->name)"/>
@endforeach
</div>
</div>

View File

@@ -40,7 +40,7 @@
->filter(fn($item)=>$item->isStructural())
->sortBy(fn($item)=>$item->name_lc)
->map(fn($item)=>['id'=>$item->name,'value'=>$item->name])"
:allowclear="TRUE"
:allowclear="true"
/>
</div>
@@ -55,7 +55,7 @@
:label="__('Select a Template').'...'"
:options="$o->templates
->map(fn($item,$key)=>['id'=>$key,'value'=>$item->title])"
:allowclear="TRUE"
:allowclear="true"
/>
</div>
@endif
@@ -64,10 +64,10 @@
@case(2)
<input type="hidden" name="_template" value="{{ $template?->file }}">
<x-attribute-type :o="$o->getObject('rdn')" :edit="TRUE" :new="TRUE" :template="$template" :updated="FALSE"/>
<x-attribute-type :o="$o->getObject('rdn')" :edit="true" :new="true" :template="$template" :updated="false"/>
@foreach($o->getVisibleAttributes() as $ao)
<x-attribute-type :o="$ao" :edit="TRUE" :new="TRUE" :template="$template" :updated="FALSE"/>
<x-attribute-type :o="$ao" :edit="true" :new="true" :template="$template" :updated="false"/>
@endforeach
@if(! $template)

View File

@@ -101,7 +101,7 @@
<div class="tab-content">
@php($up=(session()->pull('updated') ?: collect()))
@foreach($o->getVisibleAttributes() as $ao)
<x-attribute-type :o="$ao" :edit="TRUE" :new="FALSE" :template="NULL" :updated="$up->contains($ao->name)"/>
<x-attribute-type :o="$ao" :edit="true" :new="false" :template="null" :updated="$up->contains($ao->name)"/>
@endforeach
@include('fragment.dn.add_attr')
@@ -124,7 +124,7 @@
<!-- Internal Attributes -->
<div class="tab-pane mt-3" id="internal" role="tabpanel">
@foreach($o->getInternalAttributes() as $ao)
<x-attribute-type :o="$ao" :edit="FALSE" :new="FALSE" :template="$template ?? NULL" :updated="FALSE"/>
<x-attribute-type :o="$ao" :edit="false" :new="false" :template="$template ?? null" :updated="false"/>
@endforeach
</div>
</div>
@@ -282,27 +282,30 @@
})
break;
case 'entry-userpassword-check':
$.ajax({
method: 'GET',
url: '{{ url('modal/userpassword-check') }}/'+dn,
dataType: 'html',
cache: false,
beforeSend: function() {
that.empty().append('<span class="p-3"><i class="fas fa-3x fa-spinner fa-pulse"></i></span>');
},
success: function(data) {
that.empty().html(data);
},
error: function(e) {
if (e.status !== 412)
alert('That didnt work? Please try again....');
},
})
break;
default:
console.log('No action for button:'+$(item.relatedTarget).attr('id'));
switch ($(item.relatedTarget).attr('name')) {
case 'entry-userpassword-check':
$.ajax({
method: 'GET',
url: '{{ url('modal/userpassword-check') }}/'+dn,
dataType: 'html',
cache: false,
beforeSend: function() {
that.empty().append('<span class="p-3"><i class="fas fa-3x fa-spinner fa-pulse"></i></span>');
},
success: function(data) {
that.empty().html(data);
},
error: function(e) {
if (e.status !== 412)
alert('That didnt work? Please try again....');
},
})
break;
default:
console.log('No action for button:'+$(item.relatedTarget).attr('id'));
}
}
});

View File

@@ -21,7 +21,11 @@
: $attribute !!}
</th>
<td>
<x-attribute :edit="false" :o="$ao"/>
@if($ao instanceof \App\Classes\LDAP\Attribute\Schema\OID)
<x-attribute :edit="false" :o="$ao"/>
@else
{!! $ao->values_old->dot()->join('<br>') !!}
@endif
</td>
</tr>
@endforeach

View File

@@ -1,31 +1,32 @@
{
"title": "Example entry",
"description": "This is the description",
"enabled": false,
"icon": "fa-star-of-life",
"rdn": "o",
"regexp": "/^$/",
"title": "Example entry", // Title shown when selecting tempaltes
"description": "This is the description", // Unused, only for documenting
"enabled": false, // Whether template is enabled or not
"icon": "fa-star-of-life", // Icon shown when rendering an existing entry that identifies as this template
"rdn": "o", // @todo not implemented
"regexp": "/^$/", // Regular expression that restricts where this template cna be used
"objectclasses": [
"objectclasses": [ // Objectclasses that entries will have if they use this template
"organization"
],
"attributes": {
"attribute1": {
"display": "Attribute 1",
"hint": "This is an example",
"order": 1
"attributes": { // Attribute configuration
"attribute1": { // LDAP attribute name
"display": "Attribute 1", // Displayed when accepting input for this value
"hint": "This is an example", // @todo not implemented
"type": null, // Default is NULL, so use normal Attribute rendering type
"order": 1 // Order to show attributes
},
"attribute2": {
"display": "Attribute 2",
"hint": "This is an example",
"type": "input", // Default is input
"type": "input", // Force attribute to use template input
"order": 2
},
"attribute3": {
"display": "Attribute 3",
"type": "select",
"options": {
"type": "select", // Force attribute to use template select
"options": { // Select options
"/bin/bash": "Bash",
"/bin/csh": "C Shell",
"/bin/dash": "Dash",
@@ -38,4 +39,4 @@
"order": 8
}
}
}
}

View File

@@ -47,7 +47,7 @@
"uidNumber": {
"display": "UID Number",
"readonly": true,
"value": "=php.GetNextNumber(/;uidNumber)",
"value": "=getNextNumber(/;uidNumber)",
"order": 6
},
"gidNumber": {
@@ -55,8 +55,7 @@
"onchange": [
"=autoFill(homeDirectory;/home/users/%gidNumber|0-0/T%/%uid|3-%)"
],
"value": "=php.GetNextNumber(/;uidNumber)",
"value": "=php.PickList(/;(&(objectClass=posixGroup));gidNumber;%cn%;;;;cn)",
"value": "=getNextNumber(/;gidNumber)",
"order": 7
},
"homeDirectory": {

View File

@@ -0,0 +1,4 @@
dn: cn=z-module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: sssvlv

View File

@@ -1,4 +1,4 @@
dn: olcDatabase={4}mdb,cn=config
changetype: modify
add: olcSizeLimit
olcSizeLimit: 2000
olcSizeLimit: 5

View File

@@ -0,0 +1,8 @@
dn: olcOverlay=sssvlv,olcDatabase={4}mdb,cn=config
changetype: add
objectClass: olcOverlayConfig
objectClass: olcSssVlvConfig
olcOverlay: sssvlv
olcSssVlvMax: 1
olcSssVlvMaxKeys: 5
olcSssVlvMaxPerConn: 5