Validation of inputs for a DN with language tags - work for #16
Some checks failed
Create Docker Image / Build Docker Image (arm64) (push) Has been cancelled
Create Docker Image / Build Docker Image (x86_64) (push) Has been cancelled
Create Docker Image / Final Docker Image Manifest (push) Has been cancelled
Create Docker Image / Test Application (x86_64) (push) Has been cancelled

This commit is contained in:
Deon George 2025-04-06 13:47:31 +10:00
parent 28f4869628
commit bcea6de791
15 changed files with 80 additions and 61 deletions

View File

@ -6,6 +6,9 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use App\Classes\LDAP\Attribute;
use App\Ldap\Entry;
/** /**
* Represents an LDAP AttributeType * Represents an LDAP AttributeType
* *
@ -341,6 +344,11 @@ final class AttributeType extends Base {
$this->used_in_object_classes->put($name,$structural); $this->used_in_object_classes->put($name,$structural);
} }
private function factory(): Attribute
{
return Attribute\Factory::create(dn:'',attribute:$this->name,values:[]);
}
/** /**
* Gets the names of attributes that are an alias for this attribute (if any). * Gets the names of attributes that are an alias for this attribute (if any).
* *
@ -548,6 +556,7 @@ final class AttributeType extends Base {
{ {
// For each item in array, we need to get the OC hierarchy // For each item in array, we need to get the OC hierarchy
$heirachy = collect($array) $heirachy = collect($array)
->flatten()
->filter() ->filter()
->map(fn($item)=>config('server') ->map(fn($item)=>config('server')
->schema('objectclasses',$item) ->schema('objectclasses',$item)
@ -556,14 +565,17 @@ final class AttributeType extends Base {
->flatten() ->flatten()
->unique(); ->unique();
// Get any config validation
$validation = collect(Arr::get(config('ldap.validation'),$this->name_lc,[])); $validation = collect(Arr::get(config('ldap.validation'),$this->name_lc,[]));
$nolangtag = sprintf('%s.%s.0',$this->name_lc,Entry::TAG_NOTAG);
// Add in schema required by conditions // Add in schema required by conditions
if (($heirachy->intersect($this->required_by_object_classes->keys())->count() > 0) if (($heirachy->intersect($this->required_by_object_classes->keys())->count() > 0)
&& (! collect($validation->get($this->name_lc))->contains('required'))) { && (! collect($validation->get($this->name_lc))->contains('required'))) {
$validation $validation
->prepend(array_merge(['required','min:1'],$validation->get($this->name_lc.'.0',[])),$this->name_lc.'.0') ->prepend(array_merge(['required','min:1'],$validation->get($nolangtag,[])),$nolangtag)
->prepend(array_merge(['required','array','min:1'],$validation->get($this->name_lc,[])),$this->name_lc); ->prepend(array_merge(['required','array','min:1',($this->factory()->no_attr_tags ? 'max:1' : NULL)],$validation->get($this->name_lc,[])),$this->name_lc);
} }
return $validation->toArray(); return $validation->toArray();

View File

@ -57,9 +57,10 @@ class HomeController extends Controller
$o = new Entry; $o = new Entry;
if (count(array_filter($x=old('objectclass',$request->objectclass)))) { if (count($x=array_filter(old('objectclass',$request->objectclass)))) {
$o->objectclass = [Entry::TAG_NOTAG=>$x]; $o->objectclass = $x;
// Also add in our required attributes
foreach($o->getAvailableAttributes()->filter(fn($item)=>$item->required) as $ao) foreach($o->getAvailableAttributes()->filter(fn($item)=>$item->required) as $ao)
$o->{$ao->name} = [Entry::TAG_NOTAG=>'']; $o->{$ao->name} = [Entry::TAG_NOTAG=>''];
@ -375,8 +376,7 @@ class HomeController extends Controller
// If we are rendering a DN, rebuild our object // If we are rendering a DN, rebuild our object
$o = config('server')->fetch($key['dn']); $o = config('server')->fetch($key['dn']);
// @todo We need to dynamically exclude request items, so we dont need to add them here foreach (collect(old())->except(['key','step','_token','userpassword_hash']) as $attr => $value)
foreach (collect(old())->except(['dn','_token','userpassword_hash']) as $attr => $value)
$o->{$attr} = $value; $o->{$attr} = $value;
return match ($key['cmd']) { return match ($key['cmd']) {

View File

@ -34,10 +34,11 @@ class EntryAddRequest extends FormRequest
if (request()->method() === 'GET') if (request()->method() === 'GET')
return []; return [];
$r = request() ?: collect();
return config('server') return config('server')
->schema('attributetypes') ->schema('attributetypes')
->intersectByKeys($this->request) ->intersectByKeys($r->all())
->map(fn($item)=>$item->validation(request()->get('objectclass'))) ->map(fn($item)=>$item->validation($r->get('objectclass',[])))
->filter() ->filter()
->flatMap(fn($item)=>$item) ->flatMap(fn($item)=>$item)
->merge([ ->merge([
@ -60,6 +61,12 @@ class EntryAddRequest extends FormRequest
'rdn_value' => 'required_if:step,2|string|min:1', 'rdn_value' => 'required_if:step,2|string|min:1',
'step' => 'int|min:1|max:2', 'step' => 'int|min:1|max:2',
'objectclass'=>[ 'objectclass'=>[
'required',
'array',
'min:1',
'max:1',
],
'objectclass._null_'=>[
'required', 'required',
'array', 'array',
'min:1', 'min:1',

View File

@ -13,10 +13,12 @@ class EntryRequest extends FormRequest
*/ */
public function rules(): array public function rules(): array
{ {
$r = request() ?: collect();
return config('server') return config('server')
->schema('attributetypes') ->schema('attributetypes')
->intersectByKeys($this->request) ->intersectByKeys($r->all())
->map(fn($item)=>$item->validation(request()?->get('objectclass') ?: [])) ->map(fn($item)=>$item->validation($r->get('objectclass',[])))
->filter() ->filter()
->flatMap(fn($item)=>$item) ->flatMap(fn($item)=>$item)
->toArray(); ->toArray();

View File

@ -188,6 +188,36 @@ class Entry extends Model
$this->objects->put($attribute,$o); $this->objects->put($attribute,$o);
} }
/**
* Export this record
*
* @param string $method
* @param string $scope
* @return string
* @throws \Exception
*/
public function export(string $method,string $scope): string
{
// @todo To implement
switch ($scope) {
case 'base':
case 'one':
case 'sub':
break;
default:
throw new \Exception('Export scope unknown:'.$scope);
}
switch ($method) {
case 'ldif':
return new LDIF(collect($this));
default:
throw new \Exception('Export method not implemented:'.$method);
}
}
/** /**
* Convert all our attribute values into an array of Objects * Convert all our attribute values into an array of Objects
* *
@ -409,36 +439,6 @@ class Entry extends Model
->has($key); ->has($key);
} }
/**
* Export this record
*
* @param string $method
* @param string $scope
* @return string
* @throws \Exception
*/
public function export(string $method,string $scope): string
{
// @todo To implement
switch ($scope) {
case 'base':
case 'one':
case 'sub':
break;
default:
throw new \Exception('Export scope unknown:'.$scope);
}
switch ($method) {
case 'ldif':
return new LDIF(collect($this));
default:
throw new \Exception('Export method not implemented:'.$method);
}
}
/** /**
* Return an icon for a DN based on objectClass * Return an icon for a DN based on objectClass
* *

View File

@ -20,7 +20,7 @@ class HasStructuralObjectClass implements ValidationRule
*/ */
public function validate(string $attribute,mixed $value,Closure $fail): void public function validate(string $attribute,mixed $value,Closure $fail): void
{ {
foreach ($value as $item) foreach (collect($value)->dot() as $item)
if ($item && config('server')->schema('objectclasses',$item)->isStructural()) if ($item && config('server')->schema('objectclasses',$item)->isStructural())
return; return;

View File

@ -122,47 +122,47 @@ return [
*/ */
'validation' => [ 'validation' => [
'objectclass' => [ 'objectclass' => [
'objectclass'=>[ 'objectclass.*'=>[
new HasStructuralObjectClass, new HasStructuralObjectClass,
] ]
], ],
'gidnumber' => [ 'gidnumber' => [
'gidnumber'=> [ 'gidnumber.*'=> [
'sometimes', 'sometimes',
'max:1' 'max:1'
], ],
'gidnumber.*' => [ 'gidnumber.*.*' => [
'nullable', 'nullable',
'integer', 'integer',
'max:65535' 'max:65535'
] ]
], ],
'mail' => [ 'mail' => [
'mail'=>[ 'mail.*'=>[
'sometimes', 'sometimes',
'min:1' 'min:1'
], ],
'mail.*' => [ 'mail.*.*' => [
'nullable', 'nullable',
'email' 'email'
] ]
], ],
'userpassword' => [ 'userpassword' => [
'userpassword' => [ 'userpassword.*' => [
'sometimes', 'sometimes',
'min:1' 'min:1'
], ],
'userpassword.*' => [ 'userpassword.*.*' => [
'nullable', 'nullable',
'min:8' 'min:8'
] ]
], ],
'uidnumber' => [ 'uidnumber' => [
'uidnumber' => [ 'uidnumber.*' => [
'sometimes', 'sometimes',
'max:1' 'max:1'
], ],
'uidnumber.*' => [ 'uidnumber.*.*' => [
'nullable', 'nullable',
'integer', 'integer',
'max:65535' 'max:65535'

View File

@ -4,7 +4,7 @@
@foreach(Arr::get(old($o->name_lc,[$langtag=>($new ?? FALSE) ? [NULL] : $o->tagValues($langtag)]),$langtag) as $key => $value) @foreach(Arr::get(old($o->name_lc,[$langtag=>($new ?? FALSE) ? [NULL] : $o->tagValues($langtag)]),$langtag) as $key => $value)
@if(($edit ?? FALSE) && ! $o->is_rdn) @if(($edit ?? FALSE) && ! $o->is_rdn)
<div class="input-group has-validation"> <div class="input-group has-validation">
<input type="text" @class(['form-control','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index)),'mb-1','border-focus'=>! ($tv=$o->tagValuesOld($langtag))->contains($value)]) name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ $value }}" placeholder="{{ ! is_null($x=$tv->get($loop->index)) ? $x : '['.__('NEW').']' }}" @readonly(! ($new ?? FALSE))> <input type="text" @class(['form-control','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$langtag.'.'.$loop->index)),'mb-1','border-focus'=>! ($tv=$o->tagValuesOld($langtag))->contains($value)]) name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ $value }}" placeholder="{{ ! is_null($x=$tv->get($loop->index)) ? $x : '['.__('NEW').']' }}" @readonly(! ($new ?? FALSE))>
<div class="invalid-feedback pb-2"> <div class="invalid-feedback pb-2">
@if($e) @if($e)

View File

@ -9,7 +9,7 @@
@default @default
<td> <td>
<input type="hidden" name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ md5($value) }}"> <input type="hidden" name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ md5($value) }}">
<img alt="{{ $o->dn }}" @class(['border','rounded','p-2','m-0','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index))]) src="data:{{ $x }};base64, {{ base64_encode($value) }}" /> <img alt="{{ $o->dn }}" @class(['border','rounded','p-2','m-0','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$langtag.'.'.$loop->index))]) src="data:{{ $x }};base64, {{ base64_encode($value) }}" />
@if($edit) @if($edit)
<br> <br>

View File

@ -4,7 +4,7 @@
@foreach($o->tagValuesOld($langtag) as $key => $value) @foreach($o->tagValuesOld($langtag) as $key => $value)
@if($edit) @if($edit)
<div class="input-group has-validation mb-3"> <div class="input-group has-validation mb-3">
<input type="password" @class(['form-control','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index)),'mb-1','border-focus'=>! $o->tagValuesOld($langtag)->contains($value)]) name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ md5($value) }}" @readonly(true)> <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)]) name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ md5($value) }}" @readonly(true)>
<div class="invalid-feedback pb-2"> <div class="invalid-feedback pb-2">
@if($e) @if($e)

View File

@ -9,7 +9,7 @@
<input type="hidden" name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ $value }}" @readonly(true)> <input type="hidden" name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ $value }}" @readonly(true)>
<div class="invalid-feedback pb-2"> <div class="invalid-feedback pb-2">
@if($e=$errors->get($o->name_lc.'.'.$loop->index)) @if($e=$errors->get($o->name_lc.'.'.$langtag.'.'.$loop->index))
{{ join('|',$e) }} {{ join('|',$e) }}
@endif @endif
</div> </div>

View File

@ -5,7 +5,7 @@
@if($edit) @if($edit)
<div class="input-group has-validation mb-3"> <div class="input-group has-validation mb-3">
<x-form.select id="userpassword_hash_{{$loop->index}}" name="userpassword_hash[{{ $langtag }}][]" :value="$o->hash($value)->id()" :options="$helpers" allowclear="false" :disabled="true"/> <x-form.select id="userpassword_hash_{{$loop->index}}" name="userpassword_hash[{{ $langtag }}][]" :value="$o->hash($value)->id()" :options="$helpers" allowclear="false" :disabled="true"/>
<input type="password" @class(['form-control','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index)),'mb-1','border-focus'=>! $o->tagValuesOld($langtag)->contains($value)]) name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ md5($value) }}" @readonly(true)> <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)]) name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ md5($value) }}" @readonly(true)>
<div class="invalid-feedback pb-2"> <div class="invalid-feedback pb-2">
@if($e) @if($e)

View File

@ -1,7 +1,7 @@
<span id="objectclass_{{$value}}"> <span id="objectclass_{{$value}}">
<div class="input-group has-validation"> <div class="input-group has-validation">
<!-- @todo Have an "x" to remove the entry, we need an event to process the removal, removing any attribute values along the way --> <!-- @todo Have an "x" to remove the entry, we need an event to process the removal, removing any attribute values along the way -->
<input type="text" @class(['form-control','input-group-end','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index)),'mb-1','border-focus'=>! $o->tagValuesOld($langtag)->contains($value)]) name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ $value }}" placeholder="{{ Arr::get($o->values,$loop->index,'['.__('NEW').']') }}" @readonly(true)> <input type="text" @class(['form-control','input-group-end','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$langtag.'.'.$loop->index)),'mb-1','border-focus'=>! $o->tagValuesOld($langtag)->contains($value)]) name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ $value }}" placeholder="{{ Arr::get($o->values,$loop->index,'['.__('NEW').']') }}" @readonly(true)>
@if ($o->isStructural($value)) @if ($o->isStructural($value))
<span class="input-group-end text-black-50">@lang('structural')</span> <span class="input-group-end text-black-50">@lang('structural')</span>
@else @else

View File

@ -1,3 +1,5 @@
@use(App\Ldap\Entry)
@extends('layouts.dn') @extends('layouts.dn')
@section('page_title') @section('page_title')
@ -31,7 +33,7 @@
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<x-form.select <x-form.select
id="objectclass" id="objectclass"
name="objectclass[]" name="objectclass[{{ Entry::TAG_NOTAG }}][]"
:label="__('Select a Structural ObjectClass...')" :label="__('Select a Structural ObjectClass...')"
:options="($oc=$server->schema('objectclasses')) :options="($oc=$server->schema('objectclasses'))
->filter(fn($item)=>$item->isStructural()) ->filter(fn($item)=>$item->isStructural())

View File

@ -62,10 +62,6 @@
<div class="ms-4 mt-4 alert alert-danger p-2" style="max-width: 30em; font-size: 0.80em;"> <div class="ms-4 mt-4 alert alert-danger p-2" style="max-width: 30em; font-size: 0.80em;">
This entry has multi-language tags used by [<strong>{!! $x->keys()->join('</strong>, <strong>') !!}</strong>] that cant be managed by PLA. You can though manage those lang tags with an LDIF import. This entry has multi-language tags used by [<strong>{!! $x->keys()->join('</strong>, <strong>') !!}</strong>] that cant be managed by PLA. You can though manage those lang tags with an LDIF import.
</div> </div>
@elseif(($x=$o->getLangTags())->count())
<div class="ms-4 mt-4 alert alert-warning p-2" style="max-width: 30em; font-size: 0.80em;">
This entry has language tags used by [<strong>{!! $x->keys()->join('</strong>, <strong>') !!}</strong>] that cant be managed by PLA yet. You can though manage those lang tags with an LDIF import.
</div>
@endif @endif
</div> </div>
</div> </div>