Compare commits

..

2 Commits

Author SHA1 Message Date
8e8a902c80 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
2025-06-09 14:26:01 +10:00
8602c2b17f Only swap in user's credentials if the requested page is not the logout page. This avoids an issue if the user's credentials are changed during their session, they couldnt log out 2025-06-09 10:31:25 +10:00
12 changed files with 313 additions and 42 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

@ -29,7 +29,7 @@ class SwapinAuthUser
if (! array_key_exists($key,config('ldap.connections')))
abort(599,sprintf('LDAP default server [%s] configuration doesnt exist?',$key));
if (Session::has('username_encrypt') && Session::has('password_encrypt')) {
if (($request->path() !== 'logout') && Session::has('username_encrypt') && Session::has('password_encrypt')) {
Config::set('ldap.connections.'.$key.'.username',Crypt::decryptString(Session::get('username_encrypt')));
Config::set('ldap.connections.'.$key.'.password',Crypt::decryptString(Session::get('password_encrypt')));

View File

@ -4,9 +4,12 @@ namespace App\Ldap;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use LdapRecord\Support\Arr;
use LdapRecord\Models\Model;
use App\Classes\Template;
use App\Classes\LDAP\Attribute;
use App\Classes\LDAP\Attribute\Factory;
use App\Classes\LDAP\Export\LDIF;
@ -39,9 +42,19 @@ class Entry extends Model
public function __construct(array $attributes = [])
{
$this->objects = collect();
$this->templates = collect(['default'=>__('LDAP Entry')]);
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
@ -131,6 +144,13 @@ class Entry extends Model
$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;
}
@ -531,4 +551,9 @@ class Entry extends Model
$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
'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,20 +74,24 @@
<div class="col-12">
<div class="d-flex justify-content-center">
<div role="group" class="btn-group btn-group-sm nav pb-3">
<!-- It is assumed that the entry has atleast 1 template "default" -->
@if($o->templates->count() > 1)
<!-- If we have templates that cover this entry -->
@foreach($o->templates as $template => $name)
<span data-bs-toggle="tab" href="#template-{{$template}}" @class(['btn','btn-outline-focus','active'=>$loop->index === 0])>{{ $name }}</span>
<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>
@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
</div>
</div>
<div class="tab-content">
@foreach($o->templates as $template => $name)
@switch($template)
@case('default')
<div @class(['tab-pane','active'=>$loop->index === 0]) id="template-{{$template}}" role="tabpanel">
@include('fragment.template.dn',['template'=>$template])
</div>
@endforeach
<div @class(['tab-pane','active'=>(! $o->templates->count())]) id="template-default" role="tabpanel">
<form id="dn-edit" method="POST" class="needs-validation" action="{{ url('entry/update/pending') }}" novalidate readonly>
@csrf
@ -104,26 +108,18 @@
</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
</div>
</div>
</div>
<div class="row d-none pt-3">
<div class="col-12 offset-sm-2 col-sm-4 col-lg-2">
<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>
<!-- Internal Attributes -->
<div class="tab-pane mt-3" id="internal" role="tabpanel">
@ -162,7 +158,6 @@
// 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;

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
}
}
}