Compare commits

...

4 Commits

Author SHA1 Message Date
fc856b973e Add Attribute required by ObjectClasses in schema viewer,
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 36s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m38s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m38s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
Attribute is_rdn dynamically calculated,
Fix Required by Objectclasses when viewing a DN
2025-03-14 23:45:54 +11:00
d41f93f2cc Move direct controller direct view calls to route/web, add global $server to use in views, negating the need to use config('server') 2025-03-14 23:39:32 +11:00
f99c96e2a0 Some DN rendering fixes, so that our Server Info renders correctly (aligned values) 2025-03-14 23:39:32 +11:00
366769455f Store our DN and objectclasses in Attribute::class entries, so that we can dynamically calculate is_rdn and required objects (to be implemented) 2025-03-14 23:39:32 +11:00
23 changed files with 177 additions and 108 deletions

View File

@ -20,9 +20,6 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
// Is this attribute an internal attribute
protected(set) bool $is_internal = FALSE;
// Is this attribute the RDN?
public bool $is_rdn = FALSE;
// MIN/MAX number of values
protected(set) int $min_values_count = 0;
protected(set) int $max_values_count = 0;
@ -34,10 +31,14 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
// The schema's representation of this attribute
protected(set) ?AttributeType $schema;
// The DN this object is in
protected(set) string $dn;
// The old values for this attribute - helps with isDirty() to determine if there is an update pending
protected(set) Collection $values_old;
// Current Values
public Collection $values;
// The objectclasses of the entry that has this attribute
protected(set) Collection $oc;
/*
# Has the attribute been modified
@ -90,12 +91,22 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
protected $postvalue = array();
*/
public function __construct(string $name,array $values)
/**
* Create an Attribute
*
* @param string $dn DN this attribute is used in
* @param string $name Name of the attribute
* @param array $values Current Values
* @param array $oc The objectclasses that the DN of this attribute has
*/
public function __construct(string $dn,string $name,array $values,array $oc=[])
{
$this->dn = $dn;
$this->name = $name;
$this->values_old = collect($values);
$this->values = collect();
$this->oc = collect($oc);
$this->lang_tags = collect();
$this->schema = (new Server)
@ -129,6 +140,10 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
'hints' => $this->hints(),
// Can this attribute be edited
'is_editable' => $this->schema ? $this->schema->{$key} : NULL,
// Objectclasses that required this attribute for an LDAP entry
'required' => $this->required(),
// Is this attribute an RDN attribute
'is_rdn' => $this->isRDN(),
// We prefer the name as per the schema if it exists
'name' => $this->schema ? $this->schema->{$key} : $this->{$key},
// Attribute name in lower case
@ -218,11 +233,8 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
// If this attribute name is an alias for the schema attribute name
// @todo
// objectClasses requiring this attribute
// @todo limit this to this DNs objectclasses
// eg: $result->put('required','Required by objectClasses: a,b');
if ($this->required_by->count())
$result->put(__('required'),sprintf('%s: %s',__('Required Attribute by ObjectClass(es)'),$this->required_by->join(',')));
if ($this->required()->count())
$result->put(__('required'),sprintf('%s: %s',__('Required Attribute by ObjectClass(es)'),$this->required()->join(', ')));
// This attribute has language tags
if ($this->lang_tags->count())
@ -242,6 +254,22 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
|| ($this->values->diff($this->values_old)->count() !== 0);
}
/**
* Work out if this attribute is an RDN attribute
*
* @return bool
*/
public function isRDN(): bool
{
// If we dont have an DN, then we cant know
if (! $this->dn)
return FALSE;
$rdns = collect(explode('+',substr($this->dn,0,strpos($this->dn,','))));
return $rdns->filter(fn($item) => str_starts_with($item,$this->name.'='))->count() > 0;
}
/**
* Display the attribute value
*
@ -273,6 +301,19 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
return Arr::get($this->values,$key);
}
/**
* Work out if this attribute is required by an objectClass the entry has
*
* @return Collection
*/
public function required(): Collection
{
// If we dont have any objectclasses then we cant know if it is required
return $this->oc->count()
? $this->oc->intersect($this->schema->required_by_object_classes->keys())->sort()
: collect();
}
/**
* If this attribute has RFC3866 Language Tags, this will enable those values to be captured
*

View File

@ -49,15 +49,17 @@ class Factory
/**
* Create the new Object for an attribute
*
* @param string $dn
* @param string $attribute
* @param array $values
* @param array $oc
* @return Attribute
*/
public static function create(string $attribute,array $values): Attribute
public static function create(string $dn,string $attribute,array $values,array $oc=[]): Attribute
{
$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($attribute,$values);
return new $class($dn,$attribute,$values,$oc);
}
}

View File

@ -15,13 +15,21 @@ final class ObjectClass extends Attribute
// The schema ObjectClasses for this objectclass of a DN
protected Collection $oc_schema;
public function __construct(string $name,array $values)
/**
* Create an ObjectClass Attribute
*
* @param string $dn DN this attribute is used in
* @param string $name Name of the attribute
* @param array $values Current Values
* @param array $oc The objectclasses that the DN of this attribute has
*/
public function __construct(string $dn,string $name,array $values,array $oc=[])
{
parent::__construct($name,$values);
parent::__construct($dn,$name,$values,['top']);
$this->oc_schema = config('server')
->schema('objectclasses')
->filter(fn($item)=>$this->values->contains($item->name));
->filter(fn($item)=>$this->values->merge($this->values_old)->unique()->contains($item->name));
}
public function __get(string $key): mixed

View File

@ -3,9 +3,9 @@
namespace App\Classes\LDAP;
use Illuminate\Support\Collection;
use LdapRecord\LdapRecordException;
use App\Exceptions\Import\GeneralException;
use App\Exceptions\Import\ObjectExistsException;
use App\Ldap\Entry;
/**
@ -48,7 +48,6 @@ abstract class Import
* @param int $action
* @return Collection
* @throws GeneralException
* @throws ObjectExistsException
*/
final protected function commit(Entry $o,int $action): Collection
{
@ -57,7 +56,8 @@ abstract class Import
try {
$o->save();
} catch (\Exception $e) {
} catch (LdapRecordException $e) {
if ($e->getDetailedError())
return collect([
'dn'=>$o->getDN(),
'result'=>sprintf('%d: %s (%s)',
@ -66,6 +66,14 @@ abstract class Import
$x->getDiagnosticMessage(),
)
]);
else
return collect([
'dn'=>$o->getDN(),
'result'=>sprintf('%d: %s',
$e->getCode(),
$e->getMessage(),
)
]);
}
return collect(['dn'=>$o->getDN(),'result'=>__('Created')]);

View File

@ -46,7 +46,7 @@ class LDIF extends Import
if (! $line) {
if (! is_null($o)) {
// Add the last attribute;
$o->addAttribute($attribute,$base64encoded ? base64_decode($value) : $value);
$o->addAttributeItem($attribute,$base64encoded ? base64_decode($value) : $value);
Log::debug(sprintf('%s: Committing Entry [%s]',self::LOGKEY,$o->getDN()));
@ -125,7 +125,7 @@ class LDIF extends Import
Log::debug(sprintf('%s: Adding Attribute [%s] value [%s] (%d)',self::LOGKEY,$attribute,$value,$c));
if ($value)
$o->addAttribute($attribute,$base64encoded ? base64_decode($value) : $value);
$o->addAttributeItem($attribute,$base64encoded ? base64_decode($value) : $value);
else
throw new GeneralException(sprintf('Attribute has no value [%s] (line %d)',$attribute,$c));
}
@ -147,7 +147,7 @@ class LDIF extends Import
// We may still have a pending action
if ($action) {
// Add the last attribute;
$o->addAttribute($attribute,$base64encoded ? base64_decode($value) : $value);
$o->addAttributeItem($attribute,$base64encoded ? base64_decode($value) : $value);
Log::debug(sprintf('%s: Committing Entry [%s]',self::LOGKEY,$o->getDN()));

View File

@ -320,11 +320,12 @@ final class AttributeType extends Base {
* that is the list of objectClasses which must have this attribute.
*
* @param string $name The name of the objectClass to add.
* @param bool $structural
*/
public function addRequiredByObjectClass(string $name): void
public function addRequiredByObjectClass(string $name,bool $structural): void
{
if (! $this->required_by_object_classes->contains($name))
$this->required_by_object_classes->push($name);
if (! $this->required_by_object_classes->has($name))
$this->required_by_object_classes->put($name,$structural);
}
/**
@ -332,6 +333,7 @@ final class AttributeType extends Base {
* that is the list of objectClasses which provide this attribute.
*
* @param string $name The name of the objectClass to add.
* @param bool $structural
*/
public function addUsedInObjectClass(string $name,bool $structural): void
{
@ -544,7 +546,7 @@ final class AttributeType extends Base {
*/
public function validation(array $array): ?array
{
// For each item in array, we need to get the OC heirachy
// For each item in array, we need to get the OC hierarchy
$heirachy = collect($array)
->filter()
->map(fn($item)=>config('server')

View File

@ -428,7 +428,7 @@ final class Server
// Add Required By.
foreach ($must_attrs as $attr_name)
if ($this->attributetypes->has(strtolower($attr_name)))
$this->attributetypes[strtolower($attr_name)]->addRequiredByObjectClass($object_class->name);
$this->attributetypes[strtolower($attr_name)]->addRequiredByObjectClass($object_class->name,$object_class->isStructural());
// Force May
foreach ($object_class->getForceMayAttrs() as $attr_name)

View File

@ -41,14 +41,6 @@ class HomeController extends Controller
});
}
/**
* Debug Page
*/
public function debug()
{
return view('debug');
}
/**
* Create a new object in the LDAP server
*
@ -69,7 +61,7 @@ class HomeController extends Controller
$o->objectclass = $x;
foreach($o->getAvailableAttributes()->filter(fn($item)=>$item->required) as $ao)
$o->addAttribute($ao,'');
$o->{$ao->name} = '';
$o->setRDNBase($key['dn']);
}
@ -96,14 +88,14 @@ class HomeController extends Controller
$xx = new \stdClass;
$xx->index = 0;
$x = $request->noheader
$dn = $request->dn ? Crypt::decrypt($request->dn) : '';
return $request->noheader
? view(sprintf('components.attribute.widget.%s',$id))
->with('o',Factory::create($id,[]))
->with('o',Factory::create($dn,$id,[],$request->objectclasses))
->with('value',$request->value)
->with('loop',$xx)
: (new AttributeType(Factory::create($id,[]),TRUE,collect($request->oc ?: [])))->render();
return $x;
: new AttributeType(Factory::create($dn,$id,[],$request->objectclasses),TRUE)->render();
}
public function entry_create(EntryAddRequest $request): \Illuminate\Http\RedirectResponse
@ -214,7 +206,8 @@ class HomeController extends Controller
*/
public function entry_objectclass_add(Request $request): Collection
{
$oc = Factory::create('objectclass',$request->oc);
$dn = $request->key ? Crypt::decryptString($request->dn) : '';
$oc = Factory::create($dn,'objectclass',$request->oc);
$ocs = $oc
->structural
@ -450,22 +443,6 @@ class HomeController extends Controller
->with('ldif',htmlspecialchars($x));
}
public function import_frame()
{
return view('frames.import');
}
/**
* LDAP Server INFO
*
* @return \Illuminate\View\View
*/
public function info(): \Illuminate\View\View
{
return view('frames.info')
->with('s',config('server'));
}
/**
* For any incoming request, work out the command and DN involved
*

View File

@ -25,6 +25,7 @@ class ApplicationSession
{
Config::set('server',new Server);
view()->share('server', Config::get('server'));
view()->share('user', auth()->user() ?: new User);
return $next($request);

View File

@ -16,7 +16,9 @@ use App\Exceptions\InvalidUsage;
class Entry extends Model
{
// Our Attribute objects
private Collection $objects;
/* @deprecated */
private bool $noObjectAttributes = FALSE;
// For new entries, this is the container that this entry will be stored in
private string $rdnbase;
@ -82,22 +84,22 @@ class Entry extends Model
/**
* As attribute values are updated, or new ones created, we need to mirror that
* into our $objects
* into our $objects. This is called when we $o->key = $value
*
* @param string $key
* @param mixed $value
* @return $this
*/
public function setAttribute(string $key, mixed $value): static
public function setAttribute(string $key,mixed $value): static
{
parent::setAttribute($key,$value);
$key = $this->normalizeAttributeKey($key);
if ((! $this->objects->get($key)) && $value)
$this->objects->put($key,Factory::create($key,[]));
$o = $this->objects->get($key) ?: Factory::create($this->dn ?: '',$key,[],Arr::get($this->attributes,'objectclass',[]));
$o->values = collect($this->attributes[$key]);
$this->objects->get($key)->values = collect($this->attributes[$key]);
$this->objects->put($key,$o);
return $this;
}
@ -140,7 +142,18 @@ class Entry extends Model
/* METHODS */
public function addAttribute(string $key,mixed $value): void
/**
* Add an attribute to this entry, if the attribute already exists, then we'll add the value to the existing item.
*
* This is primarily used by LDIF imports, where attributes have multiple entries over multiple lines
*
* @param string $key
* @param mixed $value
* @return void
* @throws AttributeException
* @note Attributes added this way dont have objectclass information, and the Model::attributes are not populated
*/
public function addAttributeItem(string $key,mixed $value): void
{
// While $value is mixed, it can only be a string
if (! is_string($value))
@ -151,16 +164,11 @@ class Entry extends Model
if (! config('server')->schema('attributetypes')->has($key))
throw new AttributeException(sprintf('Schema doesnt have attribute [%s]',$key));
if ($x=$this->objects->get($key)) {
$x->addValue($value);
} else {
$o = Attribute\Factory::create($key,[]);
$o->values = collect(Arr::wrap($value));
$o = $this->objects->get($key) ?: Attribute\Factory::create($this->dn ?: '',$key,[]);
$o->addValue($value);
$this->objects->put($key,$o);
}
}
/**
* Convert all our attribute values into an array of Objects
@ -170,6 +178,7 @@ class Entry extends Model
private function getAttributesAsObjects(): Collection
{
$result = collect();
$entry_oc = Arr::get($this->attributes,'objectclass',[]);
foreach ($this->attributes as $attribute => $values) {
// If the attribute name has tags
@ -178,17 +187,22 @@ class Entry extends Model
$attribute = $matches[1];
// If the attribute doesnt exist we'll create it
$o = Arr::get($result,$attribute,Factory::create($attribute,Arr::get($this->original,$attribute,[])));
$o = Arr::get(
$result,
$attribute,
Factory::create(
$this->dn,
$attribute,
Arr::get($this->original,$attribute,[]),
$entry_oc,
));
$o->setLangTag($matches[3],$values);
} else {
$o = Factory::create($attribute,Arr::get($this->original,$attribute,[]));
$o = Factory::create($this->dn,$attribute,Arr::get($this->original,$attribute,[]),$entry_oc);
}
if (! $result->has($attribute)) {
// Set the rdn flag
$o->is_rdn = preg_match('/^'.$attribute.'=/i',$this->dn);
// Store our new values to know if this attribute has changed
$o->values = collect($values);
@ -299,8 +313,8 @@ class Entry extends Model
private function getRDNObject(): Attribute\RDN
{
$o = new Attribute\RDN('dn',['']);
// @todo for an existing object, return the base.
$o = new Attribute\RDN('','dn',['']);
// @todo for an existing object, rdnbase would be null, so dynamically get it from the DN.
$o->setBase($this->rdnbase);
$o->setAttributes($this->getAvailableAttributes()->filter(fn($item)=>$item->required));
@ -424,6 +438,7 @@ class Entry extends Model
* Dont convert our $this->attributes to $this->objects when creating a new Entry::class
*
* @return $this
* @deprecated
*/
public function noObjectAttributes(): static
{

View File

@ -11,17 +11,15 @@ use App\Classes\LDAP\Attribute as LDAPAttribute;
class AttributeType extends Component
{
public Collection $oc;
public LDAPAttribute $o;
public bool $new;
private LDAPAttribute $o;
private bool $new;
/**
* Create a new component instance.
*/
public function __construct(LDAPAttribute $o,bool $new=FALSE,?Collection $oc=NULL)
public function __construct(LDAPAttribute $o,bool $new=FALSE)
{
$this->o = $o;
$this->oc = $oc;
$this->new = $new;
}
@ -32,7 +30,6 @@ class AttributeType extends Component
{
return view('components.attribute-type')
->with('o',$this->o)
->with('oc',$this->oc)
->with('new',$this->new);
}
}

View File

@ -22,7 +22,7 @@
<div class="h5 modal-title text-center">
<h4 class="mt-2">
<div class="app-logo mx-auto mb-3"><img class="w-75" src="{{ url('images/logo-h-lg.png') }}"></div>
<small>@lang('Sign in to') <strong>{{ config('server')->name }}</strong></small>
<small>@lang('Sign in to') <strong>{{ $server->name }}</strong></small>
</h4>
</div>

View File

@ -32,7 +32,7 @@
<div class="scrollbar-sidebar">
<div class="app-sidebar__inner">
<ul class="vertical-nav-menu">
<li class="app-sidebar__heading">{{ config('server')->name }}</li>
<li class="app-sidebar__heading">{{ $server->name }}</li>
<li>
<i id="treeicon" class="metismenu-icon fa-fw fas fa-sitemap"></i>
<span class="f16" id="tree"></span>

View File

@ -1,4 +1,4 @@
<!-- $o=Internal\Timestamp::class -->
<!-- $o=Internal::class -->
@foreach (old($o->name_lc,$o->values) as $value)
@if($loop->index)<br>@endif
{{ $value }}

View File

@ -1,6 +1,6 @@
<div class="row pt-2">
<div @class(['col-1','d-none'=>(! $edit) && (! ($detail ?? true))])></div>
<div class="col-10 p-2">
<div @class(['col-1','d-none'=>(! $edit) && (! ($detail ?? false))])></div>
<div class="col-10">
<attribute id="{{ $o->name }}">
{{ $slot }}
</attribute>

View File

@ -106,6 +106,24 @@
@endif
</td>
</tr>
<tr>
<td>@lang('Required by ObjectClasses')</td>
<td>
@if ($o->required_by_object_classes->count())
@foreach ($o->required_by_object_classes as $class => $structural)
@if($structural)
<strong>
@endif
<a class="objectclass" id="{{ strtolower($class) }}" href="#{{ strtolower($class) }}">{{ $class }}</a>
@if($structural)
</strong>
@endif
@endforeach
@else
@lang('(none)')
@endif
</td>
</tr>
<tr>
<td>@lang('Force as MAY by config')</td><td><strong>@lang($o->forced_as_may ? 'Yes' : 'No')</strong></td>
</tr>

View File

@ -1,7 +1,7 @@
@extends('layouts.dn')
@section('page_title')
@include('fragment.dn.header',['o'=>($oo=config('server')->fetch(old('container',$container)))])
@include('fragment.dn.header',['o'=>($oo=$server->fetch(old('container',$container)))])
@endsection
@section('main-content')
@ -30,7 +30,7 @@
id="objectclass"
name="objectclass[]"
:label="__('Select a Structural ObjectClass...')"
:options="($oc=config('server')->schema('objectclasses'))
:options="($oc=$server->schema('objectclasses'))
->filter(fn($item)=>$item->isStructural())
->sortBy(fn($item)=>$item->name_lc)
->map(fn($item)=>['id'=>$item->name,'value'=>$item->name])"

View File

@ -1,7 +1,7 @@
@extends('layouts.dn')
@section('page_title')
@include('fragment.dn.header',['o'=>($o ?? $o=config('server')->fetch($dn))])
@include('fragment.dn.header',['o'=>($o ?? $o=$server->fetch($dn))])
@endsection
@section('main-content')

View File

@ -4,7 +4,7 @@
<table class="table table-borderless">
<tr>
<td style="border-radius: 5px;"><div class="page-title-icon f32"><i class="fas fa-upload"></i></div></td>
<td class="top text-start align-text-top p-0 pt-2"><strong>@lang('LDIF Import')</strong><br><small>@lang('To Server') <strong>{{ config('server')->name }}</strong></small></td>
<td class="top text-start align-text-top p-2"><strong>@lang('LDIF Import')</strong><br><small>@lang('To Server') <strong>{{ $server->name }}</strong></small></td>
</tr>
</table>
@endsection

View File

@ -2,7 +2,7 @@
<table class="table table-borderless">
<tr>
<td style="border-radius: 5px;"><div class="page-title-icon f32"><i class="fas fa-upload"></i></div></td>
<td class="top text-start align-text-top p-0 pt-2"><strong>@lang('LDIF Import Result')</strong><br><small>@lang('To Server') <strong>{{ config('server')->name }}</strong></small></td>
<td class="top text-start align-text-top p-0 pt-2"><strong>@lang('LDIF Import Result')</strong><br><small>@lang('To Server') <strong>{{ $server->name }}</strong></small></td>
</tr>
</table>
@endsection

View File

@ -4,7 +4,7 @@
<table class="table table-borderless">
<tr>
<td style="border-radius: 5px;"><div class="page-title-icon f32"><i class="fas fa-info"></i></div></td>
<td class="top text-end align-text-top p-0 pt-2"><strong>@lang('Server Info')</strong><br><small>{{ $s->rootDSE()->entryuuid[0] ?? '' }}</small></td>
<td class="top text-end align-text-top p-2"><strong>@lang('Server Info')</strong><br><small>{{ $server->rootDSE()->entryuuid[0] ?? '' }}</small></td>
</tr>
</table>
@endsection
@ -13,10 +13,10 @@
<div class="main-card mb-3 card">
<div class="card-body">
<table class="table">
@foreach ($s->rootDSE()->getObjects() as $attribute => $ao)
@foreach ($server->rootDSE()->getObjects() as $attribute => $ao)
<tr>
<th class="w-25">
{!! ($x=$s->schema('attributetypes',$attribute))
{!! ($x=$server->schema('attributetypes',$attribute))
? sprintf('<a class="attributetype" id="strtolower(%s)" href="%s">%s</a>',$x->name_lc,url('schema/attributetypes',$x->name_lc),$x->name)
: $attribute !!}
</th>

View File

@ -5,7 +5,7 @@
<table class="table table-borderless">
<tr>
<td style="border-radius: 5px;"><div class="page-title-icon f32"><i class="fas fa-fingerprint"></i></div></td>
<td class="top text-end align-text-top p-0 pt-2"><strong>{{ Server::schemaDN() }}</strong></td>
<td class="top text-end align-text-top p-2"><strong>{{ Server::schemaDN() }}</strong></td>
</tr>
</table>
@endsection

View File

@ -31,10 +31,10 @@ Route::get('logout',[LoginController::class,'logout']);
Route::controller(HomeController::class)->group(function() {
Route::middleware(AllowAnonymous::class)->group(function() {
Route::get('/','home');
Route::get('info','info');
Route::get('debug','debug');
Route::view('info','frames.info');
Route::view('debug','debug');
Route::post('frame','frame');
Route::get('import','import_frame');
Route::view('import','frames.import');
Route::get('schema','schema_frame');
Route::group(['prefix'=>'user'],function() {