Start of enabling DN update.

This commit is contained in:
Deon George 2023-03-31 15:55:08 +11:00
parent a1a9b8ba76
commit c36383b0fc
13 changed files with 359 additions and 125 deletions

View File

@ -2,6 +2,7 @@
namespace App\Classes\LDAP;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use App\Classes\LDAP\Schema\AttributeType;
@ -130,12 +131,14 @@ class Attribute
'hints' => $this->hints(),
// Is this an internal attribute
'is_internal' => isset($this->{$key}) && $this->{$key},
// Is this attribute the RDN
'is_rdn' => $this->is_rdn,
// We prefer the name as per the schema if it exists
'name' => $this->schema ? $this->schema->{$key} : $this->{$key},
// Attribute name in lower case
'name_lc' => strtolower($this->name),
// Is this attribute the RDN
'rdn' => $this->is_rdn,
// Attribute values
'values' => $this->values,
default => throw new \Exception('Unknown key:' . $key),
};
@ -151,22 +154,6 @@ class Attribute
return $this->values->join('<br>');
}
/**
* Return an instance of this attribute that is deletable.
* This is primarily used for rendering to know if to render delete options.
*
* @return Attribute
*/
public function deletable(): self
{
$clone = clone $this;
if (! $this->required_by->count())
$clone->is_deletable = TRUE;
return $clone;
}
/**
* Return the hints about this attribute, ie: RDN, Required, etc
*
@ -195,6 +182,13 @@ class Attribute
return $result->toArray();
}
public function render(bool $edit): View
{
return view('components.attribute')
->with('edit',$edit)
->with('o',$this);
}
/**
* Set the objectclasses that require this attribute
*

View File

@ -27,7 +27,7 @@ class APIController extends Controller
->transform(function($item) {
return [
'title'=>$item->getRdn(),
'item'=>Crypt::encryptString($item->getDn()),
'item'=>$item->getDNSecure(),
'icon'=>$item->icon(),
'lazy'=>Arr::get($item->getAttribute('hassubordinates'),0) == 'TRUE',
'tooltip'=>$item->getDn(),

View File

@ -8,10 +8,14 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Session;
use LdapRecord\Exceptions\InsufficientAccessException;
use LdapRecord\LdapRecordException;
use LdapRecord\Query\ObjectNotFoundException;
use App\Classes\LDAP\Server;
use App\Exceptions\InvalidUsage;
use App\Http\Requests\EntryRequest;
class HomeController extends Controller
{
@ -25,6 +29,66 @@ class HomeController extends Controller
return view('debug');
}
/**
* Render a specific DN
*
* @param Request $request
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function dn_frame(Request $request)
{
$dn = Crypt::decryptString($request->post('key'));
return view('frames.dn')
->with('o',config('server')->fetch($dn))
->with('dn',$dn);
}
public function entry_update(EntryRequest $request)
{
$dn = Crypt::decryptString($request->dn);
$o = config('server')->fetch($dn);
foreach ($request->except(['_token','dn']) as $key => $value)
$o->{$key} = array_filter($value);
Session::put('dn',$request->dn);
if (! $dirty=$o->getDirty())
return back()->with(['note'=>__('No attributes changed')]);
try {
$o->update($request->except(['_token','dn']));
} catch (InsufficientAccessException $e) {
$request->flash();
switch ($x=$e->getDetailedError()->getErrorCode()) {
case 50:
return back()->withErrors(sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
default:
abort(599,$e->getDetailedError()->getErrorMessage());
}
} catch (LdapRecordException $e) {
$request->flash();
switch ($x=$e->getDetailedError()->getErrorCode()) {
case 8:
return back()->withErrors(sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
default:
abort(599,$e->getDetailedError()->getErrorMessage());
}
}
return back()
->with(['success'=>__('Entry updated')])
->with(['updated'=>$dirty]);
}
/**
* Application home page
*/
@ -32,17 +96,25 @@ class HomeController extends Controller
{
$base = Server::baseDNs() ?: collect();
return view('home')
->with('server',config('ldap.connections.default.name'))
->with('bases',$base->transform(function($item) {
$bases = $base->transform(function($item) {
return [
'title'=>$item->getRdn(),
'item'=>Crypt::encryptString($item->getDn()),
'item'=>$item->getDNSecure(),
'lazy'=>TRUE,
'icon'=>'fa-fw fas fa-sitemap',
'tooltip'=>$item->getDn(),
];
}));
});
if (Session::has('dn'))
return view('dn')
->with('bases',$bases)
->with('o',config('server')->fetch($dn=Crypt::decryptString(Session::pull('dn'))))
->with('dn',$dn);
else
return view('home')
->with('bases',$bases)
->with('server',config('ldap.connections.default.name'));
}
/**
@ -62,21 +134,6 @@ class HomeController extends Controller
->with('s',$s);
}
/**
* Render a specific DN
*
* @param Request $request
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function dn_frame(Request $request)
{
$dn = Crypt::decryptString($request->post('key'));
return view('frames.dn')
->with('o',config('server')->fetch($dn))
->with('dn',$dn);
}
/**
* Show the Schema Viewer
*

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests;
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()
{
return [
'dn'=>'string|min:3',
'objectclass'=>'array|min:1',
];
}
}

View File

@ -4,6 +4,7 @@ namespace App\Ldap;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Crypt;
use LdapRecord\Models\Model;
use App\Classes\LDAP\Attribute;
@ -93,6 +94,15 @@ class Entry extends Model
/* METHODS */
/**
* Return a secure version of the DN
* @return string
*/
public function getDNSecure(): string
{
return Crypt::encryptString($this->getDn());
}
/**
* Return a list of LDAP internal attributes
*

View File

@ -0,0 +1,34 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use App\Classes\LDAP\Attribute as LDAPAttribute;
class Attribute extends Component
{
public LDAPAttribute $o;
public bool $edit;
/**
* Create a new component instance.
*
* @return void
*/
public function __construct(bool $edit,LDAPAttribute $o)
{
$this->edit = $edit;
$this->o = $o;
}
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\Contracts\View\View|\Closure|string
*/
public function render()
{
return $this->o->render($this->edit);
}
}

65
public/js/custom.js vendored
View File

@ -11,6 +11,38 @@ function expandChildren(node) {
}
}
function getNode(item) {
$.ajax({
url: 'dn',
method: 'POST',
data: { key: item },
dataType: 'html',
beforeSend: function() {
content = $('.main-content').contents();
$('.main-content').empty().append('<div class="fa-3x"><i class="fas fa-spinner fa-pulse"></i></div>');
}
}).done(function(html) {
$('.main-content').empty().append(html);
}).fail(function(item) {
switch(item.status) {
case 404:
$('.main-content').empty().append(item.responseText);
break;
case 419:
alert('Session has expired, reloading the page and try again...');
location.reload();
break;
case 500:
$('.main-content').empty().append(item.responseText);
break;
default:
alert(item.status+': Well that didnt work?');
}
});
}
$(document).ready(function() {
// If our bases have been set, we'll render them directly
if (typeof basedn !== 'undefined') {
@ -50,35 +82,7 @@ $(document).ready(function() {
},
click: function(event,data) {
if (data.targetType == 'title') {
$.ajax({
url: 'dn',
method: 'POST',
data: { key: data.node.data.item },
dataType: 'html',
beforeSend: function() {
content = $('.main-content').contents();
$('.main-content').empty().append('<div class="fa-3x"><i class="fas fa-spinner fa-pulse"></i></div>');
}
}).done(function(html) {
$('.main-content').empty().append(html);
}).fail(function(item) {
switch(item.status) {
case 404:
$('.main-content').empty().append(item.responseText);
break;
case 419:
alert('Session has expired, reloading the page and try again...');
location.reload();
break;
case 500:
$('.main-content').empty().append(item.responseText);
break;
default:
alert(item.status+': Well that didnt work?');
}
});
getNode(data.node.data.item);
}
},
source: sources,
@ -97,6 +101,9 @@ $(document).ready(function() {
data.node.toggleExpanded();
break;
}
},
restore: function(event,data) {
//getNode(data.tree.getActiveNode().data.item);
}
});
});

View File

@ -15,9 +15,9 @@
<div class="modal-dialog w-100 mx-auto">
<div class="modal-content">
<div class="modal-header">
<div class="app-logo"><img class="w-50" src="{{ url('images/logo-h-lg.png') }}"></div>
<img class="w-25" src="{{ url('images/logo-h-lg.png') }}">
<span class="card-header-title text-danger ms-auto fs-4">@yield('title')</span>
</div>
<div class="modal-body">

View File

@ -0,0 +1,23 @@
<div class="row">
<div class="col-12">
<div id="{{ $o->name_lc }}">
@foreach (old($o->name_lc,$o->values) as $value)
@if ($edit && ! $o->is_rdn)
<input class="form-control mb-1 @if($x=($o->values->search($value) === FALSE)) border-danger @endif" type="text" name="{{ $o->name_lc }}[]" value="{{ $value }}" @if($x)placeholder="{{ Arr::get($o->values,$loop->index) }}"@endif>
@else
{{ $value }}<br>
@endif
@endforeach
</div>
</div>
<div class="col-12 col-sm-6 col-lg-4">
@if($o->is_rdn)
<span class="btn btn-sm btn-outline-focus mt-3 mb-3"><i class="fas fa-fw fa-exchange"></i> {{ __('Rename') }}</span>
@elseif($edit && $o->can_addvalues)
<div class="p-0 m-0 addable" id="{{ $o->name_lc }}">
<span class="btn btn-sm btn-outline-primary mt-3 mb-3"><i class="fas fa-fw fa-plus"></i> {{ __('Add Value') }}</span>
</div>
@endif
</div>
</div>

View File

@ -0,0 +1,22 @@
@extends('architect::layouts.app')
{{--
@section('htmlheader_title')
@lang('Home')
@endsection
@section('page_title')
@endsection
@section('page_icon')
@endsection
--}}
@section('main-content')
@include('frames.dn')
@endsection
@section('page-scripts')
<script type="text/javascript">
var basedn = {!! $bases->toJson() !!};
</script>
@append

View File

@ -0,0 +1,9 @@
@extends('architect::layouts.error')
@section('title')
599: @lang('Untrapped Error')
@endsection
@section('content')
{{ $exception->getMessage() }}
@endsection

View File

@ -28,14 +28,44 @@
@endsection
@section('main-content')
@if(session()->has('note'))
<div class="alert alert-info">
<h4 class="alert-heading"><i class="fas fa-fw fa-note-sticky"></i> Note:</h4>
<hr>
<p>{{ session()->pull('note') }}</p>
</div>
@endif
@if(session()->has('success'))
<div class="alert alert-success">
<h4 class="alert-heading"><i class="fas fa-fw fa-thumbs-up"></i> Success!</h4>
<hr>
<p>{{ session()->pull('success') }}</p>
<ul style="list-style-type: square;">
@foreach (session()->pull('updated') as $key => $values)
<li>{{ $key }}: {{ join(',',$values) }}</li>
@endforeach
</ul>
</div>
@endif
@if($errors->any())
<div class="alert alert-danger">
<h4 class="alert-heading"><i class="fas fa-fw fa-thumbs-down"></i> Error?</h4>
<hr>
<ul style="list-style-type: square;">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="main-card mb-3 card">
<div class="card-body">
<div class="card-header-tabs">
<ul class="nav nav-tabs">
<li class="nav-item"><a data-bs-toggle="tab" href="#attributes" class="nav-link active">{{ __('Attributes') }}</a></li>
{{--
<li class="nav-item"><a data-bs-toggle="tab" href="#placeholder" class="nav-link">placeholder</a></li>
--}}
<li class="nav-item"><a data-bs-toggle="tab" href="#internal" class="nav-link">{{ __('Internal') }}</a></li>
@env(['local'])
<li class="nav-item"><a data-bs-toggle="tab" href="#debug" class="nav-link">{{ __('Debug') }}</a></li>
@ -45,6 +75,11 @@
<div class="tab-content">
<!-- All Attributes -->
<div class="tab-pane active" id="attributes" role="tabpanel">
<form id="form-entry" method="POST" action="{{ url('entry/update') }}">
@csrf
<input type="hidden" name="dn" value="{{ $o->getDNSecure() }}">
<div class="row">
<div class="offset-2 col-8">
<table class="table">
@ -63,25 +98,22 @@
</tr>
<tr>
<td class="ps-5">
{!! $ao->deletable() !!}<br>
@if ($ao->can_addvalues)
<span class="p-0 m-0" id="add{{ $ao->name_lc }}"></span>
<span class="btn btn-sm btn-outline-primary mt-3 mb-3"><i class="fas fa-plus"></i> {{ __('Add Value') }}</span>
@endif
<x-attribute :edit="true" :o="$ao"/>
</td>
</tr>
@endforeach
</table>
</div>
</div>
</div>
{{--
<!-- Templates -->
<div class="tab-pane" id="placeholder" role="tabpanel">
<div><i class="fas fa-fw fa-spinner fa-pulse"></i></div>
<div class="row">
<div class="col-12 offset-sm-2 col-sm-4 col-lg-2">
<span id="form-reset" class="btn btn-outline-danger">{{ __('Reset') }}</span>
<span id="form-submit" class="btn btn-success">{{ __('Update') }}</span>
</div>
</div>
</form>
</div>
--}}
<!-- Internal Attributes -->
<div class="tab-pane" id="internal" role="tabpanel">
@ -96,7 +128,7 @@
</tr>
<tr>
<td class="ps-5">
{!! $ao !!}
<x-attribute :edit="false" :o="$ao"/>
</td>
</tr>
@endforeach
@ -116,16 +148,29 @@
</div>
</div>
</div>
{{--
<!-- Add Template -->
<div class="tab-pane" id="addtemplate" role="tabpanel">
<div><i class="fas fa-fw fa-spinner fa-pulse"></i></div>
</div>
--}}
</div>
</div>
</div>
</div>
@endsection
@section('page-scripts')
<script>
$(document).ready(function() {
$('#reset').click(function() {
$('#form-entry')[0].reset();
})
$('#form-submit').click(function() {
$('#form-entry')[0].submit();
})
// Create a new entry when Add Value clicked
$('.addable').click(function(item) {
var cln = $(this).parent().parent().find('input:last').clone();
cln.val('').attr('placeholder',undefined);
cln.appendTo('#'+item.currentTarget.id)
})
});
</script>
@append

View File

@ -38,3 +38,5 @@ Route::get('logout',[LoginController::class,'logout']);
Route::group(['prefix'=>'user'],function() {
Route::get('image',[HomeController::class,'user_image']);
});
Route::post('entry/update',[HomeController::class,'entry_update']);