Compare commits

...

6 Commits

Author SHA1 Message Date
16a1f85a79 Release v2.1.3
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m21s
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m33s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-05-04 19:06:02 +10:00
f8d7819153 Fix for ARGON2 passwords, they shouldnt be base64 encoded. Fixes #316 2025-05-04 00:01:40 +10:00
75dbb37d8b $langtag doesnt need to be passed to x-attribute.layout. Fix adding new userpassword. Only show Add Value when the Attribute has atleast 1 old value 2025-05-04 00:01:40 +10:00
309fe83c98 LDAP server errors were not being displayed when used with ->withErrors(), so created a new component x-failed 2025-05-04 00:01:40 +10:00
ffb98631a6 Implemented search via the top menu 2025-05-03 20:27:16 +10:00
be69e22867 Update home page 2025-05-03 20:27:16 +10:00
22 changed files with 230 additions and 47 deletions

View File

@ -3,7 +3,7 @@ run-name: ${{ gitea.actor }} Building Docker Image 🐳
on: [push]
env:
DOCKER_HOST: tcp://127.0.0.1:2375
ASSETS: c2780a3
ASSETS: 10eca55
jobs:
test:

View File

@ -38,6 +38,7 @@ The update to v2 is progressing well - here is a list of work to do and done:
- [X] Support different password hash options
- [X] Validate password is correct
- [ ] JpegPhoto Create/Delete
- [ ] Binary attribute upload
- [X] JpegPhoto Display
- [X] ObjectClass Add/Remove
- [X] Add additional required attributes (for ObjectClass Addition)
@ -53,7 +54,7 @@ The update to v2 is progressing well - here is a list of work to do and done:
- [X] Export entries as an LDAP
- [X] Import LDIF
- [X] Schema Browser
- [ ] Searching
- [X] Searching
- [ ] Enforcing attribute uniqueness
- [ ] Is there something missing?

View File

@ -18,6 +18,7 @@ final class Password extends Attribute
use MD5Updates;
protected(set) bool $no_attr_tags = TRUE;
protected(set) int $max_values_count = 1;
private const password_helpers = 'Classes/LDAP/Attribute/Password';
public const commands = 'App\\Classes\\LDAP\\Attribute\\Password\\';

View File

@ -10,16 +10,16 @@ final class Argon2i extends Base
public static function subid(string $password): bool
{
return str_starts_with(base64_decode(self::password($password)),self::identifier.'$');
return str_starts_with(self::password($password),self::identifier.'$');
}
public function compare(string $source,string $compare): bool
{
return password_verify($compare,base64_decode($this->password($source)));
return password_verify($compare,$this->password($source));
}
public function encode(string $password): string
{
return sprintf('{%s}%s',self::key,base64_encode(password_hash($password,PASSWORD_ARGON2I)));
return sprintf('{%s}%s',self::key,password_hash($password,PASSWORD_ARGON2I));
}
}

View File

@ -127,7 +127,7 @@ class HomeController extends Controller
case 50:
return Redirect::to('/')
->withInput()
->withErrors(sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
->with('failed',sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
default:
abort(599,$e->getDetailedError()->getErrorMessage());
@ -135,14 +135,14 @@ class HomeController extends Controller
// @todo when we create an entry, and it already exists, enable a redirect to it
} catch (LdapRecordException $e) {
return Redirect::back()
->withInput()
->withErrors(sprintf('%s: %s - %s: %s',
__('LDAP Server Error Code'),
$e->getDetailedError()->getErrorCode(),
__($e->getDetailedError()->getErrorMessage()),
$e->getDetailedError()->getDiagnosticMessage(),
));
return Redirect::back()
->withInput()
->with('failed',sprintf('%s: %s - %s: %s',
__('LDAP Server Error Code'),
$e->getDetailedError()->getErrorCode(),
__($e->getDetailedError()->getErrorMessage()),
$e->getDetailedError()->getDiagnosticMessage(),
));
}
return Redirect::to('/')
@ -165,7 +165,7 @@ class HomeController extends Controller
case 50:
return Redirect::to('/')
->withInput()
->withErrors(sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
->with('failed',sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
default:
abort(599,$e->getDetailedError()->getErrorMessage());
@ -178,7 +178,7 @@ class HomeController extends Controller
case 8:
return Redirect::to('/')
->withInput()
->withErrors(sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
->with('failed',sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
default:
abort(599,$e->getDetailedError()->getErrorMessage());
@ -335,21 +335,21 @@ class HomeController extends Controller
case 50:
return Redirect::to('/')
->withInput()
->withErrors(sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
->with('failed',sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
default:
abort(599,$e->getDetailedError()->getErrorMessage());
}
} catch (LdapRecordException $e) {
return Redirect::to('/')
->withInput()
->withErrors(sprintf('%s: %s - %s: %s',
__('LDAP Server Error Code'),
$e->getDetailedError()->getErrorCode(),
__($e->getDetailedError()->getErrorMessage()),
$e->getDetailedError()->getDiagnosticMessage(),
));
return Redirect::to('/')
->withInput()
->with('failed',sprintf('%s: %s - %s: %s',
__('LDAP Server Error Code'),
$e->getDetailedError()->getErrorCode(),
__($e->getDetailedError()->getErrorMessage()),
$e->getDetailedError()->getDiagnosticMessage(),
));
}
return Redirect::to('/')

View File

@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Str;
use App\Ldap\Entry;
class SearchController extends Controller
{
public function search(Request $request): Collection
{
$so = config('server');
// We are searching for a value
if (strpos($request->term,'=')) {
list($attr,$value) = explode('=',$request->term,2);
$value = trim($value);
$result = collect();
foreach ($so->baseDNs() as $base) {
$search = (new Entry)
->in($base);
$search = ($x=Str::startsWith($value,'*'))
? $search->whereEndsWith($attr,substr($value,1))
: $search->whereStartsWith($attr,$value);
$result = $result->merge($search->get());
}
return $result
->map(fn($item)=>[
'name'=>$item->getDN(),
'value'=>Crypt::encryptString($item->getDN()),
'category'=>sprintf('%s: [%s=%s%s]',__('Result'),$attr,$value,($x ? '' : '*'))
]);
// We are searching for an attribute
} else {
$attrs = $so
->schema('attributetypes')
->sortBy('name')
->filter(fn($item)=>Str::contains($item->name_lc,$request->term));
return $attrs
->map(fn($item)=>[
'name'=>$item->name,
'value'=>'',
'category'=>__('Select attribute...')
])
->values();
}
}
}

View File

@ -1 +1 @@
v2.1.2-rel
v2.1.3-rel

View File

@ -33,8 +33,12 @@
<div class="app-header-left">
<div class="search-wrapper">
<div class="input-holder">
<input type="text" class="search-input" placeholder="Type to search">
<button class="search-icon"><span></span></button>
<input type="text" class="search-input" id="search" placeholder="Type to search">
<button class="search-icon">
<span></span>
<div id="searching" class="d-none"><i class="fas fa-fw fa-spinner fa-pulse text-light"></i></div>
</button>
<div id="search_results" style="height: 300px; overflow: scroll"></div>
</div>
<button class="btn-close"></button>
</div>
@ -160,6 +164,13 @@
</div>
@section('page-scripts')
<style>
#search_results ul.typeahead.dropdown-menu {
overflow: scroll;
max-height: 300px;
}
</style>
<script type="text/javascript">
$(document).ready(function() {
$('button[id^="link-"]').on('click',function(item) {
@ -190,6 +201,70 @@
return false;
});
$('.search-wrapper input[id="search"]').typeahead({
autoSelect: false,
scrollHeight: 10,
theme: 'bootstrap5',
delay: 500,
minLength: 2,
items: {{ $search_limit ?? 100 }},
selectOnBlur: false,
appendTo: "#search_results",
source: function(query,process) {
search('{{ url('search') }}',query,process);
},
// Disable sorting and just return the items (items should be sorted by the ajax method)
sorter: function(items) {
return items;
},
matcher: function() { return true; },
// Disable sorting and just return the items (items should by the ajax method)
updater: function(item) {
// If item has a data value, then we'll use that
if (item.data && item.data.length)
return item.data;
if (! item.value)
return item.name+'=';
location.replace('/#'+item.value);
location.reload();
return '';
},
})
.on('keyup keypress',function(event) {
var key = event.keyCode || event.which;
if (key === 13) {
event.preventDefault();
return false;
}
});
});
var search = _.debounce(function(url,query,process){
$.ajax({
url : url,
type : 'POST',
data : 'term=' + query,
dataType : 'JSON',
async : true,
cache : false,
beforeSend : function() {
$('.search-wrapper div#searching').removeClass('d-none');
$('.search-wrapper .search-icon span').addClass('d-none');
},
success : function(data) {
// if json is null, means no match, won't do again.
if(data==null || (data.length===0)) return;
process(data);
},
complete : function() {
$('.search-wrapper div#searching').addClass('d-none');
$('.search-wrapper .search-icon span').removeClass('d-none');
}
})
}, 500);
</script>
@append

View File

@ -3,7 +3,7 @@
<!-- $o=Attribute::class -->
<x-attribute.layout :edit="$edit=($edit ?? FALSE)" :new="$new=($new ?? FALSE)" :o="$o">
<div class="col-12">
@foreach(Arr::get(old($o->name_lc,[($langtag=($langtag ?? Entry::TAG_NOTAG))=>$new ? [NULL] : $o->tagValues($langtag)]),$langtag,[]) as $key => $value)
@foreach(Arr::get(old($o->name_lc,[$langtag=>$new ? [NULL] : $o->tagValues($langtag)]),$langtag,[]) as $key => $value)
@if($edit && (! $o->is_rdn))
<div class="input-group has-validation">
<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),'bg-success-subtle'=>$updated]) name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ $value }}" placeholder="{{ ! is_null($x=$tv->get($loop->index)) ? $x : '['.__('NEW').']' }}" @readonly(! $new) @disabled($o->isDynamic())>

View File

@ -1,6 +1,6 @@
<!-- @todo We are not handling redirect backs yet with updated photos -->
<!-- $o=Binary\JpegPhoto::class -->
<x-attribute.layout :edit="$edit" :new="$new" :o="$o" :langtag="$langtag">
<x-attribute.layout :edit="$edit" :new="$new" :o="$o">
<table class="table table-borderless p-0 m-0">
@foreach($o->tagValuesOld() as $key => $value)
<tr>

View File

@ -1,10 +1,10 @@
<!-- @todo We are not handling redirect backs yet with updated passwords -->
<!-- $o=KrbPrincipleKey::class -->
<x-attribute.layout :edit="$edit" :new="$new" :o="$o" :langtag="$langtag">
<x-attribute.layout :edit="$edit" :new="$new" :o="$o">
@foreach($o->tagValuesOld($langtag) as $key => $value)
@if($edit)
<div class="input-group has-validation mb-3">
<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),'bg-success-subtle'=>$updated]) 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),'bg-success-subtle'=>$updated]) name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ md5($value) }}" @readonly(! $new)>
<div class="invalid-feedback pb-2">
@if($e)

View File

@ -1,5 +1,5 @@
<!-- $o=Attribute/ObjectClass::class -->
<x-attribute.layout :edit="$edit" :new="$new" :o="$o" :langtag="$langtag">
<x-attribute.layout :edit="$edit" :new="$new" :o="$o">
@foreach(Arr::get(old($o->name_lc,[$langtag=>$new ? [NULL] : $o->tagValues($langtag)]),$langtag,[]) as $key => $value)
@if($edit)
<x-attribute.widget.objectclass :o="$o" :edit="$edit" :new="$new" :langtag="$langtag" :updated="$updated" :value="$value" :loop="$loop" />

View File

@ -1,11 +1,11 @@
<!-- @todo We are not handling redirect backs yet with updated passwords -->
<!-- $o=Password::class -->
<x-attribute.layout :edit="$edit" :new="$new" :o="$o" :langtag="$langtag">
@foreach($o->tagValuesOld($langtag) as $key => $value)
<x-attribute.layout :edit="$edit" :new="$new" :o="$o">
@foreach(Arr::get(old($o->name_lc,[$langtag=>$new ? [NULL] : $o->tagValues($langtag)]),$langtag,[]) as $key => $value)
@if($edit)
<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"/>
<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),'bg-success-subtle'=>$updated]) name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ md5($value) }}" @readonly(true)>
<x-form.select id="userpassword_hash_{{$loop->index}}" name="userpassword_hash[{{ $langtag }}][]" :value="$o->hash($new ? '' : $value)->id()" :options="$helpers" allowclear="false" :disabled="! $new"/>
<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),'bg-success-subtle'=>$updated]) name="{{ $o->name_lc }}[{{ $langtag }}][]" value="{{ md5($value) }}" @readonly(! $new)>
<div class="invalid-feedback pb-2">
@if($e)

View File

@ -255,7 +255,9 @@
@default
@if($o->isDynamic()) @break @endif
@php($clone=TRUE)
<span @class(['btn','btn-sm','btn-outline-primary','mt-3','addable','d-none'=>(! $new)]) id="{{ $o->name }}-addnew"><i class="fas fa-fw fa-plus"></i> @lang('Add Value')</span>
@if($o->values_old->count())
<span @class(['btn','btn-sm','btn-outline-primary','mt-3','addable','d-none'=>(! $new)]) id="{{ $o->name }}-addnew"><i class="fas fa-fw fa-plus"></i> @lang('Add Value')</span>
@endif
@section('page-scripts')
@if($clone && $edit && $o->can_addvalues)

View File

@ -0,0 +1,5 @@
@if(session()->has('failed'))
<div class="alert alert-danger p-2">
<p class="m-0"><i class="fas fa-fw fa-thumbs-down"></i> {{ session()->pull('failed') }}</p>
</div>
@endif

View File

@ -1,7 +1,7 @@
@use(App\Classes\LDAP\Attribute\Certificate)
<!-- $o=Certificate::class -->
<x-attribute.layout :edit="$edit" :new="$new" :o="$o" langtag="binary">
<x-attribute.layout :edit="$edit" :new="$new" :o="$o">
@foreach($o->tagValuesOld('binary') as $key => $value)
<!-- If this attribute is not handle, it'll be an Attribute::class, we'll just render it normally -->
@if(($o instanceof Certificate) && $edit)

View File

@ -1,5 +1,5 @@
<!-- $o=CertificateList::class -->
<x-attribute.layout :edit="$edit" :new="$new" :o="$o" langtag="binary">
<x-attribute.layout :edit="$edit" :new="$new" :o="$o">
@foreach($o->tagValuesOld('binary') as $key => $value)
<!-- If this attribute is not handle, it'll be an Attribute::class, we'll just render it normally -->
<span class="form-control mb-1"><pre class="m-0">{{ $o->render_item_old('binary.'.$key) }}</pre></span>

View File

@ -11,6 +11,7 @@
@section('page_status')
<x-error/>
<x-failed/>
@endsection
@section('main-content')

View File

@ -67,6 +67,7 @@
<x-note/>
<x-error/>
<x-updated/>
<x-failed/>
@endsection
@section('main-content')

View File

@ -23,18 +23,51 @@
</div>
<div class="col-12 col-sm-8">
<h3 class="mb-1">Welcome to phpLDAPadmin</h3>
<h4 class="mb-3"><small>{{ config('app.version') }}</small></h4>
<p>phpLDAPadmin (or PLA for short) is an LDAP data management tool for administrators.</p>
<p>PLA aims to adhere to the LDAP standards so that it can interact with any LDAP server that implements those standards.</p>
<h1 class="mb-2">Welcome to phpLDAPadmin</h1>
<p>phpLDAPadmin (or PLA for short) is an LDAP (Lightweight Directory Access Protocol) data management tool for administrators.</p>
<p>PLA provides an easy-to-use interface for browsing, searching, and modifying data in an LDAP directory. Essentially, it's a user-friendly alternative to using command-line tools for LDAP management.</p>
</div>
</div>
<div class="row">
<div class="col-12">
<hr>
<p>Version 2 is a complete re-write of PLA, leveraging the advancements and modernisation of web tools and methods, libraries since version 1 was released.</p>
<p>You can support this application by letting us know which LDAP server you use (including version and platform).</p>
</div>
</div>
<div class="row">
<div class="col-12 col-sm-4">
<span class="text-dark">
<a class="link-opacity-50 link-opacity-100-hover link-dark" href="https://phpldapadmin.org"><i class="fas fa-fw fa-2x fa-globe me-1"></i></a>
<a class="link-opacity-50 link-opacity-100-hover link-dark" href="https://github.com/leenooks/phpldapadmin"><i class="fab fa-fw fa-2x fa-github me-1"></i></a>
<a class="link-opacity-50 link-opacity-100-hover link-dark" href="https://github.com/leenooks/phpLDAPadmin/discussions"><i class="fas fa-fw fa-2x fa-hand me-1"></i></a>
<a class="link-opacity-50 link-opacity-100-hover link-dark" href="https://github.com/leenooks/phpLDAPadmin/issues"><i class="fas fa-fw fa-2x fa-bug me-1"></i></a>
<a class="link-opacity-50 link-opacity-100-hover link-dark" href="https://hub.docker.com/r/phpldapadmin/phpldapadmin"><i class="fab fa-fw fa-2x fa-docker me-1"></i></a>
</span>
</div>
<div class="col-12 col-sm-8 col-xl-4">
<h5>Key Features and Functionality</h5>
<ul class="list-unstyled">
<li class="ps-0 p-1">
<i class="fas fa-fw fa-globe me-2"></i> Easy To Use Web Interface
</li>
<li class="ps-0 p-1">
<i class="fas fa-fw fa-sitemap me-2"></i> Hierarchical Tree View
</li>
<li class="ps-0 p-1">
<i class="fas fa-fw fa-language me-2"></i> Multi-language Support
</li>
<li class="ps-0 p-1">
<i class="fas fa-fw fa-file-export me-2"></i> LDIF Import/Export
</li>
<li class="ps-0 p-1">
<i class="fas fa-fw fa-clipboard-list me-2"></i> Build on RFC Standards
</li>
<li class="ps-0 p-1">
<i class="fas fa-fw fa-pen-to-square me-2"></i> Open Source
</li>
</ul>
</div>
</div>
</div>

View File

@ -2,7 +2,7 @@
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\{AjaxController,HomeController};
use App\Http\Controllers\{AjaxController,HomeController,SearchController};
use App\Http\Controllers\Auth\LoginController;
use App\Http\Middleware\AllowAnonymous;
@ -27,6 +27,7 @@ Auth::routes([
]);
Route::get('logout',[LoginController::class,'logout']);
Route::post('search',[SearchController::class,'search']);
Route::controller(HomeController::class)->group(function() {
Route::middleware(AllowAnonymous::class)->group(function() {

View File

@ -0,0 +1,4 @@
dn: cn=z-module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: argon2