Compare commits

...

13 Commits

Author SHA1 Message Date
2d732e5d52 Remove our highlighted item from the tree, when we click on the top-menu buttons
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 1m39s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 2m53s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2025-06-19 17:46:13 +10:00
d294c9449d Fix page expired 419 started showing a page expired message, instead of refreshing the session and loading the clicked item on the tree 2025-06-19 17:22:24 +10:00
deefd9be43 Fix for when specifying multiple base DNs with LDAP_BASE_DN, and the user doesnt have access to the first one. 2025-06-19 16:15:22 +10:00
59cf0d337e Add alert for DN logins that dont exist. Might be attempts to use the rootdn which is not supported.
Closes #345
2025-06-19 16:15:22 +10:00
c4b1d9ec51 Pass the template object to the attributes, so we can leverage template rules when rendering attributes 2025-06-19 16:15:22 +10:00
5ce3a63878 Revert c56df8d3d and remove adding Objects directly - taking a different approach to add template actions 2025-06-19 16:15:22 +10:00
ac8e79ab99 Minor logging message updates, no functional changes 2025-06-19 16:15:22 +10:00
d0c02b91c0 Re-implement LDAP_BASE_DN to limit what is shown in the tree, and what PLA uses internally to search the server. Fixes #342 2025-06-19 16:15:22 +10:00
2a691c147e Remove references to APP_URL and LDAP_BASE_DN, they are not actually used 2025-06-19 16:15:22 +10:00
781c87cb83 Fix positioning of Check Password box, and dont render it when creating a new entry 2025-06-19 16:15:22 +10:00
98a0b87afe Add objects directly to Entry::class when rendering a template. Fix objectclasses and attributes processing for templates 2025-06-19 16:15:22 +10:00
88db4ccc99 Update AttributeTypes/LDAPSyntaxes/MatchingRules for performance and process improvements 2025-06-18 22:39:23 +10:00
6059bc1e45 Pass template to our component rendering to avoid duplicate javascript object id's 2025-06-18 22:39:23 +10:00
46 changed files with 816 additions and 1029 deletions

View File

@ -2,7 +2,6 @@ APP_NAME=Laravel
APP_ENV=production APP_ENV=production
APP_KEY= APP_KEY=
APP_DEBUG=false APP_DEBUG=false
APP_URL=http://localhost
LOG_CHANNEL=daily LOG_CHANNEL=daily
@ -12,7 +11,7 @@ SESSION_DRIVER=file
SESSION_LIFETIME=120 SESSION_LIFETIME=120
LDAP_HOST= LDAP_HOST=
LDAP_BASE_DN=
LDAP_USERNAME= LDAP_USERNAME=
LDAP_PASSWORD= LDAP_PASSWORD=
LDAP_CACHE=false LDAP_CACHE=false
LDAP_ALERT_ROOTDN=true

View File

@ -2,7 +2,6 @@ APP_NAME=Laravel
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stderr LOG_CHANNEL=stderr
@ -12,7 +11,6 @@ SESSION_DRIVER=file
SESSION_LIFETIME=120 SESSION_LIFETIME=120
LDAP_HOST=openldap LDAP_HOST=openldap
LDAP_BASE_DN="dc=Test"
LDAP_USERNAME="cn=admin,dc=Test" LDAP_USERNAME="cn=admin,dc=Test"
LDAP_PASSWORD="test" LDAP_PASSWORD="test"
LDAP_CACHE=false LDAP_CACHE=false

View File

@ -7,6 +7,8 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use App\Classes\LDAP\Schema\AttributeType; use App\Classes\LDAP\Schema\AttributeType;
use App\Classes\Template;
use App\Exceptions\InvalidUsage;
use App\Ldap\Entry; use App\Ldap\Entry;
/** /**
@ -14,9 +16,6 @@ use App\Ldap\Entry;
*/ */
class Attribute implements \Countable, \ArrayAccess class Attribute implements \Countable, \ArrayAccess
{ {
// Attribute Name
protected string $name;
// Is this attribute an internal attribute // Is this attribute an internal attribute
protected ?bool $_is_internal = NULL; protected ?bool $_is_internal = NULL;
protected(set) bool $no_attr_tags = FALSE; protected(set) bool $no_attr_tags = FALSE;
@ -98,11 +97,11 @@ class Attribute implements \Countable, \ArrayAccess
* @param string $name Name of the attribute * @param string $name Name of the attribute
* @param array $values Current Values * @param array $values Current Values
* @param array $oc The objectclasses that the DN of this attribute has * @param array $oc The objectclasses that the DN of this attribute has
* @throws InvalidUsage
*/ */
public function __construct(string $dn,string $name,array $values,array $oc=[]) public function __construct(string $dn,string $name,array $values,array $oc=[])
{ {
$this->dn = $dn; $this->dn = $dn;
$this->name = $name;
$this->_values = collect($values); $this->_values = collect($values);
$this->_values_old = collect($values); $this->_values_old = collect($values);
@ -113,8 +112,12 @@ class Attribute implements \Countable, \ArrayAccess
// Get the objectclass heirarchy for required attribute determination // Get the objectclass heirarchy for required attribute determination
foreach ($oc as $objectclass) { foreach ($oc as $objectclass) {
$this->oc->push($objectclass); $soc = config('server')->schema('objectclasses',$objectclass);
$this->oc = $this->oc->merge(config('server')->schema('objectclasses',$objectclass)->getParents()->pluck('name'));
if ($soc) {
$this->oc->push($soc->oid);
$this->oc = $this->oc->merge($soc->getParents()->pluck('oid'));
}
} }
/* /*
@ -140,8 +143,6 @@ class Attribute implements \Countable, \ArrayAccess
public function __get(string $key): mixed public function __get(string $key): mixed
{ {
return match ($key) { return match ($key) {
// List all the attributes
'attributes' => $this->attributes(),
// Can this attribute have more values // Can this attribute have more values
'can_addvalues' => $this->schema && (! $this->schema->is_single_value) && ((! $this->max_values_count) || ($this->values->count() < $this->max_values_count)), 'can_addvalues' => $this->schema && (! $this->schema->is_single_value) && ((! $this->max_values_count) || ($this->values->count() < $this->max_values_count)),
// Schema attribute description // Schema attribute description
@ -164,7 +165,7 @@ class Attribute implements \Countable, \ArrayAccess
// Is this attribute an RDN attribute // Is this attribute an RDN attribute
'is_rdn' => $this->isRDN(), 'is_rdn' => $this->isRDN(),
// We prefer the name as per the schema if it exists // We prefer the name as per the schema if it exists
'name' => $this->schema ? $this->schema->{$key} : $this->{$key}, 'name' => $this->schema->{$key},
// Attribute name in lower case // Attribute name in lower case
'name_lc' => strtolower($this->name), 'name_lc' => strtolower($this->name),
// Required by Object Classes // Required by Object Classes
@ -268,9 +269,6 @@ class Attribute implements \Countable, \ArrayAccess
if ($this->is_rdn) if ($this->is_rdn)
$result->put(__('rdn'),__('This attribute is required for the RDN')); $result->put(__('rdn'),__('This attribute is required for the RDN'));
// If this attribute name is an alias for the schema attribute name
// @todo
if ($this->required()->count()) if ($this->required()->count())
$result->put(__('required'),sprintf('%s: %s',__('Required Attribute by ObjectClass(es)'),$this->required()->join(', '))); $result->put(__('required'),sprintf('%s: %s',__('Required Attribute by ObjectClass(es)'),$this->required()->join(', ')));
@ -302,7 +300,7 @@ class Attribute implements \Countable, \ArrayAccess
{ {
return $this->schema->used_in_object_classes return $this->schema->used_in_object_classes
->keys() ->keys()
->intersect($this->schema->heirachy($this->oc)) ->intersect($this->oc)
->count() === 0; ->count() === 0;
} }
@ -329,9 +327,10 @@ class Attribute implements \Countable, \ArrayAccess
* @param bool $old Use old value * @param bool $old Use old value
* @param bool $new Enable adding values * @param bool $new Enable adding values
* @param bool $updated Has the entry been updated (uses rendering highlights)) * @param bool $updated Has the entry been updated (uses rendering highlights))
* @param Template|null $template
* @return View * @return View
*/ */
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE): View public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
{ {
if ($this->is_internal) if ($this->is_internal)
// @note Internal attributes cannot be edited // @note Internal attributes cannot be edited
@ -352,6 +351,7 @@ class Attribute implements \Countable, \ArrayAccess
->with('edit',$edit) ->with('edit',$edit)
->with('old',$old) ->with('old',$old)
->with('new',$new) ->with('new',$new)
->with('template',$template)
->with('updated',$updated); ->with('updated',$updated);
} }

View File

@ -5,7 +5,7 @@ namespace App\Classes\LDAP\Attribute\Binary;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use App\Classes\LDAP\Attribute\Binary; use App\Classes\LDAP\Attribute\Binary;
use App\Ldap\Entry; use App\Classes\Template;
use App\Traits\MD5Updates; use App\Traits\MD5Updates;
/** /**
@ -15,7 +15,7 @@ final class JpegPhoto extends Binary
{ {
use MD5Updates; use MD5Updates;
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE): View public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
{ {
return view('components.attribute.binary.jpegphoto') return view('components.attribute.binary.jpegphoto')
->with('o',$this) ->with('o',$this)

View File

@ -2,9 +2,6 @@
namespace App\Classes\LDAP\Attribute; namespace App\Classes\LDAP\Attribute;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use App\Classes\LDAP\Attribute; use App\Classes\LDAP\Attribute;
use App\Traits\MD5Updates; use App\Traits\MD5Updates;

View File

@ -3,7 +3,6 @@
namespace App\Classes\LDAP\Attribute; namespace App\Classes\LDAP\Attribute;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use App\Classes\LDAP\Attribute; use App\Classes\LDAP\Attribute;
@ -59,8 +58,6 @@ class Factory
public static function create(string $dn,string $attribute,array $values,array $oc=[]): Attribute public static function create(string $dn,string $attribute,array $values,array $oc=[]): Attribute
{ {
$class = Arr::get(self::map,strtolower($attribute),Attribute::class); $class = Arr::get(self::map,strtolower($attribute),Attribute::class);
Log::debug(sprintf('%s:Creating LDAP Attribute [%s] as [%s]',static::LOGKEY,$attribute,$class));
return new $class($dn,$attribute,$values,$oc); return new $class($dn,$attribute,$values,$oc);
} }
} }

View File

@ -5,14 +5,14 @@ namespace App\Classes\LDAP\Attribute\Internal;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use App\Classes\LDAP\Attribute\Internal; use App\Classes\LDAP\Attribute\Internal;
use App\Ldap\Entry; use App\Classes\Template;
/** /**
* Represents an attribute whose values are timestamps * Represents an attribute whose values are timestamps
*/ */
final class Timestamp extends Internal final class Timestamp extends Internal
{ {
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE): View public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
{ {
// @note Internal attributes cannot be edited // @note Internal attributes cannot be edited
return view('components.attribute.internal.timestamp') return view('components.attribute.internal.timestamp')

View File

@ -5,7 +5,7 @@ namespace App\Classes\LDAP\Attribute;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use App\Classes\LDAP\Attribute; use App\Classes\LDAP\Attribute;
use App\Ldap\Entry; use App\Classes\Template;
use App\Traits\MD5Updates; use App\Traits\MD5Updates;
/** /**
@ -17,7 +17,7 @@ final class KrbPrincipalKey extends Attribute
protected(set) bool $no_attr_tags = TRUE; protected(set) bool $no_attr_tags = TRUE;
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE): View public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
{ {
return view('components.attribute.krbprincipalkey') return view('components.attribute.krbprincipalkey')
->with('o',$this) ->with('o',$this)

View File

@ -6,7 +6,7 @@ use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use App\Classes\LDAP\Attribute; use App\Classes\LDAP\Attribute;
use App\Ldap\Entry; use App\Classes\Template;
/** /**
* Represents an attribute whose value is a Kerberos Ticket Flag * Represents an attribute whose value is a Kerberos Ticket Flag
@ -50,7 +50,7 @@ final class KrbTicketFlags extends Attribute
return $helpers; return $helpers;
} }
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE): View public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
{ {
return view('components.attribute.krbticketflags') return view('components.attribute.krbticketflags')
->with('o',$this) ->with('o',$this)

View File

@ -6,7 +6,7 @@ use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use App\Classes\LDAP\Attribute; use App\Classes\LDAP\Attribute;
use App\Ldap\Entry; use App\Classes\Template;
/** /**
* Represents an ObjectClass Attribute * Represents an ObjectClass Attribute
@ -70,7 +70,7 @@ final class ObjectClass extends Attribute
->contains($value); ->contains($value);
} }
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE): View public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
{ {
return view('components.attribute.objectclass') return view('components.attribute.objectclass')
->with('o',$this) ->with('o',$this)

View File

@ -7,7 +7,7 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use App\Classes\LDAP\Attribute; use App\Classes\LDAP\Attribute;
use App\Ldap\Entry; use App\Classes\Template;
use App\Traits\MD5Updates; use App\Traits\MD5Updates;
/** /**
@ -80,13 +80,14 @@ final class Password extends Attribute
return ($helpers=static::helpers())->has($id) ? new ($helpers->get($id)) : NULL; return ($helpers=static::helpers())->has($id) ? new ($helpers->get($id)) : NULL;
} }
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE): View public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
{ {
return view('components.attribute.password') return view('components.attribute.password')
->with('o',$this) ->with('o',$this)
->with('edit',$edit) ->with('edit',$edit)
->with('old',$old) ->with('old',$old)
->with('new',$new) ->with('new',$new)
->with('template',$template)
->with('updated',$updated) ->with('updated',$updated)
->with('helpers',static::helpers()->map(fn($item,$key)=>['id'=>$key,'value'=>$key])->sort()); ->with('helpers',static::helpers()->map(fn($item,$key)=>['id'=>$key,'value'=>$key])->sort());
} }

View File

@ -6,7 +6,7 @@ use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use App\Classes\LDAP\Attribute; use App\Classes\LDAP\Attribute;
use App\Ldap\Entry; use App\Classes\Template;
/** /**
* Represents the RDN for an Entry * Represents the RDN for an Entry
@ -35,7 +35,7 @@ final class RDN extends Attribute
]); ]);
} }
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE): View public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
{ {
return view('components.attribute.rdn') return view('components.attribute.rdn')
->with('o',$this); ->with('o',$this);

View File

@ -2,12 +2,10 @@
namespace App\Classes\LDAP\Attribute; namespace App\Classes\LDAP\Attribute;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use App\Classes\LDAP\Attribute; use App\Classes\LDAP\Attribute;
use App\Ldap\Entry;
/** /**
* Represents an attribute whose values are schema related * Represents an attribute whose values are schema related
@ -53,11 +51,4 @@ abstract class Schema extends Attribute
$key, $key,
__('No description available, can you help with one?')); __('No description available, can you help with one?'));
} }
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE): View
{
// @note Schema attributes cannot be edited
return view('components.attribute.internal')
->with('o',$this);
}
} }

View File

@ -5,14 +5,14 @@ namespace App\Classes\LDAP\Attribute\Schema;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use App\Classes\LDAP\Attribute\Schema; use App\Classes\LDAP\Attribute\Schema;
use App\Ldap\Entry; use App\Classes\Template;
/** /**
* Represents a Generic Schema Attribute * Represents a Generic Schema Attribute
*/ */
class Generic extends Schema class Generic extends Schema
{ {
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE): View public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
{ {
// @note Schema attributes cannot be edited // @note Schema attributes cannot be edited
return view('components.attribute.schema.generic') return view('components.attribute.schema.generic')

View File

@ -5,7 +5,7 @@ namespace App\Classes\LDAP\Attribute\Schema;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use App\Classes\LDAP\Attribute\Schema; use App\Classes\LDAP\Attribute\Schema;
use App\Ldap\Entry; use App\Classes\Template;
/** /**
* Represents a Mechanisms Attribute * Represents a Mechanisms Attribute
@ -34,7 +34,7 @@ final class Mechanisms extends Schema
return parent::_get(config_path('ldap_supported_saslmechanisms.txt'),$string,$key); return parent::_get(config_path('ldap_supported_saslmechanisms.txt'),$string,$key);
} }
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE): View public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
{ {
// @note Schema attributes cannot be edited // @note Schema attributes cannot be edited
return view('components.attribute.schema.mechanisms') return view('components.attribute.schema.mechanisms')

View File

@ -5,7 +5,7 @@ namespace App\Classes\LDAP\Attribute\Schema;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use App\Classes\LDAP\Attribute\Schema; use App\Classes\LDAP\Attribute\Schema;
use App\Ldap\Entry; use App\Classes\Template;
/** /**
* Represents an OID Attribute * Represents an OID Attribute
@ -35,7 +35,7 @@ final class OID extends Schema
return parent::_get(config_path('ldap_supported_oids.txt'),$string,$key); return parent::_get(config_path('ldap_supported_oids.txt'),$string,$key);
} }
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE): View public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
{ {
// @note Schema attributes cannot be edited // @note Schema attributes cannot be edited
return view('components.attribute.schema.oid') return view('components.attribute.schema.oid')

View File

@ -29,7 +29,7 @@ abstract class Export
abstract public function __toString(): string; abstract public function __toString(): string;
protected function header() protected function header(): string
{ {
$output = ''; $output = '';
@ -42,7 +42,7 @@ abstract class Export
//$output .= sprintf('# %s: %s',__('Search Filter'),$this->entry->dn).$this->br; //$output .= sprintf('# %s: %s',__('Search Filter'),$this->entry->dn).$this->br;
$output .= sprintf('# %s: %s',__('Total Entries'),$this->items->count()).$this->br; $output .= sprintf('# %s: %s',__('Total Entries'),$this->items->count()).$this->br;
$output .= '#'.$this->br; $output .= '#'.$this->br;
$output .= sprintf('# %s %s (%s) on %s',__('Generated by'),config('app.name'),config('app.url'),date('F j, Y g:i a')).$this->br; $output .= sprintf('# %s %s (%s) on %s',__('Generated by'),config('app.name'),request()->root(),date('F j, Y g:i a')).$this->br;
$output .= sprintf('# %s %s',__('Exported by'),Auth::user() ?: 'Anonymous').$this->br; $output .= sprintf('# %s %s',__('Exported by'),Auth::user() ?: 'Anonymous').$this->br;
$output .= sprintf('# %s: %s',__('Version'),config('app.version')).$this->br; $output .= sprintf('# %s: %s',__('Version'),config('app.version')).$this->br;

View File

@ -7,304 +7,74 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use App\Classes\LDAP\Attribute; use App\Classes\LDAP\Attribute;
use App\Exceptions\InvalidUsage;
use App\Ldap\Entry; use App\Ldap\Entry;
/** /**
* Represents an LDAP AttributeType * Represents an LDAP AttributeType
*
* @package phpLDAPadmin
* @subpackage Schema
*/ */
final class AttributeType extends Base { final class AttributeType extends Base
// The attribute from which this attribute inherits (if any) {
private ?string $sup_attribute = NULL; private const LOGKEY = 'SAT';
// Array of AttributeTypes which inherit from this one // An array of AttributeTypes which inherit from this one
private Collection $children; private(set) Collection $children;
// The equality rule used // The equality rule used
private ?string $equality = NULL; private(set) ?string $equality = NULL;
// The ordering of the attributeType
private ?string $ordering = NULL;
// Supports substring matching?
private ?string $sub_str_rule = NULL;
// The full syntax string, ie 1.2.3.4{16}
private ?string $syntax = NULL;
private ?string $syntax_oid = NULL;
// boolean: is single valued only?
private bool $is_single_value = FALSE;
// boolean: is collective?
private bool $is_collective = FALSE;
// boolean: can use modify?
private bool $is_no_user_modification = FALSE;
// The usage string set by the LDAP schema
private ?string $usage = NULL;
// An array of alias attribute names, strings
private Collection $aliases;
// The max number of characters this attribute can be
private ?int $max_length = NULL;
// A string description of the syntax type (taken from the LDAPSyntaxes)
/**
* @deprecated - reference syntaxes directly if possible
* @var string
*/
private ?string $type = NULL;
// An array of objectClasses which use this attributeType (must be set by caller)
private Collection $used_in_object_classes;
// A list of object class names that require this attribute type.
private Collection $required_by_object_classes;
// This attribute has been forced a MAY attribute by the configuration. // This attribute has been forced a MAY attribute by the configuration.
private bool $forced_as_may = FALSE; private(set) bool $forced_as_may = FALSE;
/** // boolean: is collective?
* Creates a new AttributeType object from a raw LDAP AttributeType string. private(set) bool $is_collective = FALSE;
*
* eg: ( 2.5.4.0 NAME 'objectClass' DESC 'RFC4512: object classes of the entity' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )
*/
public function __construct(string $line) {
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('Parsing AttributeType [%s]',$line));
parent::__construct($line); // Is this a must attribute
private(set) bool $is_must = FALSE;
$strings = preg_split('/[\s,]+/',$line,-1,PREG_SPLIT_DELIM_CAPTURE); // boolean: can use modify?
private(set) bool $is_no_user_modification = FALSE;
// Init // boolean: is single valued only?
$this->children = collect(); private(set) bool $is_single_value = FALSE;
$this->aliases = collect();
$this->used_in_object_classes = collect();
$this->required_by_object_classes = collect();
for ($i=0; $i < count($strings); $i++) { // The max number of characters this attribute can be
switch ($strings[$i]) { private(set) ?int $max_length = NULL;
case '(':
case ')':
break;
case 'NAME': // An array of names (including aliases) that this attribute is known by
// @note Some schema's return a (' instead of a ( ' private(set) Collection $names;
if ($strings[$i+1] != '(' && ! preg_match('/^\(/',$strings[$i+1])) {
do {
$this->name .= ($this->name ? ' ' : '').$strings[++$i];
} while (! preg_match("/\'$/s",$strings[$i])); // The ordering of the attributeType
private(set) ?string $ordering = NULL;
// This attribute has no aliases // A list of object class names that require this attribute type.
//$this->aliases = collect(); private(set) Collection $required_by_object_classes;
} else { // Which objectclass is defining this attribute for an Entry
$i++; public ?string $source = NULL;
do { // Supports substring matching?
// In case we came here becaues of a (' private(set) ?string $sub_str_rule = NULL;
if (preg_match('/^\(/',$strings[$i]))
$strings[$i] = preg_replace('/^\(/','',$strings[$i]);
else
$i++;
$this->name .= ($this->name ? ' ' : '').$strings[++$i]; // The attribute from which this attribute inherits (if any)
private(set) ?string $sup_attribute = NULL;
} while (! preg_match("/\'$/s",$strings[$i])); // The full syntax string, ie 1.2.3.4{16}
private(set) ?string $syntax = NULL;
private(set) ?string $syntax_oid = NULL;
// Add alias names for this attribute // The usage string set by the LDAP schema
while ($strings[++$i] != ')') { private(set) ?string $usage = NULL;
$alias = $strings[$i];
$alias = preg_replace("/^\'(.*)\'$/",'$1',$alias);
$this->addAlias($alias);
}
}
$this->name = preg_replace("/^\'(.*)\'$/",'$1',$this->name); // An array of objectClasses which use this attributeType (must be set by caller)
private(set) Collection $used_in_object_classes;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case NAME returned (%s)',$this->name),['aliases'=>$this->aliases]);
break;
case 'DESC':
do {
$this->description .= ($this->description ? ' ' : '').$strings[++$i];
} while (! preg_match("/\'$/s",$strings[$i]));
$this->description = preg_replace("/^\'(.*)\'$/",'$1',$this->description);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case DESC returned (%s)',$this->description));
break;
case 'OBSOLETE':
$this->is_obsolete = TRUE;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case OBSOLETE returned (%s)',$this->is_obsolete));
break;
case 'SUP':
$i++;
$this->sup_attribute = preg_replace("/^\'(.*)\'$/",'$1',$strings[$i]);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case SUP returned (%s)',$this->sup_attribute));
break;
case 'EQUALITY':
$this->equality = $strings[++$i];
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case EQUALITY returned (%s)',$this->equality));
break;
case 'ORDERING':
$this->ordering = $strings[++$i];
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case ORDERING returned (%s)',$this->ordering));
break;
case 'SUBSTR':
$this->sub_str_rule = $strings[++$i];
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case SUBSTR returned (%s)',$this->sub_str_rule));
break;
case 'SYNTAX':
$this->syntax = $strings[++$i];
$this->syntax_oid = preg_replace('/{\d+}$/','',$this->syntax);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('/ Evaluating SYNTAX returned (%s) [%s]',$this->syntax,$this->syntax_oid));
// Does this SYNTAX string specify a max length (ie, 1.2.3.4{16})
$m = [];
if (preg_match('/{(\d+)}$/',$this->syntax,$m))
$this->max_length = $m[1];
else
$this->max_length = NULL;
if ($i < count($strings) - 1 && $strings[$i+1] == '{')
do {
$this->name .= ' '.$strings[++$i];
} while ($strings[$i] != '}');
$this->syntax = preg_replace("/^\'(.*)\'$/",'$1',$this->syntax);
$this->syntax_oid = preg_replace("/^\'(.*)\'$/",'$1',$this->syntax_oid);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case SYNTAX returned (%s) [%s] {%d}',$this->syntax,$this->syntax_oid,$this->max_length));
break;
case 'SINGLE-VALUE':
$this->is_single_value = TRUE;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case SINGLE-VALUE returned (%s)',$this->is_single_value));
break;
case 'COLLECTIVE':
$this->is_collective = TRUE;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case COLLECTIVE returned (%s)',$this->is_collective));
break;
case 'NO-USER-MODIFICATION':
$this->is_no_user_modification = TRUE;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case NO-USER-MODIFICATION returned (%s)',$this->is_no_user_modification));
break;
case 'USAGE':
$this->usage = $strings[++$i];
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case USAGE returned (%s)',$this->usage));
break;
// @note currently not captured
case 'X-ORDERED':
if (static::DEBUG_VERBOSE)
Log::error(sprintf('- Case X-ORDERED returned (%s)',$strings[++$i]));
break;
// @note currently not captured
case 'X-ORIGIN':
$value = '';
do {
$value .= ($value ? ' ' : '').$strings[++$i];
} while (! preg_match("/\'$/s",$strings[$i]));
if (static::DEBUG_VERBOSE)
Log::error(sprintf('- Case X-ORIGIN returned (%s)',$value));
break;
default:
if (preg_match('/[\d\.]+/i',$strings[$i]) && ($i === 1)) {
$this->oid = $strings[$i];
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case default returned (%s)',$this->oid));
} elseif ($strings[$i])
Log::alert(sprintf('! Case default discovered a value NOT parsed (%s)',$strings[$i]),['line'=>$line]);
}
}
}
public function __clone()
{
// When we clone, we need to break the reference too
$this->aliases = clone $this->aliases;
}
public function __get(string $key): mixed public function __get(string $key): mixed
{ {
switch ($key) { return match ($key) {
case 'aliases': return $this->aliases; 'names_lc' => $this->names->map('strtolower'),
case 'children': return $this->children; default => parent::__get($key)
case 'forced_as_may': return $this->forced_as_may; };
case 'is_collective': return $this->is_collective;
case 'is_editable': return ! $this->is_no_user_modification;
case 'is_no_user_modification': return $this->is_no_user_modification;
case 'is_single_value': return $this->is_single_value;
case 'equality': return $this->equality;
case 'max_length': return $this->max_length;
case 'ordering': return $this->ordering;
case 'required_by_object_classes': return $this->required_by_object_classes;
case 'sub_str_rule': return $this->sub_str_rule;
case 'sup_attribute': return $this->sup_attribute;
case 'syntax': return $this->syntax;
case 'syntax_oid': return $this->syntax_oid;
case 'type': return $this->type;
case 'usage': return $this->usage;
case 'used_in_object_classes': return $this->used_in_object_classes;
default: return parent::__get($key);
}
}
/**
* Adds an attribute name to the alias array.
*
* @param string $alias The name of a new attribute to add to this attribute's list of aliases.
*/
public function addAlias(string $alias): void
{
$this->aliases->push($alias);
} }
/** /**
@ -315,7 +85,8 @@ final class AttributeType extends Base {
*/ */
public function addChild(string $child): void public function addChild(string $child): void
{ {
$this->children->push($child); $this->children
->push($child);
} }
/** /**
@ -346,144 +117,10 @@ final class AttributeType extends Base {
private function factory(): Attribute private function factory(): Attribute
{ {
return Attribute\Factory::create(dn:'',attribute:$this->name,values:[]); return Attribute\Factory::create(
} dn:'',
attribute:$this->name,
/** values:[]);
* Gets the names of attributes that are an alias for this attribute (if any).
*
* @return Collection An array of names of attributes which alias this attribute or
* an empty array if no attribute aliases this object.
* @deprecated use class->aliases
*/
public function getAliases(): Collection
{
return $this->aliases;
}
/**
* Gets this attribute's equality string
*
* @return string
* @deprecated use $this->equality
*/
public function getEquality()
{
return $this->equality;
}
/**
* Gets whether this attribute is collective.
*
* @return boolean Returns TRUE if this attribute is collective and FALSE otherwise.
* @deprecated use $this->is_collective
*/
public function getIsCollective(): bool
{
return $this->is_collective;
}
/**
* Gets whether this attribute is not modifiable by users.
*
* @return boolean Returns TRUE if this attribute is not modifiable by users.
* @deprecated use $this->is_no_user_modification
*/
public function getIsNoUserModification(): bool
{
return $this->is_no_user_modification;
}
/**
* Gets whether this attribute is single-valued. If this attribute only supports single values, TRUE
* is returned. If this attribute supports multiple values, FALSE is returned.
*
* @return boolean Returns TRUE if this attribute is single-valued or FALSE otherwise.
* @deprecated use class->is_single_value
*/
public function getIsSingleValue(): bool
{
return $this->is_single_value;
}
/**
* Gets this attribute's the maximum length. If no maximum is defined by the LDAP server, NULL is returned.
*
* @return int The maximum length (in characters) of this attribute or NULL if no maximum is specified.
* @deprecated use $this->max_length;
*/
public function getMaxLength()
{
return $this->max_length;
}
/**
* Gets this attribute's ordering specification.
*
* @return string
* @deprecated use $this->ordering
*/
public function getOrdering(): string
{
return $this->ordering;
}
/**
* Gets this attribute's substring matching specification
*
* @return string
* @deprecated use $this->sub_str_rule;
*/
public function getSubstr() {
return $this->sub_str_rule;
}
/**
* Gets this attribute's parent attribute (if any). If this attribute does not
* inherit from another attribute, NULL is returned.
*
* @return string
* @deprecated use $class->sup_attribute directly
*/
public function getSupAttribute() {
return $this->sup_attribute;
}
/**
* Gets this attribute's syntax OID. Differs from getSyntaxString() in that this
* function only returns the actual OID with any length specification removed.
* Ie, if the syntax string is "1.2.3.4{16}", this function only retruns
* "1.2.3.4".
*
* @return string The syntax OID string.
* @deprecated use $this->syntax_oid;
*/
public function getSyntaxOID()
{
return $this->syntax_oid;
}
/**
* Gets this attribute's usage string as defined by the LDAP server
*
* @return string
* @deprecated use $this->usage
*/
public function getUsage()
{
return $this->usage;
}
/**
* Gets the list of "used in" objectClasses, that is the list of objectClasses
* which provide this attribute.
*
* @return Collection An array of names of objectclasses (strings) which provide this attribute
* @deprecated use $this->used_in_object_classes
*/
public function getUsedInObjectClasses(): Collection
{
return $this->used_in_object_classes;
} }
/** /**
@ -492,58 +129,162 @@ final class AttributeType extends Base {
* @param Collection $ocs * @param Collection $ocs
* @return Collection * @return Collection
*/ */
public function heirachy(Collection $ocs): Collection private function heirachy(Collection $ocs): Collection
{ {
$result = collect(); $result = collect();
foreach ($ocs as $oc) { foreach ($ocs as $oc) {
$schema = config('server') $item = config('server')
->schema('objectclasses',$oc) ->schema('objectclasses',$oc);
->getParents(TRUE)
->pluck('name');
$result = $result->merge($schema)->push($oc); $result = $result
->merge($item
->getParents(TRUE)
->pluck('oid'))
->push($item->oid);
} }
return $result; return $result;
} }
/** /**
* @return bool * Creates a new AttributeType object from a raw LDAP AttributeType string.
* @deprecated use $this->forced_as_may
*/
public function isForceMay(): bool
{
return $this->forced_as_may;
}
/**
* Removes an attribute name from this attribute's alias array.
* *
* @param string $alias The name of the attribute to remove. * eg: ( 2.5.4.0 NAME 'objectClass' DESC 'RFC4512: object classes of the entity' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )
*/ */
public function removeAlias(string $alias): void protected function parse(string $line): void
{ {
if (($x=$this->aliases->search($alias)) !== FALSE) Log::debug(sprintf('%s:Parsing AttributeType [%s]',self::LOGKEY,$line));
$this->aliases->forget($x);
// Init
$this->names = collect();
$this->children = collect();
$this->used_in_object_classes = collect();
$this->required_by_object_classes = collect();
parent::parse($line);
} }
/** protected function parse_chunk(array $strings,int &$i): void
* Sets this attribute's list of aliases.
*
* @param Collection $aliases The array of alias names (strings)
* @deprecated use $this->aliases =
*/
public function setAliases(Collection $aliases): void
{ {
$this->aliases = $aliases; switch ($strings[$i]) {
case 'NAME':
$name = '';
// @note Some schema's return a (' instead of a ( '
// @note This attribute format has no aliases
if ($strings[$i+1] !== '(' && ! preg_match('/^\(/',$strings[$i+1])) {
do {
$name .= ($name ? ' ' : '').$strings[++$i];
} while (! preg_match("/\'$/s",$strings[$i]));
} else {
$i++;
do {
// In case we came here because of a ('
if (preg_match('/^\(/',$strings[$i]))
$strings[$i] = preg_replace('/^\(/','',$strings[$i]);
else
$i++;
$name .= ($name ? ' ' : '').$strings[++$i];
} while (! preg_match("/\'$/s",$strings[$i]));
// Add alias names for this attribute
while ($strings[++$i] !== ')') {
$alias = preg_replace("/^\'(.*)\'$/",'$1',$strings[$i]);
$this->names->push($alias);
}
} }
/** $this->names = $this->names->push(preg_replace("/^\'(.*)\'$/",'$1',$name))->sort();
* This function will mark this attribute as a forced MAY attribute $this->forced_as_may = $this->names_lc
*/ ->intersect(array_map('strtolower',config('pla.force_may',[])))
public function setForceMay() { ->count() > 0;
$this->forced_as_may = TRUE;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case NAME returned (%s)',self::LOGKEY,$this->name),['names'=>$this->names]);
break;
case 'SUP':
$this->sup_attribute = preg_replace("/^\'(.*)\'$/",'$1',$strings[++$i]);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case SUP returned (%s)',self::LOGKEY,$this->sup_attribute));
break;
case 'EQUALITY':
$this->equality = $strings[++$i];
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case EQUALITY returned (%s)',self::LOGKEY,$this->equality));
break;
case 'ORDERING':
$this->ordering = $strings[++$i];
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case ORDERING returned (%s)',self::LOGKEY,$this->ordering));
break;
case 'SUBSTR':
$this->sub_str_rule = $strings[++$i];
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case SUBSTR returned (%s)',self::LOGKEY,$this->sub_str_rule));
break;
case 'SYNTAX':
$this->syntax = preg_replace("/^\'(.*)\'$/",'$1',$strings[++$i]);
$this->syntax_oid = preg_replace("/^\'?(.*){\d+}\'?$/",'$1',$this->syntax);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:/ Evaluating SYNTAX returned (%s) [%s]',self::LOGKEY,$this->syntax,$this->syntax_oid));
// Does this SYNTAX string specify a max length (ie, 1.2.3.4{16})
$m = [];
$this->max_length = preg_match('/{(\d+)}$/',$this->syntax,$m)
? $m[1]
: NULL;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case SYNTAX returned (%s) [%s] {%d}',self::LOGKEY,$this->syntax,$this->syntax_oid,$this->max_length));
break;
case 'SINGLE-VALUE':
$this->is_single_value = TRUE;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case SINGLE-VALUE returned (%s)',self::LOGKEY,$this->is_single_value));
break;
case 'COLLECTIVE':
$this->is_collective = TRUE;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case COLLECTIVE returned (%s)',self::LOGKEY,$this->is_collective));
break;
case 'NO-USER-MODIFICATION':
$this->is_no_user_modification = TRUE;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case NO-USER-MODIFICATION returned (%s)',self::LOGKEY,$this->is_no_user_modification));
break;
case 'USAGE':
$this->usage = $strings[++$i];
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case USAGE returned (%s)',self::LOGKEY,$this->usage));
break;
default:
parent::parse_chunk($strings,$i);
}
} }
/** /**
@ -557,13 +298,28 @@ final class AttributeType extends Base {
} }
/** /**
* Sets this attribute's SUP attribute (ie, the attribute from which this attribute inherits). * If this is a MUST attribute to the objectclass that defines it
* *
* @param string $attr The name of the new parent (SUP) attribute * @return void
*/ */
public function setSupAttribute(string $attr): void public function setMust(): void
{ {
$this->sup_attribute = trim($attr); $this->is_must = TRUE;
}
/**
* Sets this attribute's name.
*
* @param string $name The new name to give this attribute.
* @throws InvalidUsage
*/
public function setName(string $name): void
{
// Quick validation
if ($this->names_lc->count() && (! $this->names_lc->contains(strtolower($name))))
throw new InvalidUsage(sprintf('Cannot set attribute name to [%s], its not an alias for [%s]',$name,$this->names->join(',')));
$this->name = $name;
} }
/** /**

View File

@ -57,7 +57,7 @@ abstract class Base
public function __toString(): string public function __toString(): string
{ {
return $this->name; return $this->oid;
} }
protected function parse(string $line): void protected function parse(string $line): void

View File

@ -6,74 +6,49 @@ use Illuminate\Support\Facades\Log;
/** /**
* Represents an LDAP Syntax * Represents an LDAP Syntax
*
* @package phpLDAPadmin
* @subpackage Schema
*/ */
final class LDAPSyntax extends Base { final class LDAPSyntax extends Base
{
private const LOGKEY = 'SLS';
// Is human readable? // Is human readable?
private ?bool $is_not_human_readable = NULL; private(set) ?bool $is_not_human_readable = NULL;
// Binary transfer required? // Binary transfer required?
private ?bool $binary_transfer_required = NULL; private(set) ?bool $binary_transfer_required = NULL;
/** /**
* Creates a new Syntax object from a raw LDAP syntax string. * Creates a new Syntax object from a raw LDAP syntax string.
*/ */
public function __construct(string $line) { protected function parse(string $line): void
Log::debug(sprintf('Parsing LDAPSyntax [%s]',$line)); {
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:Parsing LDAPSyntax [%s]',self::LOGKEY,$line));
parent::__construct($line); parent::parse($line);
}
$strings = preg_split('/[\s,]+/',$line,-1,PREG_SPLIT_DELIM_CAPTURE);
protected function parse_chunk(array $strings,int &$i): void
{
for ($i=0; $i<count($strings); $i++) { for ($i=0; $i<count($strings); $i++) {
switch($strings[$i]) { switch($strings[$i]) {
case '(':
case ')':
break;
case 'DESC':
do {
$this->description .= (strlen($this->description) ? ' ' : '').$strings[++$i];
} while (! preg_match("/\'$/s",$strings[$i]));
$this->description = preg_replace("/^\'(.*)\'$/",'$1',$this->description);
Log::debug(sprintf('- Case DESC returned (%s)',$this->description));
break;
case 'X-BINARY-TRANSFER-REQUIRED': case 'X-BINARY-TRANSFER-REQUIRED':
$this->binary_transfer_required = (str_replace("'",'',$strings[++$i]) === 'TRUE'); $this->binary_transfer_required = (str_replace("'",'',$strings[++$i]) === 'TRUE');
Log::debug(sprintf('- Case X-BINARY-TRANSFER-REQUIRED returned (%s)',$this->binary_transfer_required)); if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case X-BINARY-TRANSFER-REQUIRED returned (%s)',self::LOGKEY,$this->binary_transfer_required));
break; break;
case 'X-NOT-HUMAN-READABLE': case 'X-NOT-HUMAN-READABLE':
$this->is_not_human_readable = (str_replace("'",'',$strings[++$i]) === 'TRUE'); $this->is_not_human_readable = (str_replace("'",'',$strings[++$i]) === 'TRUE');
Log::debug(sprintf('- Case X-NOT-HUMAN-READABLE returned (%s)',$this->is_not_human_readable)); if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case X-NOT-HUMAN-READABLE returned (%s)',self::LOGKEY,$this->is_not_human_readable));
break; break;
default: default:
if (preg_match('/[\d\.]+/i',$strings[$i]) && ($i === 1)) { parent::parse_chunk($strings,$i);
$this->oid = $strings[$i];
Log::debug(sprintf('- Case default returned (%s)',$this->oid));
} elseif ($strings[$i])
Log::alert(sprintf('! Case default discovered a value NOT parsed (%s)',$strings[$i]),['line'=>$line]);
} }
} }
} }
public function __get(string $key): mixed
{
switch ($key) {
case 'binary_transfer_required': return $this->binary_transfer_required;
case 'is_not_human_readable': return $this->is_not_human_readable;
default: return parent::__get($key);
}
}
} }

View File

@ -7,106 +7,16 @@ use Illuminate\Support\Facades\Log;
/** /**
* Represents an LDAP MatchingRule * Represents an LDAP MatchingRule
*
* @package phpLDAPadmin
* @subpackage Schema
*/ */
final class MatchingRule extends Base { final class MatchingRule extends Base
{
private const LOGKEY = 'SMR';
// This rule's syntax OID // This rule's syntax OID
private ?string $syntax = NULL; private(set) ?string $syntax = NULL;
// An array of attribute names who use this MatchingRule // An array of attribute names who use this MatchingRule
private Collection $used_by_attrs; private(set) Collection $used_by_attrs;
/**
* Creates a new MatchingRule object from a raw LDAP MatchingRule string.
*/
function __construct(string $line) {
Log::debug(sprintf('Parsing MatchingRule [%s]',$line));
parent::__construct($line);
$strings = preg_split('/[\s,]+/',$line,-1,PREG_SPLIT_DELIM_CAPTURE);
// Init
$this->used_by_attrs = collect();
for ($i=0; $i<count($strings); $i++) {
switch ($strings[$i]) {
case '(':
case ')':
break;
case 'NAME':
if ($strings[$i+1] != '(') {
do {
$this->name .= (strlen($this->name) ? ' ' : '').$strings[++$i];
} while (! preg_match("/\'$/s",$strings[$i]));
} else {
$i++;
do {
$this->name .= (strlen($this->name) ? ' ' : '').$strings[++$i];
} while (! preg_match("/\'$/s",$strings[$i]));
do {
$i++;
} while (! preg_match('/\)+\)?/',$strings[$i]));
}
$this->name = preg_replace("/^\'/",'',$this->name);
$this->name = preg_replace("/\'$/",'',$this->name);
Log::debug(sprintf(sprintf('- Case NAME returned (%s)',$this->name)));
break;
case 'DESC':
do {
$this->description .= (strlen($this->description) ? ' ' : '').$strings[++$i];
} while (! preg_match("/\'$/s",$strings[$i]));
$this->description = preg_replace("/^\'(.*)\'$/",'$1',$this->description);
Log::debug(sprintf('- Case DESC returned (%s)',$this->description));
break;
case 'OBSOLETE':
$this->is_obsolete = TRUE;
Log::debug(sprintf('- Case OBSOLETE returned (%s)',$this->is_obsolete));
break;
case 'SYNTAX':
$this->syntax = $strings[++$i];
Log::debug(sprintf('- Case SYNTAX returned (%s)',$this->syntax));
break;
default:
if (preg_match('/[\d\.]+/i',$strings[$i]) && ($i === 1)) {
$this->oid = $strings[$i];
Log::debug(sprintf('- Case default returned (%s)',$this->oid));
} elseif ($strings[$i])
Log::alert(sprintf('! Case default discovered a value NOT parsed (%s)',$strings[$i]),['line'=>$line]);
}
}
}
public function __get(string $key): mixed
{
switch ($key) {
case 'syntax': return $this->syntax;
case 'used_by_attrs': return $this->used_by_attrs;
default: return parent::__get($key);
}
}
/** /**
* Adds an attribute name to the list of attributes who use this MatchingRule * Adds an attribute name to the list of attributes who use this MatchingRule
@ -120,23 +30,33 @@ final class MatchingRule extends Base {
} }
/** /**
* Gets an array of attribute names (strings) which use this MatchingRule * Creates a new MatchingRule object from a raw LDAP MatchingRule string.
* *
* @return array The array of attribute names (strings). * @param string $line
* @deprecated use $this->used_by_attrs * @return void
*/ */
public function getUsedByAttrs() protected function parse(string $line): void
{ {
return $this->used_by_attrs; if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:Parsing MatchingRule [%s]',self::LOGKEY,$line));
// Init
$this->used_by_attrs = collect();
parent::parse($line);
} }
/** protected function parse_chunk(array $strings,int &$i): void
* Sets the list of used_by_attrs to the array specified by $attrs;
*
* @param Collection $attrs The array of attribute names (strings) which use this MatchingRule
*/
public function setUsedByAttrs(Collection $attrs): void
{ {
$this->used_by_attrs = $attrs; switch ($strings[$i]) {
case 'SYNTAX':
$this->syntax = $strings[++$i];
Log::debug(sprintf('- Case SYNTAX returned (%s)',$this->syntax));
break;
default:
parent::parse_chunk($strings,$i);
}
} }
} }

View File

@ -10,9 +10,6 @@ use App\Exceptions\InvalidUsage;
/** /**
* Represents an LDAP Schema objectClass * Represents an LDAP Schema objectClass
*
* @package phpLDAPadmin
* @subpackage Schema
*/ */
final class ObjectClass extends Base final class ObjectClass extends Base
{ {
@ -27,21 +24,14 @@ final class ObjectClass extends Base
// One of STRUCTURAL, ABSTRACT, or AUXILIARY // One of STRUCTURAL, ABSTRACT, or AUXILIARY
private int $type; private int $type;
// Arrays of attribute names that this objectClass requires // Attributes that this objectclass defines
private Collection $must_attrs; private(set) Collection $attributes;
// Arrays of attribute names that this objectClass allows, but does not require
private Collection $may_attrs;
// Arrays of attribute names that this objectClass has been forced to MAY attrs, due to configuration
private(set) Collection $may_force;
private bool $is_obsolete;
public function __get(string $key): mixed public function __get(string $key): mixed
{ {
return match ($key) { return match ($key) {
'attributes' => $this->getAllAttrs(TRUE), 'all_attributes' => $this->getMustAttrs(TRUE)
->merge($this->getMayAttrs(TRUE)),
'type_name' => match ($this->type) { 'type_name' => match ($this->type) {
Server::OC_STRUCTURAL => 'Structural', Server::OC_STRUCTURAL => 'Structural',
Server::OC_ABSTRACT => 'Abstract', Server::OC_ABSTRACT => 'Abstract',
@ -52,20 +42,6 @@ final class ObjectClass extends Base
}; };
} }
/**
* Return a list of attributes that this objectClass provides
*
* @param bool $parents
* @return Collection
* @throws InvalidUsage
*/
public function getAllAttrs(bool $parents=FALSE): Collection
{
return $this
->getMustAttrs($parents)
->merge($this->getMayAttrs($parents));
}
/** /**
* Adds an objectClass to the list of objectClasses that inherit * Adds an objectClass to the list of objectClasses that inherit
* from this objectClass. * from this objectClass.
@ -93,16 +69,26 @@ final class ObjectClass extends Base
*/ */
public function getMayAttrs(bool $parents=FALSE): Collection public function getMayAttrs(bool $parents=FALSE): Collection
{ {
$attrs = $this->may_attrs; $attrs = $this->attributes
->filter(fn($item)=>! $item->is_must)
->transform(function($item) {
$item->source = $this->name;
return $item;
});
if ($parents) if ($parents)
foreach ($this->getParents() as $object_class) foreach ($this->getParents() as $object_class)
$attrs = $attrs->merge($object_class->getMayAttrs($parents)); $attrs = $attrs->merge($object_class
->getMayAttrs($parents)
->transform(function($item) use ($object_class) {
$item->source = $item->source ?: $object_class->name;
return $item;
}));
// Return a sorted list // Return a sorted list
return $attrs return $attrs
->unique(fn($item)=>$item->name) ->unique(fn($item)=>$item->name)
->sortBy(fn($item)=>strtolower($item->name.$item->source)); ->sortBy(fn($item)=>$item->name);
} }
/** /**
@ -119,16 +105,26 @@ final class ObjectClass extends Base
*/ */
public function getMustAttrs(bool $parents=FALSE): Collection public function getMustAttrs(bool $parents=FALSE): Collection
{ {
$attrs = $this->must_attrs; $attrs = $this->attributes
->filter(fn($item)=>$item->is_must)
->transform(function($item) {
$item->source = $this->name;
return $item;
});
if ($parents) if ($parents)
foreach ($this->getParents() as $object_class) foreach ($this->getParents() as $object_class)
$attrs = $attrs->merge($object_class->getMustAttrs($parents)); $attrs = $attrs->merge($object_class
->getMustAttrs($parents)
->transform(function($item) use ($object_class) {
$item->source = $item->source ?: $object_class->name;
return $item;
}));
// Return a sorted list // Return a sorted list
return $attrs return $attrs
->unique(fn($item)=>$item->name) ->unique(fn($item)=>$item->name)
->sortBy(fn($item)=>strtolower($item->name.$item->source)); ->sortBy(fn($item)=>$item->name);
} }
/** /**
@ -165,16 +161,6 @@ final class ObjectClass extends Base
return $this->type === Server::OC_AUXILIARY; return $this->type === Server::OC_AUXILIARY;
} }
/**
* Determine if an array is listed in the may_force attrs
*/
public function isForceMay(string $attr): bool
{
return $this->may_force
->pluck('name')
->contains($attr);
}
public function isStructural(): bool public function isStructural(): bool
{ {
return $this->type === Server::OC_STRUCTURAL; return $this->type === Server::OC_STRUCTURAL;
@ -192,9 +178,7 @@ final class ObjectClass extends Base
Log::debug(sprintf('%s:Parsing ObjectClass [%s]',self::LOGKEY,$line)); Log::debug(sprintf('%s:Parsing ObjectClass [%s]',self::LOGKEY,$line));
// Init // Init
$this->may_attrs = collect(); $this->attributes = collect();
$this->may_force = collect();
$this->must_attrs = collect();
$this->sup_classes = collect(); $this->sup_classes = collect();
$this->child_classes = collect(); $this->child_classes = collect();
@ -204,34 +188,8 @@ final class ObjectClass extends Base
protected function parse_chunk(array $strings,int &$i): void protected function parse_chunk(array $strings,int &$i): void
{ {
switch ($strings[$i]) { switch ($strings[$i]) {
case 'NAME':
if ($strings[$i+1] != '(') {
do {
$this->name .= (strlen($this->name) ? ' ' : '').$strings[++$i];
} while (! preg_match('/\'$/s',$strings[$i]));
} else {
$i++;
do {
$this->name .= (strlen($this->name) ? ' ' : '').$strings[++$i];
} while (! preg_match('/\'$/s',$strings[$i]));
do {
$i++;
} while (! preg_match('/\)+\)?/',$strings[$i]));
}
$this->name = preg_replace("/^\'(.*)\'$/",'$1',$this->name);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case NAME returned (%s)',self::LOGKEY,$this->name));
break;
case 'SUP': case 'SUP':
if ($strings[$i+1] != '(') { if ($strings[$i+1] !== '(') {
$this->sup_classes->push(preg_replace("/'/",'',$strings[++$i])); $this->sup_classes->push(preg_replace("/'/",'',$strings[++$i]));
} else { } else {
@ -240,7 +198,7 @@ final class ObjectClass extends Base
do { do {
$i++; $i++;
if ($strings[$i] != '$') if ($strings[$i] !== '$')
$this->sup_classes->push(preg_replace("/'/",'',$strings[$i])); $this->sup_classes->push(preg_replace("/'/",'',$strings[$i]));
} while (! preg_match('/\)+\)?/',$strings[$i+1])); } while (! preg_match('/\)+\)?/',$strings[$i+1]));
@ -276,23 +234,17 @@ final class ObjectClass extends Base
$i = $this->parseList(++$i,$strings,$attrs); $i = $this->parseList(++$i,$strings,$attrs);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:= parseList returned %d (%s)',self::LOGKEY,$i,$attrs->join(',')));
foreach ($attrs as $string) { foreach ($attrs as $string) {
$attr = new ObjectClassAttribute($string,$this->name); $attr = clone config('server')->schema('attributetypes',$string);
if (config('server')->isForceMay($attr->getName())) { if (! $attr->forced_as_may)
$this->may_force->push($attr); $attr->setMust();
$this->may_attrs->push($attr);
} else $this->attributes->push($attr);
$attr->required = TRUE;
$this->must_attrs->push($attr);
} }
if (static::DEBUG_VERBOSE) if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case MUST returned (%s) (%s)',self::LOGKEY,$this->must_attrs->join(','),$this->may_force->join(','))); Log::debug(sprintf('%s:- Case MUST returned (%s) (%s)',self::LOGKEY,$attrs->join(','),$this->forced_as_may ? 'FORCED MAY' : 'MUST'));
break; break;
case 'MAY': case 'MAY':
@ -300,16 +252,11 @@ final class ObjectClass extends Base
$i = $this->parseList(++$i,$strings,$attrs); $i = $this->parseList(++$i,$strings,$attrs);
if (static::DEBUG_VERBOSE) foreach ($attrs as $string)
Log::debug(sprintf('%s:- parseList returned %d (%s)',self::LOGKEY,$i,$attrs->join(','))); $this->attributes->push(config('server')->schema('attributetypes',$string));
foreach ($attrs as $string) {
$attr = new ObjectClassAttribute($string,$this->name);
$this->may_attrs->push($attr);
}
if (static::DEBUG_VERBOSE) if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case MAY returned (%s)',self::LOGKEY,$this->may_attrs->join(','))); Log::debug(sprintf('%s:- Case MAY returned (%s)',self::LOGKEY,$attrs->join(',')));
break; break;
default: default:

View File

@ -1,40 +0,0 @@
<?php
namespace App\Classes\LDAP\Schema;
/**
* A simple class for representing AttributeTypes used only by the ObjectClass class.
*
* Users should never instantiate this class. It represents an attribute internal to
* an ObjectClass. If PHP supported inner-classes and variable permissions, this would
* be interior to class ObjectClass and flagged private. The reason this class is used
* and not the "real" class AttributeType is because this class supports the notion of
* a "source" objectClass, meaning that it keeps track of which objectClass originally
* specified it. This class is therefore used by the class ObjectClass to determine
* inheritance.
*/
final class ObjectClassAttribute extends Base {
// This Attribute's root.
private string $source;
public bool $required = FALSE;
/**
* Creates a new ObjectClassAttribute with specified name and source objectClass.
*
* @param string $name the name of the new attribute.
* @param string $source the name of the ObjectClass which specifies this attribute.
*/
public function __construct($name,$source)
{
$this->name = $name;
$this->source = $source;
}
public function __get(string $key): mixed
{
return match ($key) {
'source' => $this->source,
default => parent::__get($key),
};
}
}

View File

@ -50,10 +50,6 @@ final class Server
public function __get(string $key): mixed public function __get(string $key): mixed
{ {
return match($key) { return match($key) {
'attributetypes' => $this->attributetypes,
'ldapsyntaxes' => $this->ldapsyntaxes,
'matchingrules' => $this->matchingrules,
'objectclasses' => $this->objectclasses,
'config' => config(sprintf('ldap.connections.%s',config('ldap.default'))), 'config' => config(sprintf('ldap.connections.%s',config('ldap.default'))),
'name' => Arr::get($this->config,'name',__('No Server Name Yet')), 'name' => Arr::get($this->config,'name',__('No Server Name Yet')),
default => throw new Exception('Unknown key:'.$key), default => throw new Exception('Unknown key:'.$key),
@ -73,7 +69,7 @@ final class Server
public static function baseDNs(bool $objects=TRUE): Collection public static function baseDNs(bool $objects=TRUE): Collection
{ {
try { try {
$rootdse = self::rootDSE(); $namingcontexts = collect(config('pla.base_dns') ?: self::rootDSE()?->namingcontexts);
/** /**
* LDAP Error Codes: * LDAP Error Codes:
@ -179,13 +175,13 @@ final class Server
} }
if (! $objects) if (! $objects)
return collect($rootdse->namingcontexts ?: []); return $namingcontexts;
return Cache::remember('basedns'.Session::id(),config('ldap.cache.time'),function() use ($rootdse) { return Cache::remember('basedns'.Session::id(),config('ldap.cache.time'),function() use ($namingcontexts) {
$result = collect(); $result = collect();
// @note: Incase our rootDSE didnt return a namingcontext, we'll have no base DNs // @note: Incase our rootDSE didnt return a namingcontext, we'll have no base DNs
foreach (($rootdse->namingcontexts ?: []) as $dn) foreach ($namingcontexts as $dn)
$result->push(self::get($dn)->read()->find($dn)); $result->push(self::get($dn)->read()->find($dn));
return $result->filter()->sort(fn($item)=>$item->sort_key); return $result->filter()->sort(fn($item)=>$item->sort_key);
@ -298,15 +294,37 @@ final class Server
} }
/** /**
* This function determines if the specified attribute is contained in the force_may list * Get an attribute key for an attributetype name
* as configured in config.php.
* *
* @return boolean True if the specified attribute is configured to be force as a may attribute * @param string $key
* @todo There are 3 isForceMay() functions - we only need one * @return int|bool
* @throws InvalidUsage
*/ */
public function isForceMay($attr_name): bool public function get_attr_id(string $key): int|bool
{ {
return in_array($attr_name,config('pla.force_may',[])); static $attributes = $this->schema('attributetypes');
$attrid = $attributes->search(fn($item)=>$item->names->contains($key));
// Second chance search using lowercase items (our Entry attribute keys are lowercase)
if ($attrid === FALSE)
$attrid = $attributes->search(fn($item)=>$item->names_lc->contains(strtolower($key)));
return $attrid;
}
/**
* Given an OID, return the ldapsyntax for the OID
*
* @param string $oid
* @return LDAPSyntax|null
* @throws InvalidUsage
*/
public function get_syntax(string $oid): ?LDAPSyntax
{
return (($id=$this->schema('ldapsyntaxes')->search(fn($item)=>$item->oid === $oid)) !== FALSE)
? $this->ldapsyntaxes[$id]
: NULL;
} }
/** /**
@ -327,31 +345,15 @@ final class Server
* @param string $item Schema Item to Fetch * @param string $item Schema Item to Fetch
* @param string|null $key * @param string|null $key
* @return Collection|LDAPSyntax|Base|NULL * @return Collection|LDAPSyntax|Base|NULL
* @throws InvalidUsage
*/ */
public function schema(string $item,?string $key=NULL): Collection|LDAPSyntax|Base|NULL public function schema(string $item,?string $key=NULL): Collection|LDAPSyntax|Base|NULL
{ {
// Ensure our item to fetch is lower case // Ensure our item to fetch is lower case
$item = strtolower($item); $item = strtolower($item);
if ($key)
$key = strtolower($key);
$result = Cache::remember('schema'.$item,config('ldap.cache.time'),function() use ($item) {
// First pass if we have already retrieved the schema item
switch ($item) {
case 'attributetypes':
case 'ldapsyntaxes':
case 'matchingrules':
case 'objectclasses':
if ($this->{$item}->count())
return $this->{$item};
break;
// This error message is not localized as only developers should ever see it
default:
throw new InvalidUsage('Invalid request to fetch schema: '.$item);
}
if (! $this->{$item}->count()) {
$this->{$item} = Cache::remember('schema.'.$item,config('ldap.cache.time'),function() use ($item) {
// Try to get the schema DN from the specified entry. // Try to get the schema DN from the specified entry.
$schema_dn = $this->schemaDN(); $schema_dn = $this->schemaDN();
// @note: 389DS does not return subschemaSubentry unless it is requested // @note: 389DS does not return subschemaSubentry unless it is requested
@ -372,68 +374,29 @@ final class Server
continue; continue;
$o = new AttributeType($line); $o = new AttributeType($line);
$this->attributetypes->put($o->name_lc,$o); $this->attributetypes->push($o);
} }
// go back and add data from aliased attributeTypes
foreach ($this->attributetypes as $o) { foreach ($this->attributetypes as $o) {
/* foreach of the attribute's aliases, create a new entry in the attrs array
* with its name set to the alias name, and all other data copied.*/
if ($o->aliases->count()) {
Log::debug(sprintf('%s:\ Attribute [%s] has the following aliases [%s]',self::LOGKEY,$o->name,$o->aliases->join(',')));
foreach ($o->aliases as $alias) {
$new_attr = clone $o;
$new_attr->setName($alias);
$new_attr->addAlias($o->name);
$new_attr->removeAlias($alias);
$this->attributetypes->put(strtolower($alias),$new_attr);
}
}
}
// Now go through and reference the parent/child relationships // Now go through and reference the parent/child relationships
foreach ($this->attributetypes as $o)
if ($o->sup_attribute) { if ($o->sup_attribute) {
$parent = strtolower($o->sup_attribute); $attrid = $this->get_attr_id($o->sup_attribute);
if ($this->attributetypes->has($parent) !== FALSE) if (! $this->attributetypes[$attrid]->children->contains($o->oid))
$this->attributetypes[$parent]->addChild($o->name); $this->attributetypes[$attrid]->addChild($o->oid);
} }
// go through any children and add details if the child doesnt have them (ie, cn inherits name) // go through any children and add details if the child doesnt have them (ie, cn inherits name)
// @todo This doesnt traverse children properly, so children of children may not get the settings they should foreach ($o->children as $child) {
foreach ($this->attributetypes as $parent) { $attrid = $this->attributetypes->search(fn($o)=>$o->oid === $child);
foreach ($parent->children as $child) {
$child = strtolower($child);
/* only overwrite the child's SINGLE-VALUE property if the parent has it set, and the child doesnt /* only overwrite the child's SINGLE-VALUE property if the parent has it set, and the child doesnt
* (note: All LDAP attributes default to multi-value if not explicitly set SINGLE-VALUE) */ * (note: All LDAP attributes default to multi-value if not explicitly set SINGLE-VALUE) */
if (! is_null($parent->is_single_value) && is_null($this->attributetypes[$child]->is_single_value)) if (! is_null($o->is_single_value) && is_null($this->attributetypes[$attrid]->is_single_value))
$this->attributetypes[$child]->setIsSingleValue($parent->is_single_value); $this->attributetypes[$attrid]->setIsSingleValue($o->is_single_value);
} }
} }
// Add the used in and required_by values.
foreach ($this->schema('objectclasses') as $object_class) {
// Add Used In.
foreach ($object_class->getAllAttrs()->pluck('name') as $attr_name)
if ($this->attributetypes->has(strtolower($attr_name)))
$this->attributetypes[strtolower($attr_name)]->addUsedInObjectClass($object_class->name,$object_class->isStructural());
// Add Required By.
foreach ($object_class->getMustAttrs()->pluck('name') as $attr_name)
if ($this->attributetypes->has(strtolower($attr_name)))
$this->attributetypes[strtolower($attr_name)]->addRequiredByObjectClass($object_class->name,$object_class->isStructural());
// Force May
foreach ($object_class->may_force as $attr_name)
if ($this->attributetypes->has(strtolower($attr_name->name)))
$this->attributetypes[strtolower($attr_name->name)]->setForceMay();
}
return $this->attributetypes; return $this->attributetypes;
case 'ldapsyntaxes': case 'ldapsyntaxes':
@ -444,7 +407,7 @@ final class Server
continue; continue;
$o = new LDAPSyntax($line); $o = new LDAPSyntax($line);
$this->ldapsyntaxes->put(strtolower($o->oid),$o); $this->ldapsyntaxes->push($o);
} }
return $this->ldapsyntaxes; return $this->ldapsyntaxes;
@ -457,14 +420,14 @@ final class Server
continue; continue;
$o = new MatchingRule($line); $o = new MatchingRule($line);
$this->matchingrules->put($o->name_lc,$o); $this->matchingrules->push($o);
} }
foreach ($this->schema('attributetypes') as $attr) { foreach ($this->schema('attributetypes') as $attr) {
$rule_key = strtolower($attr->getEquality()); $rule_id = $this->matchingrules->search(fn($item)=>$item->oid === $attr->equality);
if ($this->matchingrules->has($rule_key) !== FALSE) if ($rule_id !== FALSE)
$this->matchingrules[$rule_key]->addUsedByAttr($attr->name); $this->matchingrules[$rule_id]->addUsedByAttr($attr->name);
} }
return $this->matchingrules; return $this->matchingrules;
@ -476,19 +439,35 @@ final class Server
if (is_null($line) || ! strlen($line)) if (is_null($line) || ! strlen($line))
continue; continue;
$o = new ObjectClass($line,$this); $o = new ObjectClass($line);
$this->objectclasses->put($o->name_lc,$o); $this->objectclasses->push($o);
} }
foreach ($this->objectclasses as $o) {
// Now go through and reference the parent/child relationships // Now go through and reference the parent/child relationships
foreach ($this->objectclasses as $o) foreach ($o->sup_classes as $sup) {
foreach ($o->sup_classes as $parent) { $oc_id = $this->objectclasses->search(fn($item)=>$item->name === $sup);
$parent = strtolower($parent);
if (! $this->objectclasses->contains($parent)) if (($oc_id !== FALSE) && (! $this->objectclasses[$oc_id]->child_classes->contains($o->name)))
$this->objectclasses[$parent]->addChildObjectClass($o->name); $this->objectclasses[$oc_id]->addChildObjectClass($o->name);
} }
// Add the used in and required_by values for attributes.
foreach ($o->attributes as $attribute) {
if (($attrid = $this->schema('attributetypes')->search(fn($item)=>$item->oid === $attribute->oid)) !== FALSE) {
// Add Used In.
$this->attributetypes[$attrid]->addUsedInObjectClass($o->oid,$o->isStructural());
// Add Required By.
if ($attribute->is_must)
$this->attributetypes[$attrid]->addRequiredByObjectClass($o->oid,$o->isStructural());
}
}
}
// Put the updated attributetypes back in the cache
Cache::put('schema.attributetypes',$this->attributetypes,config('ldap.cache.time'));
return $this->objectclasses; return $this->objectclasses;
// Shouldnt get here // Shouldnt get here
@ -496,8 +475,27 @@ final class Server
throw new InvalidUsage('Invalid request to fetch schema: '.$item); throw new InvalidUsage('Invalid request to fetch schema: '.$item);
} }
}); });
}
return is_null($key) ? $result : $result->get($key); if (is_null($key))
return $this->{$item};
switch ($item) {
case 'attributetypes':
$attrid = $this->get_attr_id($key);
$attr = ($attrid === FALSE)
? new AttributeType($key)
: clone $this->{$item}->get($attrid);
$attr->setName($attr->names->get($attr->names_lc->search(strtolower($key))) ?: $key);
return $attr;
default:
return $this->{$item}->get($key)
?: $this->{$item}->first(fn($item)=>$item->name_lc === strtolower($key));
}
} }
/** /**
@ -510,15 +508,4 @@ final class Server
{ {
return Arr::get($this->rootDSE->subschemasubentry,0); return Arr::get($this->rootDSE->subschemasubentry,0);
} }
/**
* Given an OID, return the ldapsyntax for the OID
*
* @param string $oid
* @return LDAPSyntax|null
*/
public function schemaSyntaxName(string $oid): ?LDAPSyntax
{
return $this->schema('ldapsyntaxes',$oid);
}
} }

View File

@ -4,6 +4,7 @@ namespace App\Classes;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class Template class Template
{ {
@ -19,6 +20,8 @@ class Template
$this->file = $file; $this->file = $file;
try { 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 = json_decode($td->get($file),null,512,JSON_OBJECT_AS_ARRAY|JSON_THROW_ON_ERROR);
} catch (\JsonException $e) { } catch (\JsonException $e) {
@ -30,10 +33,12 @@ class Template
public function __get(string $key): mixed public function __get(string $key): mixed
{ {
return match ($key) { return match ($key) {
'attributes' => collect(array_map('strtolower',array_keys(Arr::get($this->template,$key)))), 'attributes' => collect(Arr::get($this->template,$key))->keys(),
'objectclasses' => collect(array_map('strtolower',Arr::get($this->template,$key))),
'enabled' => Arr::get($this->template,$key,FALSE) && (! $this->invalid), 'enabled' => Arr::get($this->template,$key,FALSE) && (! $this->invalid),
'icon','regexp','title' => Arr::get($this->template,$key), 'icon','regexp','title' => Arr::get($this->template,$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']),
default => throw new \Exception('Unknown key: '.$key), default => throw new \Exception('Unknown key: '.$key),
}; };
@ -44,8 +49,159 @@ class Template
return array_key_exists($key,$this->template); return array_key_exists($key,$this->template);
} }
public function __toString(): string private function autofill()
{ {
return $this->invalid ? '' : Arr::get($this->template,'title','No Template Name'); /*
autoFill:string
string is a literal string, and may contain many fields like %attr|start-end/flags|additionalcontrolchar%
to substitute values read from other fields.
|start-end is optional, but must be present if the k flag is used.
/flags is optional.
|additionalcontrolchar is optional.
flags may be:
T: Read display text from selection item (drop-down list), otherwise, read the value of the field
For fields that aren't selection items, /T shouldn't be used, and the field value will always be read.
k: Tokenize:
If the "k" flag is not given:
A |start-end instruction will perform a sub-string operation upon
the value of the attr, passing character positions start-end through.
start can be 0 for first character, or any other integer.
end can be 0 for last character, or any other integer for a specific position.
If the "k" flag is given:
The string read will be split into fields, using : as a delimiter
"start" indicates which field number to pass through.
K: The string read will be split into fields, using ' ' as a delimiter "start" indicates which field number to pass through.
If additionalcontrolchar is given, it will be used as delimiter (e.g. this allows for splitting e-mail addresses
into domain and domain-local part).
l: Make the result lower case.
U: Make the result upper case.
A: Remap special characters to their corresponding ASCII value
*/
if (! preg_match('/;/',$arg)) {
system_message(array(
'title'=>_('Problem with autoFill() in template'),
'body'=>sprintf('%s (<b>%s</b>)',_('There is only 1 argument, when there should be two'),$attribute->getName(false)),
'type'=>'warn'));
return;
}
list($attr,$string) = preg_split('(([^,]+);(.*))',$arg,-1,PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
preg_match_all('/%(\w+)(\|[0-9]*-[0-9]*)?(\/[KklTUA]+)?(?:\|(.))?%/U',$string,$matchall);
//print"<PRE>";print_r($matchall); //0 = highlevel match, 1 = attr, 2 = subst, 3 = mod, 4 = delimiter
if (! isset($attribute->js['autoFill']))
$attribute->js['autoFill'] = '';
$formula = $string;
$formula = preg_replace('/^([^%])/','\'$1',$formula);
$formula = preg_replace('/([^%])$/','$1\'',$formula);
# Check that our attributes match our schema attributes.
foreach ($matchall[1] as $index => $checkattr) {
$sattr = $this->getServer()->getSchemaAttribute($checkattr);
# If the attribute is the same as in the XML file, then dont need to do anything.
if (! $sattr || ! strcasecmp($sattr->getName(),$checkattr))
continue;
$formula = preg_replace("/$checkattr/",$sattr->getName(),$formula);
$matchall[1][$index] = $sattr->getName();
}
$elem_id = 0;
foreach ($matchall[0] as $index => $null) {
$match_attr = strtolower($matchall[1][$index]);
$match_subst = $matchall[2][$index];
$match_mod = $matchall[3][$index];
$match_delim = $matchall[4][$index];
$substrarray = array();
if (! isset($varcount[$match_attr]))
$varcount[$match_attr] = 0;
else
$varcount[$match_attr]++;
$js_match_attr = $match_attr;
$match_attr = $js_match_attr.'xx'.$varcount[$match_attr];
$formula = preg_replace('/%'.$js_match_attr.'([|\/%])/i','%'.$match_attr.'$1',$formula,1);
$attribute->js['autoFill'] .= sprintf(" var %s;\n",$match_attr);
$attribute->js['autoFill'] .= sprintf(
" var elem$elem_id = document.getElementById(pre+'%s'+suf);\n".
" if (!elem$elem_id) return;\n", $js_match_attr);
if (strstr($match_mod,'T')) {
$attribute->js['autoFill'] .= sprintf(" %s = elem$elem_id.options[elem$elem_id.selectedIndex].text;\n",
$match_attr);
} else {
$attribute->js['autoFill'] .= sprintf(" %s = elem$elem_id.value;\n",$match_attr);
}
$elem_id++;
if (strstr($match_mod,'k')) {
preg_match_all('/([0-9]+)/',trim($match_subst),$substrarray);
if (isset($substrarray[1][0])) {
$tok_idx = $substrarray[1][0];
} else {
$tok_idx = '0';
}
$attribute->js['autoFill'] .= sprintf(" %s = %s.split(':')[%s];\n",$match_attr,$match_attr,$tok_idx);
} elseif (strstr($match_mod,'K')) {
preg_match_all('/([0-9]+)/',trim($match_subst),$substrarray);
if (isset($substrarray[1][0])) {
$tok_idx = $substrarray[1][0];
} else {
$tok_idx = '0';
}
if ($match_delim == '') {
$delimiter = ' ';
} else {
$delimiter = preg_quote($match_delim);
}
$attribute->js['autoFill'] .= sprintf(" %s = %s.split('%s')[%s];\n",$match_attr,$match_attr,$delimiter,$tok_idx);
} else {
preg_match_all('/([0-9]*)-([0-9]*)/',trim($match_subst),$substrarray);
if ((isset($substrarray[1][0]) && $substrarray[1][0]) || (isset($substrarray[2][0]) && $substrarray[2][0])) {
$attribute->js['autoFill'] .= sprintf(" %s = %s.substr(%s,%s);\n",
$match_attr,$match_attr,
$substrarray[1][0] ? $substrarray[1][0] : '0',
$substrarray[2][0] ? $substrarray[2][0] : sprintf('%s.length',$match_attr));
}
}
if (strstr($match_mod,'l')) {
$attribute->js['autoFill'] .= sprintf(" %s = %s.toLowerCase();\n",$match_attr,$match_attr);
}
if (strstr($match_mod,'U')) {
$attribute->js['autoFill'] .= sprintf(" %s = %s.toUpperCase();\n",$match_attr,$match_attr);
}
if (strstr($match_mod,'A')) {
$attribute->js['autoFill'] .= sprintf(" %s = toAscii(%s);\n",$match_attr,$match_attr);
}
# Matchfor only entry without modifiers.
$formula = preg_replace('/^%('.$match_attr.')%$/U','$1 + \'\'',$formula);
# Matchfor only entry with modifiers.
$formula = preg_replace('/^%('.$match_attr.')(\|[0-9]*-[0-9]*)?(\/[KklTUA]+)?(?:\|(.))?%$/U','$1 + \'\'',$formula);
# Matchfor begining entry.
$formula = preg_replace('/^%('.$match_attr.')(\|[0-9]*-[0-9]*)?(\/[KklTUA]+)?(?:\|(.))?%/U','$1 + \'',$formula);
# Matchfor ending entry.
$formula = preg_replace('/%('.$match_attr.')(\|[0-9]*-[0-9]*)?(\/[KklTUA]+)?(?:\|(.))?%$/U','\' + $1 ',$formula);
# Match for entries not at begin/end.
$formula = preg_replace('/%('.$match_attr.')(\|[0-9]*-[0-9]*)?(\/[KklTUA]+)?(?:\|(.))?%/U','\' + $1 + \'',$formula);
$attribute->js['autoFill'] .= "\n";
}
$attribute->js['autoFill'] .= sprintf(" fillRec(pre+'%s'+suf, %s); // %s\n",strtolower($attr),$formula,$string);
$attribute->js['autoFill'] .= "\n";
} }
} }

View File

@ -12,6 +12,8 @@ use App\Classes\LDAP\Server;
class AjaxController extends Controller class AjaxController extends Controller
{ {
private const LOGKEY = 'CAc';
/** /**
* Get the LDAP server BASE DNs * Get the LDAP server BASE DNs
* *
@ -27,7 +29,7 @@ class AjaxController extends Controller
'lazy'=>TRUE, 'lazy'=>TRUE,
'icon'=>'fa-fw fas fa-sitemap', 'icon'=>'fa-fw fas fa-sitemap',
'tooltip'=>$item->getDn(), 'tooltip'=>$item->getDn(),
]); ])->values();
} }
/** /**
@ -42,7 +44,7 @@ class AjaxController extends Controller
if (str_starts_with($dn,'*') && ($x=strpos($dn,'|'))) if (str_starts_with($dn,'*') && ($x=strpos($dn,'|')))
$dn = substr($dn,$x+1); $dn = substr($dn,$x+1);
Log::debug(sprintf('%s: Query [%s]',__METHOD__,$dn)); Log::debug(sprintf('%s:Query [%s]',self::LOGKEY,$dn));
return (config('server')) return (config('server'))
->children($dn) ->children($dn)

View File

@ -8,7 +8,9 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use App\Exceptions\InvalidUsage;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Ldap\Entry;
class LoginController extends Controller class LoginController extends Controller
{ {
@ -51,6 +53,30 @@ class LoginController extends Controller
]; ];
} }
/**
* When attempt to login
*
* @param Request $request
* @return void
* @throws InvalidUsage
*/
public function attemptLogin(Request $request)
{
$attempt = $this->guard()->attempt(
$this->credentials($request), $request->boolean('remember')
);
// 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)) {
$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');
}
}
/** /**
* We need to delete our encrypted username/password cookies * We need to delete our encrypted username/password cookies
* *

View File

@ -26,6 +26,8 @@ use App\Ldap\Entry;
class HomeController extends Controller class HomeController extends Controller
{ {
private const LOGKEY = 'CHc';
/** /**
* Create a new object in the LDAP server * Create a new object in the LDAP server
* *
@ -44,21 +46,27 @@ class HomeController extends Controller
$o = new Entry; $o = new Entry;
$o->setRDNBase($key['dn']); $o->setRDNBase($key['dn']);
foreach (collect(old())->except(['_token','key','step','rdn','rdn_value','userpassword_hash']) as $old => $value)
$o->{$old} = array_filter($value);
if (count($x=collect(old('objectclass',$request->validated('objectclass')))->dot()->filter())) { if (count($x=collect(old('objectclass',$request->validated('objectclass')))->dot()->filter())) {
$o->objectclass = Arr::undot($x); $o->objectclass = Arr::undot($x);
// Also add in our required attributes // Also add in our required attributes
foreach($o->getAvailableAttributes()->filter(fn($item)=>$item->required) as $ao) foreach ($o->getAvailableAttributes()->filter(fn($item)=>$item->is_must) as $ao)
$o->{$ao->name} = [Entry::TAG_NOTAG=>'']; $o->{$ao->name} = [Entry::TAG_NOTAG=>''];
} elseif ($request->validated('template')) { } elseif ($request->validated('template')) {
$template = $o->template($request->validated('template')); $template = $o->template($request->validated('template'));
$o->objectclass = [Entry::TAG_NOTAG=>$template->objectclasses->toArray()]; $o->objectclass = [Entry::TAG_NOTAG=>$template->objectclasses->toArray()];
// @todo We need to add aliases foreach ($o->getAvailableAttributes()
foreach($o->getAvailableAttributes()->filter(fn($item)=>$template->attributes->contains($item)) as $ao) ->filter(fn($item)=>$item->names_lc->intersect($template->attributes->map('strtolower'))->count())
->sortBy(fn($item)=>Arr::get($template->order,$item->name)) as $ao)
{
$o->{$ao->name} = [Entry::TAG_NOTAG=>'']; $o->{$ao->name} = [Entry::TAG_NOTAG=>''];
} }
}
$step = $request->step ? $request->step+1 : old('step'); $step = $request->step ? $request->step+1 : old('step');

View File

@ -10,10 +10,12 @@ use Illuminate\Support\Facades\Log;
class AcceptLanguage class AcceptLanguage
{ {
private const LOGKEY = 'MAL';
public function handle(Request $request,Closure $next): mixed public function handle(Request $request,Closure $next): mixed
{ {
if ($locale=$this->parseHttpLocale($request)) { if ($locale=$this->parseHttpLocale($request)) {
Log::debug(sprintf('Accept Language changed from [%s] to [%s] from Browser (%s)',app()->getLocale(),$locale,$request->header('Accept-Language'))); Log::debug(sprintf('%s:Accept Language changed from [%s] to [%s] from Browser (%s)',self::LOGKEY,app()->getLocale(),$locale,$request->header('Accept-Language')));
app()->setLocale($locale); app()->setLocale($locale);
} }

View File

@ -35,9 +35,18 @@ class EntryAddRequest extends FormRequest
return []; return [];
$r = request() ?: collect(); $r = request() ?: collect();
$rk = array_keys($r->all());
return config('server') return config('server')
->schema('attributetypes') ->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',[]))) ->map(fn($item)=>$item->validation($r->get('objectclass',[])))
->filter() ->filter()
->flatMap(fn($item)=>$item) ->flatMap(fn($item)=>$item)

View File

@ -159,8 +159,11 @@ class Entry extends Model
if ($this->dn && (! in_array(strtolower($this->dn),['cn=subschema']))) { if ($this->dn && (! in_array(strtolower($this->dn),['cn=subschema']))) {
$this->templates = $this->templates $this->templates = $this->templates
->filter(fn($item)=>$item->enabled ->filter(fn($item)=>$item->enabled
&& (! count($item->objectclasses->diff(array_map('strtolower',Arr::get($this->attributes,'objectclass')))))) && (! $item->objectclasses
->sortBy(fn($item)=>$item); ->map('strtolower')
->diff(array_map('strtolower',Arr::get($this->attributes,'objectclass')))
->count()))
->sortBy(fn($item)=>$item->title);
} }
return $this; return $this;
@ -202,13 +205,13 @@ class Entry extends Model
// If the attribute name has tags // If the attribute name has tags
list($attribute,$tag) = $this->keytag($key); list($attribute,$tag) = $this->keytag($key);
if (! config('server')->schema('attributetypes')->has($attribute)) if (config('server')->get_attr_id($attribute) === FALSE)
throw new AttributeException(sprintf('Schema doesnt have attribute [%s]',$attribute)); throw new AttributeException(sprintf('Schema doesnt have attribute [%s]',$attribute));
$o = $this->objects->get($attribute) ?: Attribute\Factory::create($this->dn ?: '',$attribute,[]); $o = $this->objects->get($attribute) ?: Attribute\Factory::create($this->dn ?: '',$attribute,[]);
$o->addValue($tag,[$value]); $o->addValue($tag,[$value]);
$this->objects->put($attribute,$o); $this->objects->put($key,$o);
} }
/** /**
@ -312,7 +315,7 @@ class Entry extends Model
$result = collect(); $result = collect();
foreach (($this->getObject('objectclass')?->values ?: []) as $oc) foreach (($this->getObject('objectclass')?->values ?: []) as $oc)
$result = $result->merge(config('server')->schema('objectclasses',$oc)->attributes); $result = $result->merge(config('server')->schema('objectclasses',$oc)->all_attributes);
return $result; return $result;
} }
@ -415,9 +418,6 @@ class Entry extends Model
* Return a list of attributes without any values * Return a list of attributes without any values
* *
* @return Collection * @return Collection
* @todo Dont show attributes that are not provided by an objectclass, make a new function to show those
* This is for dynamic list items eg: labeledURI, which are not editable.
* We can highlight those values that are as a result of a dynamic module
*/ */
public function getMissingAttributes(): Collection public function getMissingAttributes(): Collection
{ {
@ -430,7 +430,7 @@ class Entry extends Model
$o = new Attribute\RDN('','dn',['']); $o = new Attribute\RDN('','dn',['']);
// @todo for an existing object, rdnbase would be null, so dynamically get it from the DN. // @todo for an existing object, rdnbase would be null, so dynamically get it from the DN.
$o->setBase($this->rdnbase); $o->setBase($this->rdnbase);
$o->setAttributes($this->getAvailableAttributes()->filter(fn($item)=>$item->required)); $o->setAttributes($this->getAvailableAttributes()->filter(fn($item)=>$item->is_must));
return $o; return $o;
} }

View File

@ -6,6 +6,7 @@ use Illuminate\Contracts\View\View;
use Illuminate\View\Component; use Illuminate\View\Component;
use App\Classes\LDAP\Attribute as LDAPAttribute; use App\Classes\LDAP\Attribute as LDAPAttribute;
use App\Classes\Template;
class Attribute extends Component class Attribute extends Component
{ {
@ -14,17 +15,19 @@ class Attribute extends Component
public bool $new; public bool $new;
public bool $old; public bool $old;
public bool $updated; public bool $updated;
public ?Template $template;
/** /**
* Create a new component instance. * Create a new component instance.
*/ */
public function __construct(?LDAPAttribute $o,bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE) public function __construct(?LDAPAttribute $o,bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL)
{ {
$this->o = $o; $this->o = $o;
$this->edit = $edit; $this->edit = $edit;
$this->old = $old; $this->old = $old;
$this->new = $new; $this->new = $new;
$this->updated = $updated; $this->updated = $updated;
$this->template = $template;
} }
/** /**
@ -36,7 +39,12 @@ class Attribute extends Component
{ {
return $this->o return $this->o
? $this->o ? $this->o
->render(edit: $this->edit,old: $this->old,new: $this->new,updated: $this->updated) ->render(
edit: $this->edit,
old: $this->old,
new: $this->new,
updated: $this->updated,
template: $this->template)
: __('Unknown'); : __('Unknown');
} }
} }

View File

@ -43,19 +43,6 @@ return [
'debug' => (bool) env('APP_DEBUG', false), 'debug' => (bool) env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| the application so that it's available within Artisan commands.
|
*/
'url' => env('APP_URL', 'http://localhost'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Application Timezone | Application Timezone

View File

@ -35,7 +35,6 @@ return [
'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'), 'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'),
'password' => env('LDAP_PASSWORD', 'secret'), 'password' => env('LDAP_PASSWORD', 'secret'),
'port' => env('LDAP_PORT', 389), 'port' => env('LDAP_PORT', 389),
'base_dn' => env('LDAP_BASE_DN', 'dc=local,dc=com'),
'timeout' => env('LDAP_TIMEOUT', 5), 'timeout' => env('LDAP_TIMEOUT', 5),
'use_ssl' => env('LDAP_SSL', false), 'use_ssl' => env('LDAP_SSL', false),
'use_tls' => env('LDAP_TLS', false), 'use_tls' => env('LDAP_TLS', false),
@ -47,7 +46,6 @@ return [
'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'), 'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'),
'password' => env('LDAP_PASSWORD', 'secret'), 'password' => env('LDAP_PASSWORD', 'secret'),
'port' => env('LDAP_PORT', 636), 'port' => env('LDAP_PORT', 636),
'base_dn' => env('LDAP_BASE_DN', 'dc=local,dc=com'),
'timeout' => env('LDAP_TIMEOUT', 5), 'timeout' => env('LDAP_TIMEOUT', 5),
'use_ssl' => env('LDAP_SSL', true), 'use_ssl' => env('LDAP_SSL', true),
'use_tls' => env('LDAP_TLS', false), 'use_tls' => env('LDAP_TLS', false),
@ -59,7 +57,6 @@ return [
'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'), 'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'),
'password' => env('LDAP_PASSWORD', 'secret'), 'password' => env('LDAP_PASSWORD', 'secret'),
'port' => env('LDAP_PORT', 389), 'port' => env('LDAP_PORT', 389),
'base_dn' => env('LDAP_BASE_DN', 'dc=local,dc=com'),
'timeout' => env('LDAP_TIMEOUT', 5), 'timeout' => env('LDAP_TIMEOUT', 5),
'use_ssl' => env('LDAP_SSL', false), 'use_ssl' => env('LDAP_SSL', false),
'use_tls' => env('LDAP_TLS', true), 'use_tls' => env('LDAP_TLS', true),
@ -72,7 +69,6 @@ return [
'username' => 'cn=Directory Manager', 'username' => 'cn=Directory Manager',
'password' => 'password', 'password' => 'password',
'port' => 1389, 'port' => 1389,
'base_dn' => 'dc=example,dc=com',
'timeout' => env('LDAP_TIMEOUT', 5), 'timeout' => env('LDAP_TIMEOUT', 5),
'use_ssl' => env('LDAP_SSL', false), 'use_ssl' => env('LDAP_SSL', false),
'use_tls' => env('LDAP_TLS', false), 'use_tls' => env('LDAP_TLS', false),

View File

@ -43,6 +43,17 @@ return [
'allow_guest' => env('LDAP_ALLOW_GUEST',FALSE), 'allow_guest' => env('LDAP_ALLOW_GUEST',FALSE),
/*
|--------------------------------------------------------------------------
| Base DNs
|--------------------------------------------------------------------------
|
| Normally PLA will get the base DNs from the rootDSE's namingcontexts
| 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,
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Custom Date Format | Custom Date Format
@ -73,8 +84,12 @@ return [
* setup. * setup.
*/ */
'login' => [ 'login' => [
'attr' => [env('LDAP_LOGIN_ATTR','uid') => env('LDAP_LOGIN_ATTR_DESC','User ID')], // Attribute used to find user for login // Attribute used to find user for login
'objectclass' => explode(',',env('LDAP_LOGIN_OBJECTCLASS', 'posixAccount')), // Objectclass that users must contain to login 'attr' => [strtolower(env('LDAP_LOGIN_ATTR','uid')) => env('LDAP_LOGIN_ATTR_DESC','User ID')],
// Objectclass that users must contain to login
'objectclass' => explode(',',env('LDAP_LOGIN_OBJECTCLASS', 'posixAccount')),
// Alert if DN is being used, and the login fails, and the the DN doesnt exist
'alert_rootdn' => env('LDAP_ALERT_ROOTDN',TRUE) && strtolower(env('LDAP_LOGIN_ATTR','uid')) === 'dn',
], ],
'template' => [ 'template' => [

View File

@ -37,10 +37,10 @@ function getNode(item) {
$('.main-content').empty().append(e.responseText); $('.main-content').empty().append(e.responseText);
break; break;
case 409: // Not in root case 409: // Not in root
location.replace('/#'+item);
break;
case 419: // Session Expired case 419: // Session Expired
location.replace('/#'+item); location.replace('/#'+item);
// When the session expires, and we are in the tree, we need to force a reload
if (location.pathname === '/')
location.reload(); location.reload();
break; break;
case 500: case 500:

View File

@ -169,6 +169,9 @@
$('button[id^="link-"]').on('click',function(item) { $('button[id^="link-"]').on('click',function(item) {
var content; var content;
// Remove our fancy-tree highlight, since we are rendering the frame
$('.fancytree-node.fancytree-active').removeClass('fancytree-active');
$.ajax({ $.ajax({
url: $(this).data('link'), url: $(this).data('link'),
method: 'GET', method: 'GET',

View File

@ -3,8 +3,8 @@
@foreach($o->langtags as $langtag) @foreach($o->langtags as $langtag)
@foreach(($o->tagValues($langtag)->count() ? $o->tagValues($langtag) : [$langtag => NULL]) as $key => $value) @foreach(($o->tagValues($langtag)->count() ? $o->tagValues($langtag) : [$langtag => NULL]) as $key => $value)
@if($edit) @if($edit)
<div class="input-group has-validation mb-3"> <div class="input-group has-validation">
<x-form.select id="userpassword_hash_{{$loop->index}}" 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="$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)> <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"> <div class="invalid-feedback pb-2">
@ -20,9 +20,9 @@
@endforeach @endforeach
</x-attribute.layout> </x-attribute.layout>
@if($edit) @if($edit && $o->tagValuesOld($langtag)->dot()->filter()->count())
<div class="row"> <div class="row">
<div class="offset-1 col-4 p-2"> <div class="offset-1 col-4">
<span class="p-0 m-0"> <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 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>
</span> </span>

View File

@ -8,7 +8,7 @@
@foreach($o->attrs->map(fn($item)=>['id'=>$item,'value'=>$item]) as $option) @foreach($o->attrs->map(fn($item)=>['id'=>$item,'value'=>$item]) as $option)
@continue(! Arr::get($option,'value')) @continue(! Arr::get($option,'value'))
<option value="{{ Arr::get($option,'id') }}" @selected(Arr::get($option,'id') == old('rdn',$value ?? ''))>{{ Arr::get($option,'value') }}</option> <option value="{{ strtolower(Arr::get($option,'id')) }}" @selected(Arr::get($option,'id') == old('rdn',$value ?? ''))>{{ Arr::get($option,'value') }}</option>
@endforeach @endforeach
</select> </select>

View File

@ -0,0 +1,33 @@
@extends('architect::layouts.error')
@section('error')
501: @lang('LDAP Authentication Error')
@endsection
@section('content')
<table class="table table-sm table-borderless table-condensed">
<tr>
<th>@lang('Error')</th>
</tr>
<tr>
<td colspan="2">{{ $exception->getMessage() }}</td>
</tr>
<tr>
<th>@lang('Possible Causes')</th>
</tr>
<tr>
<td>
<ul class="ps-3">
<li>The DN you used to login actually doesnt exist in the server (DN's must exist in order to login)</li>
<li>You are attempting to use the <strong>rootdn</strong> to login (not supported)</li>
</ul>
</td>
</tr>
</table>
<p>To suppress this message, set <strong>LDAP_ALERT_ROOTDN</strong> to <strong>FALSE</strong> before starting PLA.</p>
<p>Back to <a href="{{ url('login') }}">login</a>?</p>
@endsection

View File

@ -2,19 +2,19 @@
<div class="col-12 col-xl-3"> <div class="col-12 col-xl-3">
<select id="attributetype" class="form-control"> <select id="attributetype" class="form-control">
<option value="-all-">-all-</option> <option value="-all-">-all-</option>
@foreach($attributetypes as $o) @foreach(($at=$attributetypes->sortBy(fn($item)=>$item->names_lc->join(','))) as $o)
<option value="{{ $o->name_lc }}">{{ $o->name }}</option> <option value="{{ $o->names_lc->join('-') }}">{{ $o->names->join(',') }}</option>
@endforeach @endforeach
</select> </select>
</div> </div>
<div class="col-12 col-xl-9"> <div class="col-12 col-xl-9">
@foreach($attributetypes as $o) @foreach($at as $o)
<span id="at-{{ $o->name_lc }}"> <span id="at-{{ $o->names_lc->join('-') }}">
<table class="schema table table-sm table-bordered table-striped"> <table class="schema table table-sm table-bordered table-striped">
<thead> <thead>
<tr> <tr>
<th class="table-dark" colspan="2">{{ $o->name }}<span class="float-end"><abbr title="{{ $o->line }}"><i class="fas fa-fw fa-file-contract"></i></abbr></span></th> <th class="table-dark" colspan="2">{{ $o->names->join(' / ') }}<span class="float-end"><abbr title="{{ $o->line }}"><i class="fas fa-fw fa-file-contract"></i></abbr></span></th>
</tr> </tr>
</thead> </thead>
@ -57,7 +57,7 @@
<td>@lang('Substring Rule')</td><td><strong>{{ $o->sub_str_rule ?: __('(not specified)') }}</strong></td> <td>@lang('Substring Rule')</td><td><strong>{{ $o->sub_str_rule ?: __('(not specified)') }}</strong></td>
</tr> </tr>
<tr> <tr>
<td>@lang('Syntax')</td><td><strong>{{ ($o->syntax_oid && $x=$server->schemaSyntaxName($o->syntax_oid)) ? $x->description : __('(unknown syntax)') }} @if($o->syntax_oid)({{ $o->syntax_oid }})@endif</strong></td> <td>@lang('Syntax')</td><td><strong>{{ ($o->syntax_oid && $x=$server->get_syntax($o->syntax_oid)) ? $x->description : __('(unknown syntax)') }} @if($o->syntax_oid)({{ $o->syntax_oid }})@endif</strong></td>
</tr> </tr>
<tr> <tr>
<td>@lang('Single Valued')</td><td><strong>@lang($o->is_single_value ? 'Yes' : 'No')</strong></td> <td>@lang('Single Valued')</td><td><strong>@lang($o->is_single_value ? 'Yes' : 'No')</strong></td>
@ -77,11 +77,8 @@
<tr> <tr>
<td>@lang('Aliases')</td> <td>@lang('Aliases')</td>
<td><strong> <td><strong>
@if($o->aliases->count()) @if($o->names->count() > 1)
@foreach($o->aliases as $alias) {!! $o->names->join('</strong>, <strong>') !!}
@if($loop->index)</strong> <strong>@endif
<a class="attributetype" id="{{ strtolower($alias) }}" href="#{{ strtolower($alias) }}">{{ $alias }}</a>
@endforeach
@else @else
@lang('(none)') @lang('(none)')
@endif @endif

View File

@ -7,10 +7,9 @@
<div class="card-body"> <div class="card-body">
<div class="tab-content"> <div class="tab-content">
@php($up=(session()->pull('updated') ?: collect())) @php($up=(session()->pull('updated') ?: collect()))
@php($attributes=$o->template($template)?->attributes)
@foreach($o->getVisibleAttributes()->filter(fn($item)=>$attributes->contains($item)) as $ao) @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" :updated="$up->contains($ao->name_lc)"/> <x-attribute-type :o="$ao" :edit="TRUE" :new="FALSE" :template="$template" :updated="$up->contains($ao->name_lc)"/>
@endforeach @endforeach
</div> </div>
</div> </div>

View File

@ -66,10 +66,12 @@
<x-attribute-type :o="$o->getObject('rdn')" :edit="TRUE" :new="FALSE" :updated="FALSE"/> <x-attribute-type :o="$o->getObject('rdn')" :edit="TRUE" :new="FALSE" :updated="FALSE"/>
@foreach($o->getVisibleAttributes() as $ao) @foreach($o->getVisibleAttributes() as $ao)
<x-attribute-type :o="$ao" :edit="TRUE" :new="FALSE" :updated="FALSE"/> <x-attribute-type :o="$ao" :edit="TRUE" :new="FALSE" :template="$template" :updated="FALSE"/>
@endforeach @endforeach
@if(! $template) @if(! $template)
<!-- @todo When we come back from validation the javascript to append a new attribute is not loaded -->
<!-- @todo When we render attributes with javascript, the javascript is not loaded -->
@include('fragment.dn.add_attr') @include('fragment.dn.add_attr')
@endif @endif
@break; @break;
@ -89,7 +91,6 @@
@endsection @endsection
@section('page-scripts') @section('page-scripts')
<script type="text/javascript"> <script type="text/javascript">
var rdn_attr; var rdn_attr;

View File

@ -75,8 +75,8 @@
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<div role="group" class="btn-group btn-group-sm nav pb-3"> <div role="group" class="btn-group btn-group-sm nav pb-3">
<!-- If we have templates that cover this entry --> <!-- If we have templates that cover this entry -->
@foreach($o->templates as $template => $name) @foreach($o->templates as $template)
<span data-bs-toggle="tab" href="#template-{{$template}}" @class(['btn','btn-outline-focus','active'=>$loop->index === 0])><i class="fa fa-fw pe-2 {{ $o->template($template)->icon }}"></i> {{ $name }}</span> <span data-bs-toggle="tab" href="#template-{{ $template->name }}" @class(['btn','btn-outline-focus','active'=>$loop->index === 0])><i class="fa fa-fw pe-2 {{ $template->icon }}"></i> {{ $template->title }}</span>
@endforeach @endforeach
@if($o->templates->count()) @if($o->templates->count())
<span data-bs-toggle="tab" href="#template-default" @class(['btn','btn-outline-focus','p-1','active'=>(! $o->templates->count())])>{{ __('LDAP Entry') }}</span> <span data-bs-toggle="tab" href="#template-default" @class(['btn','btn-outline-focus','p-1','active'=>(! $o->templates->count())])>{{ __('LDAP Entry') }}</span>
@ -85,8 +85,8 @@
</div> </div>
<div class="tab-content"> <div class="tab-content">
@foreach($o->templates as $template => $name) @foreach($o->templates as $template)
<div @class(['tab-pane','active'=>$loop->index === 0]) id="template-{{$template}}" role="tabpanel"> <div @class(['tab-pane','active'=>$loop->index === 0]) id="template-{{ $template->name }}" role="tabpanel">
@include('fragment.template.dn',['template'=>$template]) @include('fragment.template.dn',['template'=>$template])
</div> </div>
@endforeach @endforeach

View File

@ -19,7 +19,23 @@
"attribute2": { "attribute2": {
"display": "Attribute 2", "display": "Attribute 2",
"hint": "This is an example", "hint": "This is an example",
"type": "input", // Default is input
"order": 2 "order": 2
},
"attribute3": {
"display": "Attribute 3",
"type": "select",
"options": {
"/bin/bash": "Bash",
"/bin/csh": "C Shell",
"/bin/dash": "Dash",
"/bin/sh": "Shell",
"/bin/tsh": "Turbo C Shell",
"/bin/zsh": "ZSH",
"/bin/false": "False",
"/usr/sbin/nologin": "No Login"
},
"order": 8
} }
} }
} }

View File

@ -66,7 +66,8 @@
}, },
"loginShell": { "loginShell": {
"display": "Login Shell", "display": "Login Shell",
"select": { "type": "select",
"options": {
"/bin/bash": "Bash", "/bin/bash": "Bash",
"/bin/csh": "C Shell", "/bin/csh": "C Shell",
"/bin/dash": "Dash", "/bin/dash": "Dash",