-
-
Notifications
You must be signed in to change notification settings - Fork 193
#416: Add support for ppolicy pwdReset attribute #443
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
ead18d4
65c305c
d1fc673
6499b2a
6daaaa5
d0bd75c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| <?php | ||
|
|
||
| namespace App\Classes\LDAP\Attribute; | ||
|
|
||
| use Illuminate\Contracts\View\View; | ||
|
|
||
| use App\Classes\LDAP\Attribute; | ||
| use App\Classes\Template; | ||
| use App\Interfaces\{ForceSingleValue,NoAttrTag}; | ||
| use App\Ldap\Entry; | ||
|
|
||
| /** | ||
| * Represents the pwdReset attribute from OpenLDAP ppolicy overlay | ||
| */ | ||
| final class PwdReset extends Attribute implements ForceSingleValue,NoAttrTag | ||
| { | ||
| public function __construct(string $dn, string $name, array $values, array $oc = []) | ||
| { | ||
| parent::__construct($dn, $name, $values, $oc); | ||
| $this->_is_internal = FALSE; | ||
| } | ||
|
|
||
| /** | ||
| * Override properties to handle NULL schema gracefully for this virtual attribute | ||
| */ | ||
| public function __get(string $key): mixed | ||
| { | ||
| // Handle schema-based properties if schema exists | ||
| if ($this->schema !== NULL) { | ||
| switch ($key) { | ||
| case 'description': | ||
| case 'name': | ||
| case 'name_lc': | ||
| case 'is_editable': | ||
| case 'required_by': | ||
| case 'used_in': | ||
| return parent::__get($key); | ||
| } | ||
| } | ||
|
|
||
| // Fallback values when schema is NULL (operational attribute not in LDAP schema) | ||
| return match ($key) { | ||
| 'description' => 'Password Reset Flag - Forces user to change password at next login (ppolicy overlay)', | ||
| 'name' => 'pwdReset', | ||
| 'name_lc' => 'pwdreset', | ||
| 'is_editable' => TRUE, | ||
| 'required_by' => collect(), | ||
| 'used_in' => collect(), | ||
| default => parent::__get($key), | ||
| }; | ||
| } | ||
|
|
||
| /* METHODS */ | ||
|
|
||
| public function isDirty(): bool | ||
| { | ||
| $old = $this->values_old->dot()->filter(fn($item)=>! is_null($item) && $item !== ''); | ||
| $new = $this->values->dot()->filter(fn($item)=>! is_null($item) && $item !== ''); | ||
|
|
||
| return $old->count() !== $new->count() || $old->diff($new)->count() !== 0; | ||
| } | ||
|
|
||
| /** | ||
| * pwdReset is an operational attribute (ppolicy overlay) that: | ||
| * - Can only be set to TRUE (server manages FALSE/removal automatically) | ||
| * - When set to TRUE, user must change password at next login | ||
| * - When set to FALSE we keep the attribute present with value FALSE to remain editable | ||
| */ | ||
| public function getDirty(): array | ||
| { | ||
| if (! $this->isDirty()) | ||
| return []; | ||
|
|
||
| $normalized = $this->values | ||
| ->map(fn($values)=>array_values(array_map(fn($v)=>strtoupper(trim($v)) === 'TRUE' ? 'TRUE' : 'FALSE',$values))); | ||
|
|
||
| // If any TRUE values exist, send only the TRUEs; otherwise send FALSE to keep attribute present | ||
| $trueValues = $normalized | ||
| ->map(fn($values)=>array_values(array_filter($values,fn($v)=>$v === 'TRUE'))) | ||
| ->filter(fn($values)=>count($values) > 0); | ||
|
|
||
| return $trueValues->isNotEmpty() | ||
| ? [$this->name_lc => $trueValues->toArray()] | ||
| : [$this->name_lc => [Entry::TAG_NOTAG => ['FALSE']]]; | ||
| } | ||
|
|
||
| public function render_item_old(string $dotkey): ?string | ||
| { | ||
| $value = $this->values_old->dot()->get($dotkey); | ||
|
|
||
| if ($value === NULL || $value === '') | ||
| return NULL; | ||
|
|
||
| return strtoupper($value) === 'TRUE' ? 'TRUE' : 'FALSE'; | ||
| } | ||
|
|
||
| public function render_item_new(string $dotkey): ?string | ||
| { | ||
| $value = $this->values->dot()->get($dotkey); | ||
|
|
||
| if ($value === NULL || $value === '') | ||
| return NULL; | ||
|
|
||
| return strtoupper($value) === 'TRUE' ? 'TRUE' : 'FALSE'; | ||
| } | ||
|
|
||
| public function render(string $attrtag,int $index,?View $view=NULL,bool $edit=FALSE,bool $editable=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View | ||
| { | ||
| return parent::render( | ||
| attrtag: $attrtag, | ||
| index: $index, | ||
| view: view('components.attribute.value.pwdreset'), | ||
| edit: $edit, | ||
| editable: $editable, | ||
| new: $new, | ||
| updated: $updated, | ||
| template: $template); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -357,6 +357,15 @@ private function getAttributesAsObjects(): Collection | |
| $result->put($attribute,$o); | ||
| } | ||
|
|
||
| // Ensure ppolicy operational attributes exist for user entries so they stay editable even when absent on the server | ||
| if ($this->isUserEntry() && (! $result->has('pwdreset'))) | ||
| $result->put('pwdreset',Factory::create( | ||
| dn: $this->dn, | ||
| attribute: 'pwdReset', | ||
| values: [self::TAG_NOTAG=>['FALSE']], | ||
| oc: $entry_oc, | ||
| )); | ||
|
|
||
| $sort = collect(config('pla.attr_display_order',[]))->map(fn($item)=>strtolower($item)); | ||
|
|
||
| // Order the attributes | ||
|
|
@@ -508,8 +517,26 @@ public function getOtherTags(): Collection | |
| */ | ||
| public function getMissingAttributes(): Collection | ||
| { | ||
| return $this->getAvailableAttributes() | ||
| $missing = $this->getAvailableAttributes() | ||
| ->filter(fn($a)=>(! $this->getVisibleAttributes()->contains(fn($b)=>($a->name === $b->name)))); | ||
|
|
||
| // Add ppolicy operational attributes for user entries | ||
| if ($this->isUserEntry()) { | ||
| $ppolicyAttrs = ['pwdReset']; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not all LDAP servers implement |
||
|
|
||
| foreach ($ppolicyAttrs as $attrName) { | ||
| $attrLower = strtolower($attrName); | ||
|
|
||
| // Only add if not already present in entry or missing list | ||
| if (! $this->hasAttribute($attrLower) && ! $missing->contains(fn($item)=>strtolower($item->name) === $attrLower)) { | ||
| $schema = config('server')->schema('attributetypes',$attrName); | ||
| if ($schema) | ||
| $missing->push($schema); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return $missing; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -534,6 +561,20 @@ public function hasAttribute(int|string $key): bool | |
| ->has($key); | ||
| } | ||
|
|
||
| /** | ||
| * Check if this entry is a user-like entry based on objectclasses | ||
| * | ||
| * @return bool | ||
| */ | ||
| public function isUserEntry(): bool | ||
| { | ||
| static $userObjectClasses = ['posixaccount','inetorgperson','person','account','organizationalperson']; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will need to be an external configurable item, to allow for users who (also) use custom schemas. |
||
|
|
||
| $entryOCs = $this->getObject('objectclass')?->tagValues()->map(fn($item)=>strtolower(trim($item))) ?? collect(); | ||
|
|
||
| return $entryOCs->intersect($userObjectClasses)->isNotEmpty(); | ||
| } | ||
|
|
||
| /** | ||
| * Did this query generate a size limit exception | ||
| * | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| <!-- $o=PwdReset::class --> | ||
|
|
||
| <div class="input-group has-validation"> | ||
| @if($edit || ($editable ?? false)) | ||
| <div class="form-check form-switch"> | ||
| <input type="hidden" | ||
| name="{{ $o->name_lc }}[{{ $attrtag }}][]" | ||
| value="{{ strtoupper($value ?? '') === 'TRUE' ? 'TRUE' : 'FALSE' }}" | ||
| id="{{ $o->name_lc }}_hidden_{{ $index ?? 0 }}"> | ||
| <input class="form-check-input pwdreset-toggle" | ||
| type="checkbox" | ||
| role="switch" | ||
| id="{{ $o->name_lc }}_{{ $index ?? 0 }}" | ||
| data-hidden-id="{{ $o->name_lc }}_hidden_{{ $index ?? 0 }}" | ||
| @checked(strtoupper($value ?? '') === 'TRUE')> | ||
| <label class="form-check-label" for="{{ $o->name_lc }}_{{ $index ?? 0 }}"> | ||
| @if(strtoupper($value ?? '') === 'TRUE') | ||
| <span class="text-danger"><i class="fas fa-exclamation-triangle"></i> @lang('Password must be changed')</span> | ||
| @else | ||
| <span class="text-success"><i class="fas fa-check-circle"></i> @lang('No password reset required')</span> | ||
| @endif | ||
| </label> | ||
| </div> | ||
| @if(isset($errors) && $errors->any()) | ||
| <x-form.invalid-feedback :errors="$errors->get($o->name_lc.'.'.($dotkey ?? '')) ?? []"/> | ||
| @endif | ||
| @else | ||
| @if(strtoupper($value ?? '') === 'TRUE') | ||
| <span class="text-danger"><i class="fas fa-exclamation-triangle"></i> @lang('Yes - Password must be changed')</span> | ||
| @else | ||
| <span class="text-success"><i class="fas fa-check-circle"></i> @lang('No')</span> | ||
| @endif | ||
| @endif | ||
| </div> | ||
|
|
||
| @section($o->name_lc.'-scripts') | ||
| <script type="text/javascript"> | ||
| function initPwdResetToggle() { | ||
| $('.pwdreset-toggle').off('change').on('change', function() { | ||
| var hiddenId = $(this).data('hidden-id'); | ||
| var newValue = $(this).is(':checked') ? 'TRUE' : 'FALSE'; | ||
| $('#' + hiddenId).val(newValue); | ||
|
|
||
| var label = $(this).next('label'); | ||
| if ($(this).is(':checked')) { | ||
| label.html('<span class="text-danger"><i class="fas fa-exclamation-triangle"></i> {{ __("Password must be changed") }}</span>'); | ||
| } else { | ||
| label.html('<span class="text-success"><i class="fas fa-check-circle"></i> {{ __("No password reset required") }}</span>'); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| if (window.$ === undefined) { | ||
| document.addEventListener('DOMContentLoaded', () => initPwdResetToggle()); | ||
| } else { | ||
| initPwdResetToggle(); | ||
| } | ||
| </script> | ||
| @endsection |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about this and will need to review it in more detail - not all server implement ppolicy and/or
pwdresetattributes.