Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
7346a3daf5 | |||
305ef0f5a3 | |||
f1316d698d | |||
339ba7258a | |||
883ac5d90f | |||
46277146c5 | |||
06747064d4 | |||
2c91298b41 | |||
9798863e34 | |||
4494154879 | |||
b22c9505bc | |||
29a659ff69 | |||
2348da36c4 | |||
6f58f5db36 | |||
553368c7b9 | |||
c8d1122ff6 | |||
2320445dfb | |||
6d2c9d1354 | |||
6f20d426ad | |||
7b1b4f4e50 | |||
543250e1fb | |||
3bf97fc0d1 | |||
3ad4c446ea | |||
ee3cb395c2 |
@ -48,13 +48,17 @@ Entry Editing:
|
||||
- [ ] JpegPhoto Create/Delete
|
||||
- [ ] Binary attribute upload
|
||||
- [ ] If removing an objectClass, remove all attributes that only that objectclass provided
|
||||
- [ ] Move an entry
|
||||
- [ ] Group membership selection
|
||||
- [ ] Attribute tag creation
|
||||
|
||||
Templates Engine
|
||||
- [ ] Present SELECT lists when an attribute is marked as `type=select`
|
||||
- [ ] Enforcing attribute uniqueness
|
||||
|
||||
Raise a [feature request](https://github.com/leenooks/phpLDAPadmin/issues/new) if there is a capability that you would like to see added to PLA.
|
||||
|
||||
Other items [under consideration](https://github.com/leenooks/phpLDAPadmin/issues?q=state%3Aopen%20label%3Aenhancement)
|
||||
|
||||
## Support is known for these LDAP servers:
|
||||
- [X] OpenLDAP
|
||||
- [X] OpenDJ
|
||||
|
@ -172,6 +172,8 @@ class Attribute implements \Countable, \ArrayAccess
|
||||
'required_by' => $this->schema?->required_by_object_classes ?: collect(),
|
||||
// Used in Object Classes
|
||||
'used_in' => $this->schema?->used_in_object_classes ?: collect(),
|
||||
// For single value attributes
|
||||
'value' => $this->schema?->is_single_value ? $this->values->first() : NULL,
|
||||
// The current attribute values
|
||||
'values' => ($this->no_attr_tags || $this->is_internal) ? $this->tagValues() : $this->_values,
|
||||
// The original attribute values
|
||||
|
@ -275,7 +275,6 @@ final class Server
|
||||
'c' // Needed for the tree to show icons for countries
|
||||
]))
|
||||
->list()
|
||||
->orderBy('dn')
|
||||
->get() ?: NULL;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -38,7 +38,7 @@ class AjaxController extends Controller
|
||||
*/
|
||||
public function children(Request $request): Collection
|
||||
{
|
||||
$dn = Crypt::decryptString($request->query('_key'));
|
||||
$dn = Crypt::decryptString($request->post('_key'));
|
||||
|
||||
// Sometimes our key has a command, so we'll ignore it
|
||||
if (str_starts_with($dn,'*') && ($x=strpos($dn,'|')))
|
||||
@ -57,13 +57,18 @@ class AjaxController extends Controller
|
||||
'tooltip'=>$item->getDn(),
|
||||
])
|
||||
->prepend(
|
||||
[
|
||||
'title'=>sprintf('[%s]',__('Create Entry')),
|
||||
'item'=>Crypt::encryptString(sprintf('*%s|%s','create',$dn)),
|
||||
'lazy'=>FALSE,
|
||||
'icon'=>'fas fa-fw fa-square-plus text-warning',
|
||||
'tooltip'=>__('Create new LDAP item here'),
|
||||
]);
|
||||
$request->create
|
||||
? [
|
||||
'title'=>sprintf('[%s]',__('Create Entry')),
|
||||
'item'=>Crypt::encryptString(sprintf('*%s|%s','create',$dn)),
|
||||
'lazy'=>FALSE,
|
||||
'icon'=>'fas fa-fw fa-square-plus text-warning',
|
||||
'tooltip'=>__('Create new LDAP item here'),
|
||||
]
|
||||
: []
|
||||
)
|
||||
->filter()
|
||||
->values();
|
||||
}
|
||||
|
||||
public function schema_view(Request $request)
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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_new','_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());
|
||||
}
|
||||
@ -192,7 +206,7 @@ class HomeController extends Controller
|
||||
}
|
||||
|
||||
return Redirect::to('/')
|
||||
->with('success',[sprintf('%s: %s',__('Deleted'),$dn)]);
|
||||
->with('success',sprintf('%s: %s',__('Deleted'),$dn));
|
||||
}
|
||||
|
||||
public function entry_export(Request $request,string $id): \Illuminate\View\View
|
||||
@ -277,28 +291,14 @@ 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()
|
||||
return Redirect::back()
|
||||
->withInput()
|
||||
->with('note',__('No attributes changed'));
|
||||
|
||||
@ -307,6 +307,31 @@ class HomeController extends Controller
|
||||
->with('o',$o);
|
||||
}
|
||||
|
||||
public function entry_rename(Request $request): \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||
{
|
||||
$from_dn = Crypt::decryptString($request->post('dn'));
|
||||
Log::info(sprintf('%s:Renaming [%s] to [%s]',self::LOGKEY,$from_dn,$request->post('_rdn_new')));
|
||||
|
||||
$o = config('server')->fetch($from_dn);
|
||||
|
||||
if (! $o)
|
||||
return Redirect::back()
|
||||
->withInput()
|
||||
->with('note',__('DN doesnt exist'));
|
||||
|
||||
try {
|
||||
$o->rename($request->post('_rdn_new'));
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return Redirect::to('/')
|
||||
->with('failed',$e->getMessage());
|
||||
}
|
||||
|
||||
return Redirect::to('/')
|
||||
->withInput(['_key'=>Crypt::encryptString('*dn|'.$o->getDN())])
|
||||
->with('success',sprintf('%s: %s',__('Entry renamed'),$from_dn));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a DN entry
|
||||
*
|
||||
@ -326,7 +351,7 @@ class HomeController extends Controller
|
||||
$o->{$key} = array_filter($value);
|
||||
|
||||
if (! $dirty=$o->getDirty())
|
||||
return back()
|
||||
return Redirect::back()
|
||||
->withInput()
|
||||
->with('note',__('No attributes changed'));
|
||||
|
||||
@ -412,7 +437,7 @@ class HomeController extends Controller
|
||||
->with('page_actions',collect([
|
||||
'copy'=>FALSE,
|
||||
'create'=>($x=($o->getObjects()->except('entryuuid')->count() > 0)),
|
||||
'delete'=>$x,
|
||||
'delete'=>(! is_null($xx=$o->getObject('hassubordinates')->value)) && ($xx === 'FALSE'),
|
||||
'edit'=>$x,
|
||||
'export'=>$x,
|
||||
])),
|
||||
@ -471,6 +496,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
|
||||
*
|
||||
@ -483,8 +530,8 @@ class HomeController extends Controller
|
||||
// Setup
|
||||
$cmd = NULL;
|
||||
$dn = NULL;
|
||||
$key = $request->get('_key',old('_key'))
|
||||
? Crypt::decryptString($request->get('_key',old('_key')))
|
||||
$key = ($x=$request->get('_key',old('_key')))
|
||||
? Crypt::decryptString($x)
|
||||
: NULL;
|
||||
|
||||
// Determine if our key has a command
|
||||
@ -496,9 +543,9 @@ class HomeController extends Controller
|
||||
$dn = ($m[2] !== '_NOP') ? $m[2] : NULL;
|
||||
}
|
||||
|
||||
} elseif (old('dn',$request->get('_key'))) {
|
||||
} elseif ($x=old('dn',$request->get('_key'))) {
|
||||
$cmd = 'dn';
|
||||
$dn = Crypt::decryptString(old('dn',$request->get('_key')));
|
||||
$dn = Crypt::decryptString($x);
|
||||
}
|
||||
|
||||
return ['cmd'=>$cmd,'dn'=>$dn];
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use LdapRecord\Configuration\DomainConfiguration;
|
||||
use LdapRecord\Laravel\LdapRecord;
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
],
|
||||
],
|
||||
];
|
@ -1 +1 @@
|
||||
v2.2.0-dev
|
||||
v2.2.1-rel
|
||||
|
11
public/css/custom.css
vendored
11
public/css/custom.css
vendored
@ -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;
|
||||
|
5
public/js/custom.js
vendored
5
public/js/custom.js
vendored
@ -59,7 +59,7 @@ $(document).ready(function() {
|
||||
if (typeof basedn !== 'undefined') {
|
||||
sources = basedn;
|
||||
} else {
|
||||
sources = { url: '/ajax/bases' };
|
||||
sources = { method: 'POST', url: '/ajax/bases' };
|
||||
}
|
||||
|
||||
// Attach the fancytree widget to an existing <div id="tree"> element
|
||||
@ -95,8 +95,9 @@ $(document).ready(function() {
|
||||
source: sources,
|
||||
lazyLoad: function(event,data) {
|
||||
data.result = {
|
||||
method: 'POST',
|
||||
url: '/ajax/children',
|
||||
data: {_key: data.node.data.item,depth: 1}
|
||||
data: {_key: data.node.data.item,create: true}
|
||||
};
|
||||
|
||||
expandChildren(data.tree.rootNode);
|
||||
|
@ -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
|
||||
|
||||
@ -43,7 +46,7 @@
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@if((! $o->no_attr_tags) && (! $o->is_rdn))
|
||||
@if((! $o->no_attr_tags) && (! $o->is_rdn) && (! $template))
|
||||
<span data-bs-toggle="tab" href="#langtag-{{ $o->name_lc }}-+" class="bg-primary-subtle btn btn-outline-primary border-primary addable d-none">
|
||||
<i class="fas fa-fw fa-plus text-dark" data-bs-toggle="tooltip" data-bs-custom-class="custom-tooltip" aria-label="Add Lang Tag" data-bs-original-title="Add Lang Tag"></i>
|
||||
</span>
|
||||
@ -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>
|
||||
|
||||
|
@ -1,16 +1,29 @@
|
||||
<!-- $o=Attribute::class -->
|
||||
<x-attribute.layout :edit="$edit=($edit ?? FALSE)" :new="$new=($new ?? FALSE)" :o="$o">
|
||||
<x-attribute.layout :edit="$edit=($edit ?? FALSE)" :new="$new=($new ?? FALSE)" :o="$o" :template="$template">
|
||||
<div class="col-12">
|
||||
<div class="tab-content">
|
||||
@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(! $edit)
|
||||
@disabled($o->isDynamic())>
|
||||
|
||||
<div class="invalid-feedback pb-2">
|
||||
@if($e)
|
||||
|
@ -4,7 +4,7 @@
|
||||
{{ $slot }}
|
||||
</attribute>
|
||||
|
||||
<x-attribute.widget.options :o="$o" :edit="$edit" :new="$new"/>
|
||||
<x-attribute.widget.options :o="$o" :edit="$edit" :new="$new" :template="$template ?? FALSE"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -6,7 +6,7 @@
|
||||
@php($clone=FALSE)
|
||||
<span class="p-0 m-0">
|
||||
@if($o->is_rdn)
|
||||
<button class="btn btn-sm btn-outline-focus mt-3" disabled><i class="fas fa-fw fa-exchange"></i> @lang('Rename')</button>
|
||||
<span id="entry-rename" class="btn btn-sm btn-outline-focus mt-3" data-bs-toggle="modal" data-bs-target="#page-modal"><i class="fas fa-fw fa-exchange"></i> @lang('Rename')</span>
|
||||
@elseif($edit && $o->can_addvalues)
|
||||
@switch(get_class($o))
|
||||
@case(Certificate::class)
|
||||
@ -26,7 +26,7 @@
|
||||
@break
|
||||
|
||||
@case(ObjectClass::class)
|
||||
<span type="button" @class(['btn','btn-sm','btn-outline-primary','mt-3','addable','d-none'=>(! $new)]) data-bs-toggle="modal" data-bs-target="#new_objectclass-modal"><i class="fas fa-fw fa-plus"></i> @lang('Add Objectclass')</span>
|
||||
<span @class(['btn','btn-sm','btn-outline-primary','mt-3','addable','d-none'=>(! $new)]) data-bs-toggle="modal" data-bs-target="#new_objectclass-modal"><i class="fas fa-fw fa-plus"></i> @lang('Add Objectclass')</span>
|
||||
|
||||
<!-- NEW OBJECT CLASS -->
|
||||
<div class="modal fade" id="new_objectclass-modal" tabindex="-1" aria-labelledby="new_objectclass-label" aria-hidden="true" data-bs-backdrop="static">
|
||||
@ -255,18 +255,18 @@
|
||||
@default
|
||||
@if($o->isDynamic()) @break @endif
|
||||
@php($clone=TRUE)
|
||||
@if($o->values_old->count())
|
||||
<span @class(['btn','btn-sm','btn-outline-primary','mt-3','addable','d-none'=>(! $new)]) data-attribute="{{ $o->name }}" id="{{ $o->name_lc }}-addnew"><i class="fas fa-fw fa-plus"></i> @lang('Add Value')</span>
|
||||
@if($o->values_old->count() && (! $template))
|
||||
<span @class(['btn','btn-sm','btn-outline-primary','mt-3','addable','d-none'=>(! $new)]) data-attribute="{{ $o->name_lc }}" id="{{ $o->name_lc }}-addnew"><i class="fas fa-fw fa-plus"></i> @lang('Add Value')</span>
|
||||
@endif
|
||||
|
||||
@section('page-scripts')
|
||||
@if($clone && $edit && $o->can_addvalues)
|
||||
@if((! $template) && $clone && $edit && $o->can_addvalues)
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
// Create a new entry when Add Value clicked
|
||||
$('#{{ $o->name_lc }}-addnew.addable').click(function(item) {
|
||||
$('form#dn-edit #{{ $o->name_lc }}-addnew.addable').click(function(item) {
|
||||
var attribute = $(this).data('attribute');
|
||||
var active = $('attribute[id='+attribute+']').find('.tab-pane.active');
|
||||
var active = $('#template-default attribute[id='+attribute+']');
|
||||
|
||||
active.find('input:last')
|
||||
.clone()
|
||||
|
@ -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
|
||||
|
@ -1,11 +1,5 @@
|
||||
@if(session()->has('success'))
|
||||
<div class="alert alert-success">
|
||||
<h4 class="alert-heading"><i class="fas fa-fw fa-thumbs-up"></i> Success!</h4>
|
||||
<hr>
|
||||
<ul class="square">
|
||||
@foreach(session()->get('success') as $item)
|
||||
<li>{{ $item }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<div class="alert alert-success p-2">
|
||||
<p class="m-0"><i class="fas fa-fw fa-thumbs-up"></i> {{ session()->pull('success') }}</p>
|
||||
</div>
|
||||
@endif
|
@ -1,7 +1,7 @@
|
||||
@extends('architect::layouts.error')
|
||||
|
||||
@section('error')
|
||||
501: @lang('LDAP Authentication Error')
|
||||
501: @lang('LDAP User Error')
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -9,30 +9,29 @@
|
||||
<div class="col">
|
||||
<div class="action-buttons float-end">
|
||||
<ul class="nav">
|
||||
@if(isset($page_actions) && $page_actions->get('create'))
|
||||
@if($page_actions->get('create'))
|
||||
<li>
|
||||
<button class="btn btn-outline-dark p-1 m-1" id="entry-create" data-bs-toggle="tooltip" data-bs-placement="bottom" title="@lang('Create Child Entry')"><i class="fas fa-fw fa-diagram-project fs-5"></i></button>
|
||||
</li>
|
||||
@endif
|
||||
@if(isset($page_actions) && $page_actions->get('export'))
|
||||
@if($page_actions->get('export'))
|
||||
<li>
|
||||
<span id="entry-export" data-bs-toggle="modal" data-bs-target="#page-modal">
|
||||
<button class="btn btn-outline-dark p-1 m-1" data-bs-toggle="tooltip" data-bs-placement="bottom" title="@lang('Export')"><i class="fas fa-fw fa-download fs-5"></i></button>
|
||||
</span>
|
||||
</li>
|
||||
@endif
|
||||
@if(isset($page_actions) && $page_actions->get('copy'))
|
||||
@if($page_actions->get('copy'))
|
||||
<li>
|
||||
<button class="btn btn-outline-dark p-1 m-1" id="entry-copy-move" data-bs-toggle="tooltip" data-bs-placement="bottom" title="@lang('Copy/Move')" disabled><i class="fas fa-fw fa-copy fs-5"></i></button>
|
||||
</li>
|
||||
@endif
|
||||
@if(isset($page_actions) && $page_actions->get('edit'))
|
||||
@if($page_actions->get('edit'))
|
||||
<li>
|
||||
<button class="btn btn-outline-dark p-1 m-1" id="entry-edit" data-bs-toggle="tooltip" data-bs-placement="bottom" title="@lang('Edit Entry')"><i class="fas fa-fw fa-edit fs-5"></i></button>
|
||||
</li>
|
||||
@endif
|
||||
<!-- @todo Dont offer the delete button for an entry with children -->
|
||||
@if(isset($page_actions) && $page_actions->get('delete'))
|
||||
@if($page_actions->get('delete'))
|
||||
<li>
|
||||
<span id="entry-delete" data-bs-toggle="modal" data-bs-target="#page-modal">
|
||||
<button class="btn btn-outline-danger p-1 m-1" data-bs-custom-class="custom-tooltip-danger" data-bs-toggle="tooltip" data-bs-placement="bottom" title="@lang('Delete Entry')"><i class="fas fa-fw fa-trash-can fs-5"></i></button>
|
||||
@ -52,9 +51,10 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<x-success/>
|
||||
<x-updated/>
|
||||
<x-note/>
|
||||
<x-error/>
|
||||
<x-updated/>
|
||||
<x-failed/>
|
||||
@endsection
|
||||
|
||||
@ -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,10 +282,10 @@
|
||||
})
|
||||
break;
|
||||
|
||||
case 'entry-userpassword-check':
|
||||
case 'entry-rename':
|
||||
$.ajax({
|
||||
method: 'GET',
|
||||
url: '{{ url('modal/userpassword-check') }}/'+dn,
|
||||
url: '{{ url('modal/rename') }}/'+dn,
|
||||
dataType: 'html',
|
||||
cache: false,
|
||||
beforeSend: function() {
|
||||
@ -298,11 +298,33 @@
|
||||
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'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
@section('main-content')
|
||||
<x-success/>
|
||||
<x-failed/>
|
||||
|
||||
<div class="card card-solid mb-3">
|
||||
<div class="card-body">
|
||||
|
40
resources/views/modals/entry-rename.blade.php
Normal file
40
resources/views/modals/entry-rename.blade.php
Normal file
@ -0,0 +1,40 @@
|
||||
<div class="modal-header bg-dark text-white">
|
||||
<h1 class="modal-title fs-5">
|
||||
<strong>@lang('Rename') <strong>{{ $x=Crypt::decryptString($dn) }}</strong>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<form id="entry-rename-form" method="POST" action="{{ url('entry/rename') }}">
|
||||
<div class="modal-body">
|
||||
@csrf
|
||||
<input type="hidden" name="dn" value="{{ $dn }}">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<label for="rdn" class="form-label">@lang('New RDN')</label>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" id="rdn" name="_rdn_new" class="form-control w-25" placeholder="{{ $rdn=collect(explode(',',$x))->first() }}" value="{{ $rdn }}">
|
||||
<span class="input-group-text" id="label">{{ collect(explode(',',$x))->skip(1)->join(',') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<x-modal.close/>
|
||||
<button id="entry-rename" type="submit" class="btn btn-sm btn-primary" disabled>@lang('Rename')</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
var rdn = '{{ $rdn }}';
|
||||
|
||||
// Complete the RDN
|
||||
$('#rdn').on('input',function(item) {
|
||||
rdn = $(this).val();
|
||||
|
||||
$('button[id=entry-rename]').attr('disabled',! rdn.includes('='));
|
||||
})
|
||||
});
|
||||
</script>
|
@ -49,6 +49,7 @@ Route::controller(HomeController::class)->group(function() {
|
||||
Route::post('entry/password/check/','entry_password_check');
|
||||
Route::post('entry/attr/add/{id}','entry_attr_add');
|
||||
Route::post('entry/objectclass/add','entry_objectclass_add');
|
||||
Route::post('entry/rename','entry_rename');
|
||||
Route::post('entry/update/commit','entry_update');
|
||||
Route::post('entry/update/pending','entry_pending_update');
|
||||
|
||||
@ -56,6 +57,7 @@ Route::controller(HomeController::class)->group(function() {
|
||||
|
||||
Route::view('modal/delete/{dn}','modals.entry-delete');
|
||||
Route::view('modal/export/{dn}','modals.entry-export');
|
||||
Route::view('modal/rename/{dn}','modals.entry-rename');
|
||||
Route::view('modal/userpassword-check/{dn}','modals.entry-userpassword-check');
|
||||
});
|
||||
});
|
||||
@ -63,8 +65,8 @@ Route::controller(HomeController::class)->group(function() {
|
||||
Route::controller(AjaxController::class)
|
||||
->prefix('ajax')
|
||||
->group(function() {
|
||||
Route::get('bases','bases');
|
||||
Route::get('children','children');
|
||||
Route::post('bases','bases');
|
||||
Route::post('children','children');
|
||||
Route::post('schema/view','schema_view');
|
||||
Route::post('schema/objectclass/attrs/{id}','schema_objectclass_attrs');
|
||||
});
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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": {
|
||||
|
@ -99,10 +99,7 @@ jpegphoto:: /9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkS
|
||||
Q05bKyt9PtEtrWPZEuSBkkkk5JJPJJJJJPJJoopOc5xu43SZGyMWYLDuXRRRRTU9f//Z
|
||||
l: Springfield
|
||||
mail: bart.simpson@simpsons.example.com
|
||||
mailHost: simpsons.host
|
||||
mailRoutingAddress: bsimpson@simpsons.example.com
|
||||
o: The Simpsons
|
||||
objectclass: inetLocalMailRecipient
|
||||
objectclass: inetOrgPerson
|
||||
objectclass: posixAccount
|
||||
objectclass: top
|
||||
|
4
tests/server/openldap/schema/modify/00-ssslv.ldif
Normal file
4
tests/server/openldap/schema/modify/00-ssslv.ldif
Normal file
@ -0,0 +1,4 @@
|
||||
dn: cn=z-module{0},cn=config
|
||||
changetype: modify
|
||||
add: olcModuleLoad
|
||||
olcModuleLoad: sssvlv
|
@ -1,4 +1,4 @@
|
||||
dn: olcDatabase={4}mdb,cn=config
|
||||
changetype: modify
|
||||
add: olcSizeLimit
|
||||
olcSizeLimit: 2000
|
||||
olcSizeLimit: 5
|
8
tests/server/openldap/schema/modify/40-ssslv.ldif
Normal file
8
tests/server/openldap/schema/modify/40-ssslv.ldif
Normal 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
|
Loading…
x
Reference in New Issue
Block a user