Start of work to enable creation of new entries
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 1m30s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 9m28s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s

This commit is contained in:
Deon George 2025-02-23 18:14:41 +11:00
parent f9bd352bfb
commit 6ebf588b1f
18 changed files with 454 additions and 66 deletions

View File

@ -145,9 +145,9 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
// Attribute values
'values' => $this->values,
// Required by Object Classes
'required_by' => $this->schema->required_by_object_classes,
'required_by' => $this->schema?->required_by_object_classes ?: collect(),
// Used in Object Classes
'used_in' => $this->schema->used_in_object_classes,
'used_in' => $this->schema?->used_in_object_classes ?: collect(),
default => throw new \Exception('Unknown key:' . $key),
};

View File

@ -0,0 +1,49 @@
<?php
namespace App\Classes\LDAP\Attribute;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use App\Classes\LDAP\Attribute;
/**
* Represents the RDN for an Entry
*/
final class RDN extends Attribute
{
private string $base;
private Collection $attrs;
public function __get(string $key): mixed
{
return match ($key) {
'base' => $this->base,
'attrs' => $this->attrs->pluck('name'),
default => parent::__get($key),
};
}
public function hints(): array
{
return [
'required' => __('RDN is required')
];
}
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
{
return view('components.attribute.rdn')
->with('o',$this);
}
public function setAttributes(Collection $attrs): void
{
$this->attrs = $attrs;
}
public function setBase(string $base): void
{
$this->base = $base;
}
}

View File

@ -208,7 +208,7 @@ final class ObjectClass extends Base
public function __get(string $key): mixed
{
return match ($key) {
'attributes' => $this->getAllAttrs(),
'attributes' => $this->getAllAttrs(TRUE),
'sup' => $this->sup_classes,
'type_name' => match ($this->type) {
Server::OC_STRUCTURAL => 'Structural',
@ -223,13 +223,18 @@ final class ObjectClass extends Base
/**
* Return a list of attributes that this objectClass provides
*
* @param bool $parents
* @return Collection
* @throws InvalidUsage
*/
public function getAllAttrs(): Collection
public function getAllAttrs(bool $parents=FALSE): Collection
{
return $this->getMustAttrs()
->merge($this->getMayAttrs());
return $this->getMustAttrs($parents)
->transform(function($item) {
$item->required = true;
return $item;
})
->merge($this->getMayAttrs($parents));
}
/**

View File

@ -16,6 +16,7 @@ namespace App\Classes\LDAP\Schema;
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.
@ -31,11 +32,9 @@ final class ObjectClassAttribute extends Base {
public function __get(string $key): mixed
{
switch ($key) {
case 'source':
return $this->source;
default: return parent::__get($key);
}
return match ($key) {
'source' => $this->source,
default => parent::__get($key),
};
}
}

View File

@ -22,15 +22,15 @@ class APIController extends Controller
{
$base = Server::baseDNs() ?: collect();
return $base->transform(function($item) {
return [
'title'=>$item->getRdn(),
'item'=>$item->getDNSecure(),
'lazy'=>TRUE,
'icon'=>'fa-fw fas fa-sitemap',
'tooltip'=>$item->getDn(),
];
});
return $base
->transform(fn($item)=>
[
'title'=>$item->getRdn(),
'item'=>$item->getDNSecure(),
'lazy'=>TRUE,
'icon'=>'fa-fw fas fa-sitemap',
'tooltip'=>$item->getDn(),
]);
}
/**
@ -45,15 +45,22 @@ class APIController extends Controller
return (config('server'))
->children($dn)
->transform(function($item) {
return [
->transform(fn($item)=>
[
'title'=>$item->getRdn(),
'item'=>$item->getDNSecure(),
'icon'=>$item->icon(),
'lazy'=>Arr::get($item->getAttribute('hassubordinates'),0) == 'TRUE',
'tooltip'=>$item->getDn(),
];
});
])
->prepend(
[
'title'=>sprintf('[%s]',__('Create Entry')),
'item'=>Crypt::encryptString(sprintf('*%s|%s','create_new',$dn)),
'lazy'=>FALSE,
'icon'=>'fas fa-fw fa-square-plus text-warning',
'tooltip'=>__('Create new LDAP item here'),
]);
}
public function schema_view(Request $request)

View File

@ -2,6 +2,9 @@
namespace App\Http\Controllers;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
@ -12,16 +15,16 @@ use Illuminate\Support\Facades\Redirect;
use LdapRecord\Exceptions\InsufficientAccessException;
use LdapRecord\LdapRecordException;
use LdapRecord\Query\ObjectNotFoundException;
use Nette\NotImplementedException;
use App\Classes\LDAP\{Attribute,Server};
use App\Classes\LDAP\Import\LDIF as LDIFImport;
use App\Classes\LDAP\Export\LDIF as LDIFExport;
use App\Exceptions\Import\{GeneralException,VersionException};
use App\Exceptions\InvalidUsage;
use App\Http\Requests\{EntryRequest,ImportRequest};
use App\Http\Requests\{EntryAdd,EntryRequest,ImportRequest};
use App\Ldap\Entry;
use App\View\Components\AttributeType;
use Nette\NotImplementedException;
class HomeController extends Controller
{
@ -59,13 +62,60 @@ class HomeController extends Controller
public function dn_frame(Request $request)
{
$dn = Crypt::decryptString($request->post('key'));
$cmd = '';
$page_actions = collect(['edit'=>TRUE,'copy'=>TRUE]);
if (str_contains($dn,'|')) {
$m = [];
return view('frames.dn')
->with('o',config('server')->fetch($dn))
->with('dn',$dn)
->with('page_actions',$page_actions);
if (preg_match('/\*([a-z_]+)\|(.+)$/',$dn,$m)) {
$cmd = $m[1];
$dn = $m[2];
}
}
return match ($cmd) {
'create_new' => view('frames.create')
->with('o',config('server')->fetch($dn))
->with('step',0)
->with('dn',$dn),
default => view('frames.dn')
->with('o',config('server')->fetch($dn))
->with('dn',$dn)
->with('page_actions',collect(['edit'=>TRUE,'copy'=>TRUE])),
};
}
/**
* Create a new object in the LDAP server
*
* @param EntryAdd $request
* @return Factory|View|Application|object
* @throws InvalidUsage
*/
public function entry_add(EntryAdd $request)
{
switch ($request->step) {
case 1:
$container = Crypt::decryptString($request->dn);
$o = new Entry;
$o->objectclass = $request->objectclass;
foreach($o->getAvailableAttributes()->filter(fn($item)=>$item->required) as $ao)
$o->addAttribute($ao,['']);
$o->setRDNBase($container);
return view('frame')
->with('subframe',$request->frame)
->with('bases',$this->bases())
->with('o',$o)
->with('step',$request->step)
->with('dn',$container);
default:
throw new InvalidUsage('Invalid entry step');
}
}
/**
@ -259,26 +309,30 @@ class HomeController extends Controller
}
/**
* Application home page
* This is the main page render function
*
* If a DN is set, when render a DN info/edit frame
* If a frame is set, then we render that (sub)frame
*/
public function home()
{
if (old('dn'))
if (old('frame'))
return view('frame')
->with('subframe',old('frame'))
->with('bases',$this->bases())
->with('o',old('dn') ? config('server')->fetch($dn=Crypt::decryptString(old('dn'))) : NULL)
->with('dn',$dn ?? NULL);
elseif (old('dn'))
return view('frame')
->with('subframe','dn')
->with('bases',$this->bases())
->with('o',config('server')->fetch($dn=Crypt::decryptString(old('dn'))))
->with('dn',$dn);
elseif (old('frame'))
return view('frame')
->with('subframe',old('frame'))
->with('bases',$this->bases());
else
return view('home')
->with('bases',$this->bases())
->with('server',config('ldap.connections.default.name'));
->with('bases',$this->bases());
}
/**

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use App\Rules\{DNExists,HasStructuralObjectClass};
class EntryAdd extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'dn' => new DNExists,
'objectclass' => [
'required',
'array',
new HasStructuralObjectClass,
],
'step' => 'int|min:1|max:2',
'frame' => [
'string',
Rule::in(['create']),
]
];
}
}

View File

@ -6,22 +6,12 @@ use Illuminate\Foundation\Http\FormRequest;
class EntryRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return TRUE;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
public function rules(): array
{
return config('server')
->schema('attributetypes')

View File

@ -6,12 +6,7 @@ use Illuminate\Foundation\Http\FormRequest;
class ImportRequest extends FormRequest
{
public function authorize()
{
return TRUE;
}
public function rules()
public function rules(): array
{
return [
'frame' => 'required|string|in:import',

View File

@ -12,11 +12,14 @@ use App\Classes\LDAP\Attribute;
use App\Classes\LDAP\Attribute\Factory;
use App\Classes\LDAP\Export\LDIF;
use App\Exceptions\Import\AttributeException;
use App\Exceptions\InvalidUsage;
class Entry extends Model
{
private Collection $objects;
private bool $noObjectAttributes = FALSE;
// For new entries, this is the container that this entry will be stored in
private string $rdnbase;
/* OVERRIDES */
@ -46,7 +49,7 @@ class Entry extends Model
public function getAttributes(): array
{
return $this->objects
->map(fn($item)=>$item->values->toArray())
->map(fn($item)=>$item->values)
->toArray();
}
@ -92,10 +95,7 @@ class Entry extends Model
$key = $this->normalizeAttributeKey($key);
if ((! $this->objects->get($key)) && $value) {
$o = new Attribute($key,[]);
$o->value = $value;
$this->objects->put($key,$o);
$this->objects->put($key,Factory::create($key,$value));
} elseif ($this->objects->get($key)) {
$this->objects->get($key)->value = $this->attributes[$key];
@ -265,8 +265,12 @@ class Entry extends Model
*/
public function getObject(string $key): Attribute|null
{
return $this->objects
->get($this->normalizeAttributeKey($key));
return match ($key) {
'rdn' => $this->getRDNObject(),
default => $this->objects
->get($this->normalizeAttributeKey($key))
};
}
public function getObjects(): Collection
@ -289,6 +293,16 @@ class Entry extends Model
->filter(fn($a)=>(! $this->getVisibleAttributes()->contains(fn($b)=>($a->name === $b->name))));
}
private function getRDNObject(): Attribute\RDN
{
$o = new Attribute\RDN('dn',['']);
// @todo for an existing object, return the base.
$o->setBase($this->rdnbase);
$o->setAttributes($this->getAvailableAttributes()->filter(fn($item)=>$item->required));
return $o;
}
/**
* Return this list of user attributes
*
@ -413,4 +427,12 @@ class Entry extends Model
return $this;
}
public function setRDNBase(string $bdn): void
{
if ($this->exists)
throw new InvalidUsage('Cannot set RDN base on existing entries');
$this->rdnbase = $bdn;
}
}

21
app/Rules/DNExists.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Crypt;
class DNExists implements ValidationRule
{
/**
* Run the validation rule.
*
* @param \Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
public function validate(string $attribute,mixed $value,Closure $fail): void
{
if (! config('server')->fetch($x=Crypt::decryptString($value)))
$fail(sprintf('The DN %s doesnt exist.',$x));
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class HasStructuralObjectClass implements ValidationRule
{
/**
* Run the validation rule.
*
* @param \Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
public function validate(string $attribute,mixed $value,Closure $fail): void
{
foreach ($value as $item)
if (config('server')->schema('objectclasses',$item)->isStructural())
return;
$fail('There isnt a StructuralObject class.');
}
}

View File

@ -304,3 +304,7 @@ div#objectClass .input-group-delete {
right: 10px;
height: 5px;
}
.input-group-text {
background-color: #fafafa;
}

View File

@ -0,0 +1,25 @@
<!-- $o=RDN::class -->
<x-attribute.layout :edit="$edit ?? FALSE" :new="$new ?? FALSE" :o="$o">
@foreach($o->values as $value)
@if($edit)
<div class="input-group has-validation mb-3">
<x-form.select
name="rdn"
:options="$o->attrs->map(fn($item)=>['id'=>$item,'value'=>$item])"
/>
<span class="input-group-text">=</span>
<input type="text" @class(['form-control','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index)),'border-focus'=>$o->values->contains($value)]) name="rdn" placeholder="rdn">
<label class="input-group-text" for="inputGroupSelect02">,{{ $o->base }}</label>
<div class="invalid-feedback pb-2">
@if($e)
{{ join('|',$e) }}
@endif
</div>
</div>
@else
{{ $value }}
@endif
@endforeach
</x-attribute.layout>

View File

@ -51,7 +51,6 @@
$('select#newoc').select2({
dropdownParent: $('#new_objectclass-modal'),
theme: 'bootstrap-5',
allowClear: true,
multiple: true,
data: data,
});

View File

@ -54,10 +54,16 @@
width: 'style',
allowClear: {{ $allowclear ?? 'false' }},
placeholder: '{{ $placeholder ?? '' }}',
multiple: {{ $multiple ?? 'false' }},
@isset($addvalues)
tags: true,
@endisset
});
@if(isset($multiple) && (! $multiple))
$('#{{ $id ?? $name }}').val(' ');
$('#{{ $id ?? $name }}').trigger('change');
@endif
});
</script>
@append

View File

@ -0,0 +1,155 @@
@extends('layouts.dn')
@section('page_title')
@include('fragment.dn.header')
@endsection
@section('main-content')
<div class="row">
<p class="alert alert-danger text-center">
This is a tech preview of what is to come, and it is by no means complete.
</p>
</div>
<div class="row">
<div class="offset-1 col-10">
<div class="main-card mb-3 card">
<form id="create-form" action="{{ url('entry/add') }}" method="POST" enctype="multipart/form-data">
@csrf
<input type="hidden" name="frame" value="create">
<input type="hidden" name="dn" value="{{ Crypt::encryptString($dn) }}">
<input type="hidden" name="step" value="{{ ++$step }}">
<div class="card-header">
@lang('Create New Entry')
</div>
<div class="card-body">
@switch($step)
@case(1)
<div class="row">
<div class="col-12 col-sm-6">
<x-form.select
id="objectclass"
name="objectclass[]"
:label="__('Select a Structural ObjectClass...')"
:options="($oc=config('server')->schema('objectclasses'))
->filter(fn($item)=>$item->isStructural())
->sortBy(fn($item)=>$item->name_lc)
->map(fn($item,$key)=>['id'=>$key,'value'=>$item->name])"
multiple="false"
/>
</div>
</div>
@break
@case(2)
<x-attribute-type :edit="true" :o="$o->getObject('rdn')"/>
@foreach ($o->getVisibleAttributes() as $ao)
<x-attribute-type :edit="true" :o="$ao"/>
@endforeach
<div id="newattrs"></div>
<!-- Add new attributes -->
<div class="row">
<div class="col-12 col-sm-1 col-md-2"></div>
<div class="col-12 col-sm-10 col-md-8">
<div class="d-none" id="newattr-select">
@if($o->getMissingAttributes()->count())
<div class="row">
<div class="col-12 bg-dark text-light p-2">
<i class="fas fa-plus-circle"></i> Add New Attribute
</div>
</div>
<div class="row">
<div class="col-12 pt-2">
<x-form.select id="newattr" label="Select from..." :options="$o->getMissingAttributes()->sortBy('name')->map(fn($item)=>['id'=>$item->name,'value'=>$item->name_lc])"/>
</div>
</div>
@endif
</div>
</div>
<div class="col-2"></div>
</div>
@break;
@endswitch
</div>
<div class="card-footer">
<span class="ms-auto">
<x-form.submit action="Next" form="dn-add"/>
</span>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('page-scripts')
<script type="text/javascript">
var dn = '{{ $o->getDNSecure() }}';
var oc = {!! $o->getObject('objectclass')->values !!};
function editmode() {
$('#dn-edit input[name="dn"]').val(dn);
$('button[id=entry-edit]').addClass('active').removeClass('btn-outline-dark').addClass('btn-outline-light');
// Find all input items and turn off readonly
$('input.form-control').each(function() {
// Except for objectClass - @todo show an "X" instead
if ($(this)[0].name.match(/^objectclass/))
return;
$(this).attr('readonly',false);
});
// Our password type
$('div#userPassword .form-select').each(function() {
$(this).prop('disabled',false);
})
$('.row.d-none').removeClass('d-none');
$('.addable.d-none').removeClass('d-none');
$('.deletable.d-none').removeClass('d-none');
@if($o->getMissingAttributes()->count())
$('#newattr-select.d-none').removeClass('d-none');
@endif
}
$(document).ready(function() {
$('#newattr').on('change',function(item) {
$.ajax({
type: 'POST',
beforeSend: function() {},
success: function(data) {
$('#newattrs').append(data);
},
error: function(e) {
if (e.status != 412)
alert('That didnt work? Please try again....');
},
url: '{{ url('entry/attr/add') }}/'+item.target.value,
data: {
objectclasses: oc,
},
cache: false
});
// Remove the option from the list
$(this).find('[value="'+item.target.value+'"]').remove()
// If there are no more options
if ($(this).find("option").length === 1)
$('#newattr-select').remove();
});
editmode();
});
</script>
@append

View File

@ -38,6 +38,7 @@ Route::group(['prefix'=>'user'],function() {
Route::get('image',[HomeController::class,'user_image']);
});
Route::post('entry/add',[HomeController::class,'entry_add']);
Route::get('entry/export/{id}',[HomeController::class,'entry_export']);
Route::post('entry/password/check/',[HomeController::class,'entry_password_check']);
Route::post('entry/attr/add/{id}',[HomeController::class,'entry_attr_add']);