Start of work on templates - identify templates that apply to existing 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 1m24s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 2m37s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s

This commit is contained in:
Deon George 2025-06-09 14:26:01 +10:00
parent 8602c2b17f
commit 8e8a902c80
11 changed files with 312 additions and 41 deletions

46
app/Classes/Template.php Normal file
View File

@ -0,0 +1,46 @@
<?php
namespace App\Classes;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Storage;
class Template
{
private string $file;
private array $template;
private(set) bool $invalid = FALSE;
private string $reason = '';
public function __construct(string $file)
{
$td = Storage::disk(config('pla.template.dir'));
$this->file = $file;
try {
$this->template = json_decode($td->get($file),null,512,JSON_OBJECT_AS_ARRAY|JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
$this->invalid = TRUE;
$this->reason = $e->getMessage();
}
}
public function __get(string $key): mixed
{
return match ($key) {
'attributes' => array_map('strtolower',array_keys(Arr::get($this->template,$key))),
'objectclasses' => array_map('strtolower',Arr::get($this->template,$key)),
'enabled' => Arr::get($this->template,$key,FALSE),
'icon','regexp' => Arr::get($this->template,$key),
default => throw new \Exception('Unknown key: '.$key),
};
}
public function __toString(): string
{
return $this->invalid ? '' : Arr::get($this->template,'title','No Template Name');
}
}

View File

@ -4,9 +4,12 @@ namespace App\Ldap;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use LdapRecord\Support\Arr; use LdapRecord\Support\Arr;
use LdapRecord\Models\Model; use LdapRecord\Models\Model;
use App\Classes\Template;
use App\Classes\LDAP\Attribute; use App\Classes\LDAP\Attribute;
use App\Classes\LDAP\Attribute\Factory; use App\Classes\LDAP\Attribute\Factory;
use App\Classes\LDAP\Export\LDIF; use App\Classes\LDAP\Export\LDIF;
@ -39,9 +42,19 @@ class Entry extends Model
public function __construct(array $attributes = []) public function __construct(array $attributes = [])
{ {
$this->objects = collect(); $this->objects = collect();
$this->templates = collect(['default'=>__('LDAP Entry')]);
parent::__construct($attributes); parent::__construct($attributes);
// Load any templates
$x = Storage::disk(config('pla.template.dir'));
$this->templates = collect();
foreach (array_filter($x->files(),fn($item)=>Str::endsWith($item,'.json')) as $file)
$this->templates->put($file,new Template($file));
$this->templates = $this->templates
->filter(fn($item)=>(! $item->invalid) && $item->enabled)
->sortBy(fn($item)=>$item);
} }
public function discardChanges(): static public function discardChanges(): static
@ -131,6 +144,13 @@ class Entry extends Model
$this->objects = collect(); $this->objects = collect();
} }
// Filter out our templates specific for this entry
if ($this->dn && (! in_array(strtolower($this->dn),['cn=subschema']))) {
$this->templates = $this->templates
->filter(fn($item)=>(! $item->regexp) || preg_match($item->regexp,$this->dn))
->filter(fn($item)=>! count(array_diff($item->objectclasses,array_map('strtolower',Arr::get($this->attributes,'objectclass')))));
}
return $this; return $this;
} }
@ -531,4 +551,9 @@ class Entry extends Model
$this->rdnbase = $bdn; $this->rdnbase = $bdn;
} }
public function template(string $item): Template|Null
{
return Arr::get($this->templates,$item);
}
} }

10
config/filesystems.php Normal file
View File

@ -0,0 +1,10 @@
<?php
return [
'disks' => [
'templates' => [
'driver' => 'local',
'root' => base_path(env('LDAP_TEMPLATE_DIR','templates')),
],
],
];

View File

@ -76,4 +76,8 @@ return [
'attr' => [env('LDAP_LOGIN_ATTR','uid') => env('LDAP_LOGIN_ATTR_DESC','User ID')], // Attribute used to find user for login 'attr' => [env('LDAP_LOGIN_ATTR','uid') => env('LDAP_LOGIN_ATTR_DESC','User ID')], // Attribute used to find user for login
'objectclass' => explode(',',env('LDAP_LOGIN_OBJECTCLASS', 'posixAccount')), // Objectclass that users must contain to login 'objectclass' => explode(',',env('LDAP_LOGIN_OBJECTCLASS', 'posixAccount')), // Objectclass that users must contain to login
], ],
'template' => [
'dir' => env('LDAP_TEMPLATE_DRIVER','templates'),
],
]; ];

View File

@ -0,0 +1,17 @@
<!-- $template=Template -->
<form id="template-edit" method="POST" class="needs-validation" action="{{ url('entry/update/pending') }}" novalidate readonly>
@csrf
<input type="hidden" name="dn" value="">
<div class="card-body">
<div class="tab-content">
@php($up=(session()->pull('updated') ?: collect()))
@php($attributes=$o->template($template)?->attributes)
@foreach($o->getVisibleAttributes()->filter(fn($item)=>in_array($item,$attributes)) as $ao)
<x-attribute-type :o="$ao" :edit="TRUE" :new="FALSE" :updated="$up->contains($ao->name_lc)"/>
@endforeach
</div>
</div>
</form>

View File

@ -74,53 +74,49 @@
<div class="col-12"> <div class="col-12">
<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">
<!-- It is assumed that the entry has atleast 1 template "default" --> <!-- If we have templates that cover this entry -->
@if($o->templates->count() > 1) @foreach($o->templates as $template => $name)
@foreach($o->templates as $template => $name) <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}}" @class(['btn','btn-outline-focus','active'=>$loop->index === 0])>{{ $name }}</span> @endforeach
@endforeach @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>
@endif @endif
</div> </div>
</div> </div>
<div class="tab-content"> <div class="tab-content">
@foreach($o->templates as $template => $name) @foreach($o->templates as $template => $name)
@switch($template) <div @class(['tab-pane','active'=>$loop->index === 0]) id="template-{{$template}}" role="tabpanel">
@case('default') @include('fragment.template.dn',['template'=>$template])
<div @class(['tab-pane','active'=>$loop->index === 0]) id="template-{{$template}}" role="tabpanel"> </div>
<form id="dn-edit" method="POST" class="needs-validation" action="{{ url('entry/update/pending') }}" novalidate readonly>
@csrf
<input type="hidden" name="dn" value="">
<div class="card-body">
<div class="tab-content">
@php($up=(session()->pull('updated') ?: collect()))
@foreach($o->getVisibleAttributes() as $ao)
<x-attribute-type :o="$ao" :edit="TRUE" :new="FALSE" :updated="$up->contains($ao->name_lc)"/>
@endforeach
@include('fragment.dn.add_attr')
</div>
</div>
</form>
</div>
@break
@default
<div @class(['tab-pane','active'=>$loop->index === 0]) id="template-{{$template}}" role="tabpanel">
<p>{{$name}}</p>
</div>
@endswitch
@endforeach @endforeach
</div>
</div>
</div>
<div class="row d-none pt-3"> <div @class(['tab-pane','active'=>(! $o->templates->count())]) id="template-default" role="tabpanel">
<div class="col-12 offset-sm-2 col-sm-4 col-lg-2"> <form id="dn-edit" method="POST" class="needs-validation" action="{{ url('entry/update/pending') }}" novalidate readonly>
<x-form.reset form="dn-edit"/> @csrf
<x-form.submit :action="__('Update')" form="dn-edit"/>
<input type="hidden" name="dn" value="">
<div class="card-body">
<div class="tab-content">
@php($up=(session()->pull('updated') ?: collect()))
@foreach($o->getVisibleAttributes() as $ao)
<x-attribute-type :o="$ao" :edit="TRUE" :new="FALSE" :updated="$up->contains($ao->name_lc)"/>
@endforeach
@include('fragment.dn.add_attr')
</div>
</div>
</form>
<div class="row d-none pt-3">
<div class="col-11 text-end">
<x-form.reset form="dn-edit"/>
<x-form.submit :action="__('Update')" form="dn-edit"/>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -162,7 +158,6 @@
// Find all input items and turn off readonly // Find all input items and turn off readonly
$('input.form-control').each(function() { $('input.form-control').each(function() {
// Except for objectClass - @todo show an "X" instead
if ($(this)[0].name.match(/^objectclass/)) if ($(this)[0].name.match(/^objectclass/))
return; return;

22
templates/dns_domain.json Normal file
View File

@ -0,0 +1,22 @@
{
"title": "Generic: DNS Entry",
"description": "New DNS Entry",
"enabled": true,
"icon": "fa-globe",
"rdn": "dc",
"objectclasses": [
"dnsDomain"
],
"attributes": {
"dc": {
"display": "Domain Component",
"order": 1
},
"associatedDomain": {
"display": "Associated Domain",
"order": 2
}
}
}

25
templates/example.json Normal file
View File

@ -0,0 +1,25 @@
{
"title": "Example entry",
"description": "This is the description",
"enabled": false,
"icon": "fa-star-of-life",
"rdn": "o",
"regexp": "/^$/",
"objectclasses": [
"organization"
],
"attributes": {
"attribute1": {
"display": "Attribute 1",
"hint": "This is an example",
"order": 1
},
"attribute2": {
"display": "Attribute 2",
"hint": "This is an example",
"order": 2
}
}
}

20
templates/o.json Normal file
View File

@ -0,0 +1,20 @@
{
"title": "Generic: Organisational",
"description": "New Organisational",
"enabled": true,
"icon": "fa-building",
"rdn": "ou",
"regexp": "/^o=/",
"objectclasses": [
"organization"
],
"attributes": {
"o": {
"display": "Organisation",
"hint": "This is an example",
"order": 1
}
}
}

25
templates/ou.json Normal file
View File

@ -0,0 +1,25 @@
{
"title": "Generic: Organisational Unit",
"description": "New Organisational Unit",
"enabled": true,
"icon": "fa-layer-group",
"rdn": "ou",
"regexp": "/^ou=.+,o=/",
"objectclasses": [
"organizationalUnit"
],
"attributes": {
"ou": {
"display": "Organisational Unit",
"hint": "This is an example",
"order": 1
},
"attribute2": {
"display": "Attribute 2",
"hint": "This is an example",
"order": 2
}
}
}

View File

@ -0,0 +1,82 @@
{
"title": "Generic: User Account",
"description": "New User Account",
"enabled": true,
"icon": "fa-user",
"rdn": "cn",
"regexp": "/,ou=People,o=/",
"objectclasses": [
"inetOrgPerson",
"posixAccount"
],
"attributes": {
"givenName": {
"display": "First Name",
"onchange": [
"=autoFill(cn;%givenName% %sn%)",
"=autoFill(uid;%givenName|0-1/l%%sn/l%)"
],
"order": 1
},
"sn": {
"display": "Last Name",
"onchange": [
"=autoFill(cn;%givenName% %sn%)",
"=autoFill(uid;%givenName|0-1/l%%sn/l%)"
],
"order": 2
},
"cn": {
"display": "Common Name",
"readonly": true,
"order": 3
},
"uid": {
"display": "User ID",
"onchange": [
"=autoFill(homeDirectory;/home/users/%uid%)"
],
"order": 4
},
"userPassword": {
"display": "Password",
"order": 5
},
"uidNumber": {
"display": "UID Number",
"readonly": true,
"value": "=php.GetNextNumber(/;uidNumber)",
"order": 6
},
"gidNumber": {
"display": "UID Number",
"readonly": true,
"onchange": [
"=autoFill(homeDirectory;/home/users/%gidNumber|0-0/T%/%uid|3-%)"
],
"value": "=php.GetNextNumber(/;uidNumber)",
"value": "=php.PickList(/;(&(objectClass=posixGroup));gidNumber;%cn%;;;;cn)",
"order": 7
},
"homeDirectory": {
"display": "Home Directory",
"order": 8
},
"loginShell": {
"display": "Login Shell",
"select": {
"/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": 9
}
}
}