Compare commits

...

7 Commits

Author SHA1 Message Date
aab7f01f52 More work on work on adding/removing objectclasses to an entry, still need to automatically remove attrs from removed objectclasses
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 37s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m26s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 3m28s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2025-02-04 08:56:12 +11:00
061513dafe Fix for 'Couldnt figure out a password hash for {SSHA}' fixes #216 2025-02-04 08:56:12 +11:00
13e645dde0 Schema items no longer used for test/demo 2025-02-04 08:56:12 +11:00
1f1db14ae9 Fix getMissingAttributes(), wasnt evaluating the different objects correctly 2025-02-04 08:56:12 +11:00
b2335e26f2 Consistent calling of btn css, no functional changes 2025-02-04 08:56:12 +11:00
d61685a5b2 Work on adding additional objectclasses to an entry 2025-02-04 08:56:12 +11:00
3a4b0bfe05 Remove hardcoded use of default LDAP server, added example for opendj 2025-02-04 08:56:12 +11:00
26 changed files with 377 additions and 112 deletions

View File

@ -35,8 +35,8 @@ The update to v2 is progressing well - here is a list of work to do and done:
- [X] Validate password is correct - [X] Validate password is correct
- [ ] JpegPhoto Create/Delete - [ ] JpegPhoto Create/Delete
- [X] JpegPhoto Display - [X] JpegPhoto Display
- [ ] ObjectClass Add/Remove - [X] ObjectClass Add/Remove
- [ ] Add additional required attributes (for ObjectClass Addition) - [X] Add additional required attributes (for ObjectClass Addition)
- [ ] Remove existing required attributes (for ObjectClass Removal) - [ ] Remove existing required attributes (for ObjectClass Removal)
- [X] Add additional values to Attributes that support multiple values - [X] Add additional values to Attributes that support multiple values
- [X] Delete extra values for Attributes that support multiple values - [X] Delete extra values for Attributes that support multiple values
@ -52,6 +52,7 @@ The update to v2 is progressing well - here is a list of work to do and done:
Support is known for these LDAP servers: Support is known for these LDAP servers:
- [X] OpenLDAP - [X] OpenLDAP
- [X] OpenDJ
- [ ] Microsoft Active Directory - [ ] Microsoft Active Directory
If there is an LDAP server that you have that you would like to have supported, please open an issue to request it. If there is an LDAP server that you have that you would like to have supported, please open an issue to request it.

View File

@ -33,9 +33,6 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
// Is this attribute the RDN? // Is this attribute the RDN?
protected bool $is_rdn = FALSE; protected bool $is_rdn = FALSE;
// Objectclasses that require this attribute
protected Collection $required_by;
// MIN/MAX number of values // MIN/MAX number of values
protected int $min_values_count = 0; protected int $min_values_count = 0;
protected int $max_values_count = 0; protected int $max_values_count = 0;
@ -102,7 +99,6 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
$this->name = $name; $this->name = $name;
$this->values = collect($values); $this->values = collect($values);
$this->lang_tags = collect(); $this->lang_tags = collect();
$this->required_by = collect();
$this->oldValues = collect($values); $this->oldValues = collect($values);
// No need to load our schema for internal attributes // No need to load our schema for internal attributes
@ -149,6 +145,10 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
'old_values' => $this->oldValues, 'old_values' => $this->oldValues,
// Attribute values // Attribute values
'values' => $this->values, 'values' => $this->values,
// Required by Object Classes
'required_by' => $this->schema->required_by_object_classes,
// Used in Object Classes
'used_in' => $this->schema->used_in_object_classes,
default => throw new \Exception('Unknown key:' . $key), default => throw new \Exception('Unknown key:' . $key),
}; };
@ -296,19 +296,6 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
return Arr::get($this->values,$key); return Arr::get($this->values,$key);
} }
/**
* Set the objectclasses that require this attribute
*
* @param Collection $oc
* @return Collection
*/
public function required_by(Collection $oc): Collection
{
return $this->required_by = ($this->schema
? $oc->intersect($this->schema->required_by_object_classes)
: collect());
}
/** /**
* If this attribute has RFC3866 Language Tags, this will enable those values to be captured * If this attribute has RFC3866 Language Tags, this will enable those values to be captured
* *

View File

@ -10,4 +10,9 @@ final class SHA extends Base
{ {
return sprintf('{%s}%s',self::key,base64_encode(hash('sha1',$password,true))); return sprintf('{%s}%s',self::key,base64_encode(hash('sha1',$password,true)));
} }
public static function subid(string $password): bool
{
return preg_match('/^{'.static::key.'}/',$password);
}
} }

View File

@ -16,4 +16,9 @@ final class SSHA extends Base
{ {
return sprintf('{%s}%s',self::key,$this->salted_hash($password,'sha1',self::salt,$salt)); return sprintf('{%s}%s',self::key,$this->salted_hash($password,'sha1',self::salt,$salt));
} }
public static function subid(string $password): bool
{
return preg_match('/^{'.static::key.'}/',$password);
}
} }

View File

@ -549,17 +549,6 @@ final class AttributeType extends Base {
$this->aliases->forget($x); $this->aliases->forget($x);
} }
/**
* Given a list of object classes, determine if this is a required attribute
*
* @param Collection $oc List of objectclasses to compare.
* @return Collection
*/
public function required_by(Collection $oc): Collection
{
return $oc->diff($this->required_by_object_classes);
}
/** /**
* Sets this attribute's list of aliases. * Sets this attribute's list of aliases.
* *

View File

@ -442,6 +442,16 @@ final class ObjectClass extends Base
return $this->type; return $this->type;
} }
/**
* Return if this objectclass is auxiliary
*
* @return bool
*/
public function isAuxiliary(): bool
{
return $this->type === Server::OC_AUXILIARY;
}
/** /**
* Determine if an array is listed in the may_force attrs * Determine if an array is listed in the may_force attrs
*/ */

View File

@ -59,14 +59,14 @@ final class Server
* Gets the root DN of the specified LDAPServer, or throws an exception if it * Gets the root DN of the specified LDAPServer, or throws an exception if it
* can't find it. * can't find it.
* *
* @param null $connection Return a collection of baseDNs * @param string|null $connection Return a collection of baseDNs
* @param bool $objects Return a collection of Entry Models * @param bool $objects Return a collection of Entry Models
* @return Collection * @return Collection
* @throws ObjectNotFoundException * @throws ObjectNotFoundException
* @testedin GetBaseDNTest::testBaseDNExists(); * @testedin GetBaseDNTest::testBaseDNExists();
* @todo Need to allow for the scenario if the baseDN is not readable by ACLs * @todo Need to allow for the scenario if the baseDN is not readable by ACLs
*/ */
public static function baseDNs(string $connection='default',bool $objects=TRUE): Collection public static function baseDNs(string $connection=NULL,bool $objects=TRUE): Collection
{ {
$cachetime = Carbon::now() $cachetime = Carbon::now()
->addSeconds(Config::get('ldap.cache.time')); ->addSeconds(Config::get('ldap.cache.time'));
@ -360,9 +360,13 @@ final class Server
} }
// Try to get the schema DN from the specified entry. // Try to get the schema DN from the specified entry.
$schema_dn = $this->schemaDN('default'); $schema_dn = $this->schemaDN($this->connection);
$schema = $this->fetch($schema_dn); $schema = $this->fetch($schema_dn);
// If our schema's null, we didnt find it.
if (! $schema)
throw new Exception('Couldnt find schema at:'.$schema_dn);
switch ($item) { switch ($item) {
case 'attributetypes': case 'attributetypes':
Log::debug('Attribute Types'); Log::debug('Attribute Types');

View File

@ -82,4 +82,21 @@ class APIController extends Controller
abort(404); abort(404);
} }
} }
}
/**
* Return the required and additional attributes for an object class
*
* @param Request $request
* @param string $objectclass
* @return array
*/
public function schema_objectclass_attrs(Request $request,string $objectclass): array
{
$oc = config('server')->schema('objectclasses',$objectclass);
return [
'must' => $oc->getMustAttrs()->pluck('name'),
'may' => $oc->getMayAttrs()->pluck('name'),
];
}
}

View File

@ -68,6 +68,28 @@ class HomeController extends Controller
->with('page_actions',$page_actions); ->with('page_actions',$page_actions);
} }
/**
* Render a new attribute view
*
* @param Request $request
* @param string $id
* @return \Closure|\Illuminate\Contracts\View\View|string
*/
public function entry_attr_add(Request $request,string $id)
{
$xx = new \stdClass();
$xx->index = 0;
$x = $request->noheader
? (string)view(sprintf('components.attribute.widget.%s',$id))
->with('o',new Attribute($id,[]))
->with('value',$request->value)
->with('loop',$xx)
: (new AttributeType(new Attribute($id,[]),TRUE,collect($request->oc ?: [])))->render();
return $x;
}
public function entry_export(Request $request,string $id) public function entry_export(Request $request,string $id)
{ {
$dn = Crypt::decryptString($id); $dn = Crypt::decryptString($id);
@ -84,10 +106,33 @@ class HomeController extends Controller
->with('result',new LDIFExport($result)); ->with('result',new LDIFExport($result));
} }
public function entry_newattr(string $id) /**
* Render an available list of objectclasses for an Entry
*
* @param string $id
* @return mixed
*/
public function entry_objectclass_add(string $id)
{ {
$x = new AttributeType(new Attribute($id,[]),TRUE); $dn = Crypt::decryptString($id);
return $x->render(); $o = config('server')->fetch($dn);
$ocs = $o->getObject('objectclass')
->structural
->map(fn($item)=>$item->getParents())
->flatten()
->merge(
config('server')->schema('objectclasses')
->filter(fn($item)=>$item->isAuxiliary())
)
->sortBy(fn($item)=>$item->name);
return $ocs->groupBy(fn($item)=>$item->isStructural())
->map(fn($item,$key) =>
[
'text' => sprintf('%s Object Class',$key ? 'Structural' : 'Auxiliary'),
'children' => $item->map(fn($item)=>['id'=>$item->name,'text'=>$item->name]),
]);
} }
public function entry_password_check(Request $request) public function entry_password_check(Request $request)
@ -126,20 +171,22 @@ class HomeController extends Controller
$o->{$key} = array_filter($value,fn($item)=>! is_null($item)); $o->{$key} = array_filter($value,fn($item)=>! is_null($item));
// We need to process and encrypt the password // We need to process and encrypt the password
$passwords = []; if ($request->userpassword) {
foreach ($request->userpassword as $key => $value) { $passwords = [];
// If the password is still the MD5 of the old password, then it hasnt changed foreach ($request->userpassword as $key => $value) {
if (($old=Arr::get($o->userpassword,$key)) && ($value === md5($old))) { // If the password is still the MD5 of the old password, then it hasnt changed
array_push($passwords,$old); if (($old=Arr::get($o->userpassword,$key)) && ($value === md5($old))) {
continue; array_push($passwords,$old);
} continue;
}
if ($value) { if ($value) {
$type = Arr::get($request->userpassword_hash,$key); $type = Arr::get($request->userpassword_hash,$key);
array_push($passwords,Attribute\Password::hash_id($type)->encode($value)); array_push($passwords,Attribute\Password::hash_id($type)->encode($value));
}
} }
$o->userpassword = $passwords;
} }
$o->userpassword = $passwords;
if (! $o->getDirty()) if (! $o->getDirty())
return back() return back()
@ -294,10 +341,8 @@ class HomeController extends Controller
*/ */
public function schema_frame(Request $request) public function schema_frame(Request $request)
{ {
$s = config('server');
// If an invalid key, we'll 404 // If an invalid key, we'll 404
if ($request->type && $request->key && ($s->schema($request->type)->has($request->key) === FALSE)) if ($request->type && $request->key && (! config('server')->schema($request->type)->has($request->key)))
abort(404); abort(404);
return view('frames.schema') return view('frames.schema')

View File

@ -186,12 +186,8 @@ class Entry extends Model
if (preg_match('/^'.$attribute.'=/i',$this->dn)) if (preg_match('/^'.$attribute.'=/i',$this->dn))
$o->setRDN(); $o->setRDN();
// Set required flag
$o->required_by(collect($this->getAttribute('objectclass')));
// Store our original value to know if this attribute has changed // Store our original value to know if this attribute has changed
if ($x=Arr::get($this->original,$attribute)) $o->oldValues(Arr::get($this->original,$attribute,[]));
$o->oldValues($x);
$result->put($attribute,$o); $result->put($attribute,$o);
} }
@ -290,7 +286,7 @@ class Entry extends Model
public function getMissingAttributes(): Collection public function getMissingAttributes(): Collection
{ {
return $this->getAvailableAttributes() return $this->getAvailableAttributes()
->diff($this->getVisibleAttributes()); ->filter(fn($a)=>(! $this->getVisibleAttributes()->contains(fn($b)=>($a->name === $b->name))));
} }
/** /**

View File

@ -4,21 +4,24 @@ namespace App\View\Components;
use Closure; use Closure;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use Illuminate\View\Component; use Illuminate\View\Component;
use App\Classes\LDAP\Attribute as LDAPAttribute; use App\Classes\LDAP\Attribute as LDAPAttribute;
class AttributeType extends Component class AttributeType extends Component
{ {
public Collection $oc;
public LDAPAttribute $o; public LDAPAttribute $o;
public bool $new; public bool $new;
/** /**
* Create a new component instance. * Create a new component instance.
*/ */
public function __construct(LDAPAttribute $o,bool $new=FALSE) public function __construct(LDAPAttribute $o,bool $new=FALSE,Collection $oc=NULL)
{ {
$this->o = $o; $this->o = $o;
$this->oc = $oc;
$this->new = $new; $this->new = $new;
} }
@ -29,6 +32,7 @@ class AttributeType extends Component
{ {
return view('components.attribute-type') return view('components.attribute-type')
->with('o',$this->o) ->with('o',$this->o)
->with('oc',$this->oc)
->with('new',$this->new); ->with('new',$this->new);
} }
} }

View File

@ -13,7 +13,7 @@ return [
| |
*/ */
'default' => env('LDAP_CONNECTION', 'default'), 'default' => env('LDAP_CONNECTION', 'openldap'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -28,7 +28,7 @@ return [
'connections' => [ 'connections' => [
'default' => [ 'openldap' => [
'hosts' => [env('LDAP_HOST', '127.0.0.1')], 'hosts' => [env('LDAP_HOST', '127.0.0.1')],
'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'), 'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'),
'password' => env('LDAP_PASSWORD', 'secret'), 'password' => env('LDAP_PASSWORD', 'secret'),
@ -40,6 +40,18 @@ return [
'name' => env('LDAP_NAME','LDAP Server'), 'name' => env('LDAP_NAME','LDAP Server'),
], ],
'opendj' => [
'hosts' => ['opendj'],
'username' => 'cn=Directory Manager',
'password' => 'password',
'port' => 1389,
'base_dn' => 'dc=example,dc=com',
'timeout' => env('LDAP_TIMEOUT', 5),
'use_ssl' => env('LDAP_SSL', false),
'use_tls' => env('LDAP_TLS', false),
'name' => 'OpenDJ Server',
],
], ],
/* /*

13
public/css/fixes.css vendored
View File

@ -290,4 +290,17 @@ select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__
.select2-container--bootstrap-5 .select2-selection--single .select2-selection__rendered .select2-selection__placeholder { .select2-container--bootstrap-5 .select2-selection--single .select2-selection__rendered .select2-selection__placeholder {
line-height: 1.0; line-height: 1.0;
font-size: 90%; font-size: 90%;
}
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group {
color: #212529;
font-weight: 800;
}
div#objectclass .input-group-delete {
position: relative;
float: inline-end;
bottom: 30px;
right: 10px;
height: 5px;
} }

View File

@ -57,7 +57,7 @@
</div> </div>
@endif @endif
<div class="float-end"> <div class="float-end">
<button class="btn btn-primary btn-lg">Login</button> <button class="btn btn-lg btn-primary">Login</button>
</div> </div>
</div> </div>
</form> </form>

View File

@ -30,7 +30,7 @@
@if ((isset($page_actions) && $page_actions->contains('edit')) || old()) @if ((isset($page_actions) && $page_actions->contains('edit')) || old())
<li class="nav-item"> <li class="nav-item">
<span class="nav-link pt-0 pb-1"> <span class="nav-link pt-0 pb-1">
<button id="entry-edit" class="p-2 m-0 border-0 btn-transition btn btn-outline-dark w-100 text-start"> <button id="entry-edit" class="p-2 m-0 border-0 btn btn-transition btn-outline-dark w-100 text-start">
<i class="fas fa-fw fa-edit me-2"></i> @lang('Edit') <i class="fas fa-fw fa-edit me-2"></i> @lang('Edit')
</button> </button>
</span> </span>
@ -40,7 +40,7 @@
@if (isset($page_actions) && $page_actions->contains('export')) @if (isset($page_actions) && $page_actions->contains('export'))
<li class="nav-item"> <li class="nav-item">
<a class="nav-link pt-0 pb-1"> <a class="nav-link pt-0 pb-1">
<button type="button" class="p-2 m-0 border-0 btn-transition btn btn-outline-dark w-100 text-start" data-bs-toggle="modal" data-bs-target="#entry_export-modal"> <button type="button" class="p-2 m-0 border-0 btn btn-transition btn-outline-dark w-100 text-start" data-bs-toggle="modal" data-bs-target="#entry_export-modal">
<i class="fas fa-fw fa-file-export me-2"></i> @lang('Export') <i class="fas fa-fw fa-file-export me-2"></i> @lang('Export')
</button> </button>
</a> </a>
@ -50,7 +50,7 @@
@if (isset($page_actions) && $page_actions->contains('copy')) @if (isset($page_actions) && $page_actions->contains('copy'))
<li class="nav-item"> <li class="nav-item">
<a class="nav-link pt-0 pb-1"> <a class="nav-link pt-0 pb-1">
<button class="p-2 m-0 border-0 btn-transition btn btn-outline-dark w-100 text-start"> <button class="p-2 m-0 border-0 btn btn-transition btn-outline-dark w-100 text-start">
<i class="fas fa-fw fa-truck-moving me-2"></i> @lang('Copy or Move') <i class="fas fa-fw fa-truck-moving me-2"></i> @lang('Copy or Move')
</button> </button>
</a> </a>

View File

@ -22,7 +22,7 @@
</div> </div>
<div class="app-header__menu"> <div class="app-header__menu">
<span> <span>
<button type="button" class="btn-icon btn-icon-only btn btn-primary btn-sm mobile-toggle-header-nav"> <button type="button" class="btn-icon btn-icon-only btn btn-sm btn-primary mobile-toggle-header-nav">
<span class="btn-icon-wrapper"> <span class="btn-icon-wrapper">
<i class="fas fa-ellipsis-v fa-w-6"></i> <i class="fas fa-ellipsis-v fa-w-6"></i>
</span> </span>

View File

@ -22,7 +22,7 @@
</div> </div>
<div class="app-header__menu"> <div class="app-header__menu">
<span> <span>
<button type="button" class="btn-icon btn-icon-only btn btn-primary btn-sm mobile-toggle-header-nav"> <button type="button" class="btn-icon btn-icon-only btn btn-sm btn-primary mobile-toggle-header-nav">
<span class="btn-icon-wrapper"> <span class="btn-icon-wrapper">
<i class="fas fa-ellipsis-v fa-w-6"></i> <i class="fas fa-ellipsis-v fa-w-6"></i>
</span> </span>

View File

@ -4,7 +4,7 @@
<div class="col-12 col-sm-10 col-md-8"> <div class="col-12 col-sm-10 col-md-8">
<div class="row"> <div class="row">
<div class="col-12 bg-light text-dark p-2"> <div class="col-12 bg-light text-dark p-2">
<strong><abbr title="{{ $o->description }}">{{ $o->name }}</abbr></strong> <strong><abbr title="{{ $o->description }}" data-attr-name="{{ $o->name_lc }}" data-attr-required="{{ $o->required_by->intersect($oc)->join('|') }}" data-oc="{{ $oc->count() ? $o->required_by->keys()->intersect($oc)->join('|') : $o->used_in->keys()->join('|') }}">{{ $o->name }}</abbr></strong>
<!-- Attribute Hints --> <!-- Attribute Hints -->
<span class="float-end small"> <span class="float-end small">
@foreach($o->hints as $name => $description) @foreach($o->hints as $name => $description)

View File

@ -2,14 +2,7 @@
<x-attribute.layout :edit="$edit" :new="$new" :o="$o"> <x-attribute.layout :edit="$edit" :new="$new" :o="$o">
@foreach (old($o->name_lc,$o->values) as $value) @foreach (old($o->name_lc,$o->values) as $value)
@if ($edit && ($value === NULL || (! $o->isStructural($value)))) @if ($edit && ($value === NULL || (! $o->isStructural($value))))
<div class="input-group has-validation"> <x-attribute.widget.objectclass :o="$o" :edit="$edit" :new="$new" :loop="$loop" :value="$value"/>
<input type="text" @class(['form-control','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index)),'mb-1','border-focus'=>$o->values->contains($value)]) name="{{ $o->name_lc }}[]" value="{{ $value }}" placeholder="{{ ! is_null($x=Arr::get($o->values,$loop->index)) ? $x : '['.__('NEW').']' }}" @readonly(true)>
<div class="invalid-feedback pb-2">
@if($e)
{{ join('|',$e) }}
@endif
</div>
</div>
@else @else
{{ $value }} {{ $value }}
@if ($o->isStructural($value)) @if ($o->isStructural($value))

View File

@ -0,0 +1,12 @@
<span id="objectclass_{{$value}}">
<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 -->
<input type="text" @class(['form-control','is-invalid'=>($e=$errors->get($o->name_lc.'.'.$loop->index)),'mb-1','border-focus'=>$o->values->contains($value)]) name="{{ $o->name_lc }}[]" value="{{ $value }}" placeholder="{{ Arr::get($o->values,$loop->index,'['.__('NEW').']') }}" @readonly(true)>
<div class="invalid-feedback pb-2">
@if($e)
{{ join('|',$e) }}
@endif
</div>
</div>
<span class="input-group-delete"><i class="fas fa-fw fa-xmark"></i></span>
</span>

View File

@ -1,22 +1,193 @@
@php($clone=FALSE)
@if($o->is_rdn) @if($o->is_rdn)
<span class="btn btn-sm btn-outline-focus mt-3"><i class="fas fa-fw fa-exchange"></i> @lang('Rename')</span> <span class="btn btn-sm btn-outline-focus mt-3"><i class="fas fa-fw fa-exchange"></i> @lang('Rename')</span>
@elseif($edit && $o->can_addvalues) @elseif($edit && $o->can_addvalues)
<span class="p-0 m-0"> <span class="p-0 m-0">
<span @class(['btn','btn-sm','btn-outline-primary','mt-3','addable','d-none'=>(! $new)]) id="{{ $o->name_lc }}"><i class="fas fa-fw fa-plus"></i> @lang('Add Value')</span> @switch(get_class($o))
</span> @case('App\Classes\LDAP\Attribute\Binary\JpegPhoto')
@endif <span @class(['btn','btn-sm','btn-outline-primary','mt-3','addable','d-none'=>(! $new)]) id="{{ $o->name_lc }}"><i class="fas fa-fw fa-plus"></i> @lang('Upload JpegPhoto')</span>
@section('page-scripts') @break
@if($edit && $o->can_addvalues)
<script type="text/javascript"> @case('App\Classes\LDAP\Attribute\ObjectClass')
$(document).ready(function() { <button type="button" @class(['btn','btn-sm','btn-outline-primary','mt-3','addable','d-none'=>(! $new)]) data-bs-toggle="modal" data-bs-target="#new_objectclass-modal"><i class="fas fa-fw fa-plus"></i> @lang('Add Objectclass')</button>
// Create a new entry when Add Value clicked
$('#{{ $o->name_lc }}.addable').click(function (item) { <!-- NEW OBJECT CLASS -->
var cln = $(this).parent().parent().find('input:last').parent().clone(); <div class="modal fade" id="new_objectclass-modal" tabindex="-1" aria-labelledby="new_objectclass-label" aria-hidden="true" data-bs-backdrop="static">
cln.find('input:last').attr('value','').attr('placeholder', '[@lang('NEW')]'); <div class="modal-dialog modal-lg modal-fullscreen-lg-down">
cln.appendTo('#' + item.currentTarget.id) <div class="modal-content">
}); <div class="modal-header">
}); <h1 class="modal-title fs-5" id="new_objectclass-label">New Object Class</h1>
</script> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
@endif </div>
@append
<div class="modal-body">
<x-form.select id="newoc" label="Select from..."/>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-primary" data-bs-dismiss="modal">Next</button>
{{--
<button type="button" class="btn btn-sm btn-primary" data-bs-dismiss="modal"><i class="fas fa-fw fa-spinner fa-spin d-none"></i> Next</button>
--}}
</div>
</div>
</div>
</div>
@section('page-scripts')
<script type="text/javascript">
$(document).ready(function() {
var added_oc = []; // Object classes being added to this entry
// Show our ObjectClass modal so that we can add more objectclasses
$('#new_objectclass-modal').on('shown.bs.modal',function() {
$.ajax({
type: 'POST',
success: function(data) {
$('select#newoc').select2({
dropdownParent: $('#new_objectclass-modal'),
theme: 'bootstrap-5',
allowClear: true,
multiple: true,
data: data,
});
},
error: function(e) {
if (e.status != 412)
alert('That didnt work? Please try again....');
},
url: '{{ url('entry/objectclass/add') }}/'+dn,
cache: false
});
})
// When the ObjectClass modal is closed, process what was selected
$('#new_objectclass-modal').on('hide.bs.modal',function() {
var c = {{ $o->values->count() }}; // @todo do we need this?
var newadded = $('select#newoc').val();
// If nothing selected, we dont have anything to do
if (added_oc.sort().join('|') == newadded.sort().join('|'))
return;
var attrs = $('[data-attr-name]').map(function() {
return $(this).data('attrName');
});
// Find out what was selected, and add them
newadded.forEach(function (item) {
if (added_oc.indexOf(item) !== -1)
return;
// Add attribute to the page
$.ajax({
type: 'POST',
beforeSend: function() {},
success: function(data) {
$('#{{ $o->name_lc }}').append(data);
},
error: function(e) {
if (e.status != 412)
alert('That didnt work? Please try again....');
},
url: '{{ url('entry/attr/add',[$o->name_lc]) }}',
data: {
noheader: true,
value: item,
objectclasses: oc,
loop: c++, // @todo can we omit loop and c
},
cache: false
});
$.ajax({
type: 'POST',
beforeSend: function() {},
success: function(data) {
// Render any must attributes
if (data.must.length) {
data.must.forEach(function(item) {
// Add attribute to the page
$.ajax({
type: 'POST',
beforeSend: function() {},
success: function(data) {
$('#newattrs').append(data);
},
error: function(e) {
if (e.status != 412)
alert('That didnt work? Please try again....');
},
url: '{{ url('entry/attr/add') }}/'+item,
data: {
value: item,
objectclasses: oc,
loop: c++, // @todo can we omit loop and c
},
cache: false
});
})
}
// Add attributes to "Add new Attribute" that are now available
if (data.may.length) {
var newattr = $('select#newattr');
// @todo It might be nice to resort this options
data.may.forEach(function(item) {
newattr.append(new Option(item,item,false,false));
});
}
},
error: function(e) {
if (e.status != 412)
alert('That didnt work? Please try again....');
},
url: '{{ url('api/schema/objectclass/attrs') }}/'+item,
cache: false
});
});
// Loop through added_oc, and remove anything not in newadded
added_oc.forEach(function(item) {
if (newadded.indexOf(item) === -1) {
$('span#objectclass_'+item).empty();
// @todo remove any required attributes that are no longer defined as a result of removing this OC
console.log('Remove required attributes of:'+item);
// @todo Remove attributes from "Add new Attribute" that are no longer available
console.log('Remove additional attributes of:'+item);
}
});
added_oc = newadded;
});
});
</script>
@append
@break
@case('App\Classes\LDAP\Attribute')
@default
@php($clone=TRUE)
<span @class(['btn','btn-sm','btn-outline-primary','mt-3','addable','d-none'=>(! $new)]) id="{{ $o->name_lc }}"><i class="fas fa-fw fa-plus"></i> @lang('Add Value')</span>
@section('page-scripts')
@if($clone && $edit && $o->can_addvalues)
<script type="text/javascript">
$(document).ready(function() {
// Create a new entry when Add Value clicked
$('#{{ $o->name_lc }}.addable').click(function (item) {
var cln = $(this).parent().parent().find('input:last').parent().clone();
cln.find('input:last').attr('value','').attr('placeholder', '[@lang('NEW')]');
cln.appendTo('#' + item.currentTarget.id)
});
});
</script>
@endif
@append
@endswitch
</span>
@endif

View File

@ -3,7 +3,7 @@
<input type="hidden" id="{{ $id ?? $name }}_disabled" name="{{ $name }}" value="" disabled> <input type="hidden" id="{{ $id ?? $name }}_disabled" name="{{ $name }}" value="" disabled>
@endisset @endisset
<select style="width: 80%" class="form-select @isset($name)@error((! empty($old)) ? $old : $name) is-invalid @enderror @endisset" id="{{ $id ?? $name }}" @isset($name)name="{{ $name }}"@endisset @required(isset($required) && $required) @disabled(isset($disabled) && $disabled)> <select style="width: 80%" class="form-select @isset($name)@error((! empty($old)) ? $old : $name) is-invalid @enderror @endisset" id="{{ $id ?? $name }}" @isset($name)name="{{ $name }}"@endisset @required(isset($required) && $required) @disabled(isset($disabled) && $disabled)>
@if(empty($value) || isset($addnew) || isset($choose)) @if((empty($value) && ! empty($options)) || isset($addnew) || isset($choose))
<option value=""></option> <option value=""></option>
@isset($addnew) @isset($addnew)
<option value="new">{{ $addnew ?: 'Add New' }}</option> <option value="new">{{ $addnew ?: 'Add New' }}</option>

View File

@ -31,7 +31,7 @@
<input type="hidden" name="dn" value=""> <input type="hidden" name="dn" value="">
@foreach ($o->getVisibleAttributes() as $ao) @foreach ($o->getVisibleAttributes() as $ao)
<x-attribute-type :edit="true" :o="$ao"/> <x-attribute-type :edit="true" :o="$ao" :oc="collect($o->objectclass)"/>
@endforeach @endforeach
<div id="newattrs"></div> <div id="newattrs"></div>
@ -126,8 +126,8 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary btn-sm" id="entry_export-download">Download</button> <button type="button" class="btn btn-sm btn-primary" id="entry_export-download">Download</button>
</div> </div>
</div> </div>
</div> </div>
@ -161,8 +161,8 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary btn-sm" id="userpassword_check-submit"><i class="fas fa-fw fa-spinner fa-spin d-none"></i> Check</button> <button type="button" class="btn btn-sm btn-primary" id="userpassword_check-submit"><i class="fas fa-fw fa-spinner fa-spin d-none"></i> Check</button>
</div> </div>
</div> </div>
</div> </div>
@ -173,6 +173,7 @@
@section('page-scripts') @section('page-scripts')
<script type="text/javascript"> <script type="text/javascript">
var dn = '{{ $o->getDNSecure() }}'; var dn = '{{ $o->getDNSecure() }}';
var oc = {!! $o->getObject('objectclass')->values !!};
function download(filename,text) { function download(filename,text) {
var element = document.createElement('a'); var element = document.createElement('a');
@ -193,6 +194,10 @@
// 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/))
return;
$(this).attr('readonly',false); $(this).attr('readonly',false);
}); });
@ -213,7 +218,7 @@
$(document).ready(function() { $(document).ready(function() {
$('#newattr').on('change',function(item) { $('#newattr').on('change',function(item) {
$.ajax({ $.ajax({
type: 'GET', type: 'POST',
beforeSend: function() {}, beforeSend: function() {},
success: function(data) { success: function(data) {
$('#newattrs').append(data); $('#newattrs').append(data);
@ -222,7 +227,10 @@
if (e.status != 412) if (e.status != 412)
alert('That didnt work? Please try again....'); alert('That didnt work? Please try again....');
}, },
url: '{{ url('entry/newattr') }}/'+item.target.value, url: '{{ url('entry/attr/add') }}/'+item.target.value,
data: {
objectclasses: oc,
},
cache: false cache: false
}); });

View File

@ -19,6 +19,7 @@ Route::group([],function() {
Route::get('bases',[APIController::class,'bases']); Route::get('bases',[APIController::class,'bases']);
Route::get('children',[APIController::class,'children']); Route::get('children',[APIController::class,'children']);
Route::post('schema/view',[APIController::class,'schema_view']); Route::post('schema/view',[APIController::class,'schema_view']);
Route::post('schema/objectclass/attrs/{id}',[APIController::class,'schema_objectclass_attrs']);
}); });
Route::group(['middleware'=>'auth:api','prefix'=>'user'],function() { Route::group(['middleware'=>'auth:api','prefix'=>'user'],function() {

View File

@ -38,10 +38,11 @@ Route::group(['prefix'=>'user'],function() {
Route::get('image',[HomeController::class,'user_image']); Route::get('image',[HomeController::class,'user_image']);
}); });
Route::post('entry/update/commit',[HomeController::class,'entry_update']);
Route::post('entry/update/pending',[HomeController::class,'entry_pending_update']);
Route::get('entry/newattr/{id}',[HomeController::class,'entry_newattr']);
Route::get('entry/export/{id}',[HomeController::class,'entry_export']); Route::get('entry/export/{id}',[HomeController::class,'entry_export']);
Route::post('entry/password/check/',[HomeController::class,'entry_password_check']); Route::post('entry/password/check/',[HomeController::class,'entry_password_check']);
Route::post('entry/attr/add/{id}',[HomeController::class,'entry_attr_add']);
Route::post('entry/objectclass/add/{id}',[HomeController::class,'entry_objectclass_add']);
Route::post('entry/update/commit',[HomeController::class,'entry_update']);
Route::post('entry/update/pending',[HomeController::class,'entry_pending_update']);
Route::post('import/process/{type}',[HomeController::class,'import']); Route::post('import/process/{type}',[HomeController::class,'import']);

View File

@ -1,9 +0,0 @@
# At the moment we want to override osixia/ldap to enable anonymous reads
dn: olcDatabase={1}{{ LDAP_BACKEND }},cn=config
changetype: modify
delete: olcAccess
-
add: olcAccess
olcAccess: to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break
olcAccess: to attrs=userPassword,shadowLastChange by self read by dn="cn=admin,{{ LDAP_BASE_DN }}" write by anonymous read by * read
olcAccess: to * by self read by dn="cn=admin,{{ LDAP_BASE_DN }}" write by * read