Compare commits

..

No commits in common. "master" and "2.0.0" have entirely different histories.

313 changed files with 7699 additions and 11913 deletions

View File

@ -1,7 +1,8 @@
APP_NAME=Laravel
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=daily
@ -11,7 +12,7 @@ SESSION_DRIVER=file
SESSION_LIFETIME=120
LDAP_HOST=
LDAP_BASE_DN=
LDAP_USERNAME=
LDAP_PASSWORD=
LDAP_CACHE=false
LDAP_ALERT_ROOTDN=true
LDAP_CACHE=true

View File

@ -1,16 +1,51 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_ENV=dev
APP_KEY=base64:KvIecx8zoy6RjcbJM8s98ZKs9IDGUHFVqBRn3Awfmso=
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stderr
LOG_CHANNEL=stack
CACHE_DRIVER=array
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
LDAP_HOST=openldap
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
LDAP_HOST=test_ldap
LDAP_PORT=389
LDAP_BASE_DN="dc=Test"
LDAP_USERNAME="cn=admin,dc=Test"
LDAP_PASSWORD="test"
LDAP_CACHE=false

View File

@ -2,8 +2,8 @@ name: Create Docker Image
run-name: ${{ gitea.actor }} Building Docker Image 🐳
on: [push]
env:
VERSION: latest
DOCKER_HOST: tcp://127.0.0.1:2375
ASSETS: 2d732e5
jobs:
test:
@ -66,7 +66,7 @@ jobs:
public/js/manifest.js
public/js/vendor.js
#key: build-pla-page-assets-${{ hashFiles('**/package-lock.json') }}
key: build-pla-page-assets-${{ env.ASSETS }}
key: build-pla-page-assets-29f7ce2
#restore-keys: |
# build-pla-page-assets-
@ -85,6 +85,7 @@ jobs:
privileged: true
env:
ARCH: ${{ matrix.arch }}
VERSIONARCH: ${{ env.VERSION }}-${{ env.ARCH }}
steps:
- name: Environment Setup
@ -129,7 +130,7 @@ jobs:
public/js/manifest.js
public/js/vendor.js
#key: build-pla-page-assets-${{ hashFiles('**/package-lock.json') }}
key: build-pla-page-assets-${{ env.ASSETS }}
key: build-pla-page-assets-29f7ce2
#restore-keys: |
# build-pla-page-assets-
@ -144,12 +145,12 @@ jobs:
- name: Record version and Delete Unnecessary files
id: prebuild
run: |
echo "version=$(cat public/VERSION)" >> "$GITHUB_OUTPUT"
echo "revision=${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
echo ${GITHUB_SHA::8} > VERSION
# [ "${GITHUB_REF_TYPE}" -eq "tag" ] && echo v${GITHUB_REF_NAME}-rel > public/VERSION
rm -rf .git* tests/ storage/app/test/
cat VERSION public/VERSION
# ls -al public/css/
# ls -al public/js/
ls -al public/css/
ls -al public/js/
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
@ -157,10 +158,10 @@ jobs:
context: .
file: docker/Dockerfile
push: true
tags: "${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.GITHUB_REF_NAME }}-${{ env.ARCH }}"
tags: "${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSIONARCH }}"
build-args: |
BUILD_REVISION=${{ env.GITHUB_SHA }}
BUILD_VERSION=v${{ env.GITHUB_REF_NAME }}
BUILD_REVISION=${{ steps.prebuild.outputs.revision }}
BUILD_VERSION=${{ steps.prebuild.outputs.version }}
manifest:
name: Final Docker Image Manifest
@ -195,8 +196,7 @@ jobs:
- name: Build Docker Manifest
run: |
docker manifest create ${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.GITHUB_REF_NAME }} \
${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.GITHUB_REF_NAME }}-x86_64 \
${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.GITHUB_REF_NAME }}-arm64
docker manifest push --purge ${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.GITHUB_REF_NAME }}
echo "Built container: ${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.GITHUB_REF_NAME }}"
docker manifest create ${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSION }} \
${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSION }}-x86_64 \
${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSION }}-arm64
docker manifest push --purge ${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSION }}

View File

@ -10,9 +10,6 @@ assignees: ''
**Describe the bug**
A clear and concise description of what the bug is. (One issue per report please.)
**Version of PLA**
What version of PLA are you using. Are you using the docker container, an distribution package or running from GIT source?
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'

View File

@ -1 +0,0 @@
blank_issues_enabled: false

View File

@ -1,10 +1,4 @@
# phpLDAPadmin
![GitHub commit activity](https://img.shields.io/github/commit-activity/m/leenooks/phpldapadmin)
![Docker Pulls](https://img.shields.io/docker/pulls/phpldapadmin/phpldapadmin)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/leenooks/phpldapadmin/total)
![GitHub Release Date](https://img.shields.io/github/release-date/leenooks/phpldapadmin)
![GitHub commits since latest release](https://img.shields.io/github/commits-since/leenooks/phpldapadmin/latest)
phpLDAPadmin is a web based LDAP data management tool for system administrators. It is commonly known and referred by many as "PLA".
PLA is designed to be compliant with LDAP RFCs, enabling it to be used with any LDAP server.
@ -33,37 +27,37 @@ Take a look at the [Docker Container](https://github.com/leenooks/phpLDAPadmin/w
>
> Open an issue (details below) with enough information for me to be able to recreate the problem. An `LDIF` will be invaluable if it is not handling data correctly.
## Templates
Starting with v2.2, PLA reintroduces the template engine. Each point release going forward will improve the template
functionality. Check [releases](https://github.com/leenooks/phpLDAPadmin/releases) for details.
## Version 2 Progress
Templates in v2 are in JSON format (in v1 they were XML format). If you want to create your own templates you can use
the [example.json](/templates/example.json) template as a guide. Place your custom templates in a subdirectory
under `templates`, eg: `templates/custom`, and they wont be overwritten by an update.
The update to v2 is progressing well - here is a list of work to do and done:
## Outstanding items
Compare to v1.x, there are a couple of outstanding items to address
Entry Editing:
- [X] Creating new LDAP entries
- [ ] Delete existing LDAP entries
- [X] Updating existing LDAP Entries
- [X] Password attributes
- [X] Support different password hash options
- [X] Validate password is correct
- [ ] JpegPhoto Create/Delete
- [ ] Binary attribute upload
- [ ] If removing an objectClass, remove all attributes that only that objectclass provided
- [ ] Move an entry
- [ ] Group membership selection
- [ ] Attribute tag creation
- [X] JpegPhoto Display
- [X] ObjectClass Add/Remove
- [X] Add additional required attributes (for ObjectClass Addition)
- [ ] Remove existing required attributes (for ObjectClass Removal)
- [X] Add additional values to Attributes that support multiple values
- [X] Delete extra values for Attributes that support multiple values
- [ ] Delete Attributes
- [ ] Templates to enable entries to conform to a custom standard
- [X] Login to LDAP server
- [X] Configure login by a specific attribute
- [X] Logout LDAP server
- [X] Export entries as an LDAP
- [X] Import LDIF
- [X] Schema Browser
- [ ] Is there something missing?
Templates Engine
- [ ] Enforcing attribute uniqueness
Raise a [feature request](https://github.com/leenooks/phpLDAPadmin/issues/new) if there is a capability that you would like to see added to PLA.
Other items [under consideration](https://github.com/leenooks/phpLDAPadmin/issues?q=state%3Aopen%20label%3Aenhancement)
## Support is known for these LDAP servers:
Support is known for these LDAP servers:
- [X] OpenLDAP
- [X] OpenDJ
- [ ] Microsoft Active Directory
- [X] 389 Directory Server
If there is an LDAP server that you have that you would like to have supported, please open an issue to request it.
You might need to provide access, provide a copy or instructions to get an environment for testing. If you have enabled

View File

@ -7,37 +7,41 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use App\Classes\LDAP\Schema\AttributeType;
use App\Classes\Template;
use App\Exceptions\InvalidUsage;
use App\Ldap\Entry;
/**
* Represents an attribute of an LDAP Object
*/
class Attribute implements \Countable, \ArrayAccess
class Attribute implements \Countable, \ArrayAccess, \Iterator
{
// Attribute Name
protected string $name;
private int $counter = 0;
protected ?AttributeType $schema = NULL;
/*
# Source of this attribute definition
protected $source;
*/
// Current and Old Values
protected Collection $values;
// Is this attribute an internal attribute
protected ?bool $_is_internal = NULL;
protected(set) bool $no_attr_tags = FALSE;
protected bool $is_internal = FALSE;
// Is this attribute the RDN?
protected bool $is_rdn = FALSE;
// MIN/MAX number of values
protected(set) int $min_values_count = 0;
protected(set) int $max_values_count = 0;
protected int $min_values_count = 0;
protected int $max_values_count = 0;
// The schema's representation of this attribute
protected(set) ?AttributeType $schema;
// RFC3866 Language Tags
protected Collection $lang_tags;
// The DN this object is in
protected(set) string $dn;
// The old values for this attribute - helps with isDirty() to determine if there is an update pending
private Collection $_values_old;
// Current Values
private Collection $_values;
// The objectclasses of the entry that has this attribute
protected(set) Collection $oc;
private const SYNTAX_CERTIFICATE = '1.3.6.1.4.1.1466.115.121.1.8';
private const SYNTAX_CERTIFICATE_LIST = '1.3.6.1.4.1.1466.115.121.1.9';
protected Collection $oldValues;
/*
# Has the attribute been modified
@ -90,36 +94,16 @@ class Attribute implements \Countable, \ArrayAccess
protected $postvalue = array();
*/
/**
* Create an Attribute
*
* @param string $dn DN this attribute is used in
* @param string $name Name of the attribute
* @param array $values Current Values
* @param array $oc The objectclasses that the DN of this attribute has
* @throws InvalidUsage
*/
public function __construct(string $dn,string $name,array $values,array $oc=[])
public function __construct(string $name,array $values)
{
$this->dn = $dn;
$this->_values = collect($values);
$this->_values_old = collect($values);
$this->name = $name;
$this->values = collect($values);
$this->lang_tags = collect();
$this->oldValues = collect($values);
$this->schema = config('server')
$this->schema = (new Server)
->schema('attributetypes',$name);
$this->oc = collect();
// Get the objectclass heirarchy for required attribute determination
foreach ($oc as $objectclass) {
$soc = config('server')->schema('objectclasses',$objectclass);
if ($soc) {
$this->oc->push($soc->oid);
$this->oc = $this->oc->merge($soc->getParents()->pluck('oid'));
}
}
/*
# Should this attribute be hidden
if ($server->isAttrHidden($this->name))
@ -135,49 +119,35 @@ class Attribute implements \Countable, \ArrayAccess
*/
}
public function __call(string $name,array $arguments)
{
abort(555,'Method not handled: '.$name);
}
public function __get(string $key): mixed
{
return match ($key) {
// List all the attributes
'attributes' => $this->attributes(),
// Can this attribute have more values
'can_addvalues' => $this->schema && (! $this->schema->is_single_value) && ((! $this->max_values_count) || ($this->values->count() < $this->max_values_count)),
// Schema attribute description
'description' => $this->schema ? $this->schema->{$key} : NULL,
// Attribute hints
'hints' => $this->hints(),
// Attribute language tags
'langtags' => ($this->no_attr_tags || (! $this->_values->count()))
? collect(Entry::TAG_NOTAG)
: $this->_values
->keys()
->filter(fn($item)=>($item === Entry::TAG_NOTAG) || preg_match(sprintf('/%s;?/',Entry::TAG_CHARS_LANG),$item))
->sortBy(fn($item)=>($item === Entry::TAG_NOTAG) ? NULL : $item),
// Can this attribute be edited
'is_editable' => $this->schema ? $this->schema->{$key} : NULL,
// Is this an internal attribute
'is_internal' => is_null($this->_is_internal) ? ($this->used_in->count() === 0) : $this->_is_internal,
// Objectclasses that required this attribute for an LDAP entry
'required' => $this->required(),
// Is this attribute an RDN attribute
'is_rdn' => $this->isRDN(),
'is_internal' => isset($this->{$key}) && $this->{$key},
// Is this attribute the RDN
'is_rdn' => $this->is_rdn,
// We prefer the name as per the schema if it exists
'name' => $this->schema->{$key},
'name' => $this->schema ? $this->schema->{$key} : $this->{$key},
// Attribute name in lower case
'name_lc' => strtolower($this->name),
// Old Values
'old_values' => $this->oldValues,
// Attribute values
'values' => $this->values,
// Required by Object Classes
'required_by' => $this->schema?->required_by_object_classes ?: collect(),
// Used in Object Classes
'used_in' => $this->schema?->used_in_object_classes ?: collect(),
// For single value attributes
'value' => $this->schema?->is_single_value ? $this->values->first() : NULL,
// The current attribute values
'values' => ($this->no_attr_tags || $this->is_internal) ? $this->tagValues() : $this->_values,
// The original attribute values
'values_old' => ($this->no_attr_tags || $this->is_internal) ? $this->tagValuesOld() : $this->_values_old,
default => throw new \Exception('Unknown key:' . $key),
};
@ -186,16 +156,11 @@ class Attribute implements \Countable, \ArrayAccess
public function __set(string $key,mixed $values): void
{
switch ($key) {
case 'values':
$this->_values = $values;
break;
case 'values_old':
$this->_values_old = $values;
case 'value':
$this->values = collect($values);
break;
default:
throw new \Exception('Unknown key:'.$key);
}
}
@ -204,27 +169,49 @@ class Attribute implements \Countable, \ArrayAccess
return $this->name;
}
/* INTERFACE */
public function addValue(string $value): void
{
$this->values->push($value);
}
public function current(): mixed
{
return $this->values->get($this->counter);
}
public function next(): void
{
$this->counter++;
}
public function key(): mixed
{
return $this->counter;
}
public function valid(): bool
{
return $this->values->has($this->counter);
}
public function rewind(): void
{
$this->counter = 0;
}
public function count(): int
{
return $this->_values
->dot()
->count();
return $this->values->count();
}
public function offsetExists(mixed $offset): bool
{
return $this->_values
->dot()
->has($offset);
return ! is_null($this->values->has($offset));
}
public function offsetGet(mixed $offset): mixed
{
return $this->_values
->dot()
->get($offset);
return $this->values->get($offset);
}
public function offsetSet(mixed $offset, mixed $value): void
@ -237,48 +224,32 @@ class Attribute implements \Countable, \ArrayAccess
// We cannot clear values using array syntax
}
/* METHODS */
public function addValue(string $tag,array $values): void
{
$this->_values->put(
$tag,
array_unique(array_merge($this->_values
->get($tag,[]),$values)));
}
public function addValueOld(string $tag,array $values): void
{
$this->_values_old->put(
$tag,
array_unique(array_merge($this->_values_old
->get($tag,[]),$values)));
}
/**
* Return the hints about this attribute, ie: RDN, Required, etc
*
* @return Collection
* @return array
*/
public function hints(): Collection
public function hints(): array
{
$result = collect();
if ($this->is_internal)
return $result;
// Is this Attribute an RDN
if ($this->is_rdn)
$result->put(__('rdn'),__('This attribute is required for the RDN'));
if ($this->required()->count())
$result->put(__('required'),sprintf('%s: %s',__('Required Attribute by ObjectClass(es)'),$this->required()->join(', ')));
// If this attribute name is an alias for the schema attribute name
// @todo
// If this attribute is a dynamic attribute
if ($this->isDynamic())
$result->put(__('dynamic'),__('These are dynamic values present as a result of another attribute'));
// objectClasses requiring this attribute
// eg: $result->put('required','Required by objectClasses: a,b');
if ($this->required_by->count())
$result->put(__('required'),sprintf('%s: %s',__('Required Attribute by ObjectClass(es)'),$this->required_by->join(',')));
return $result;
// This attribute has language tags
if ($this->lang_tags->count())
$result->put(__('language tags'),sprintf('%s: %d',__('This Attribute has Language Tags'),$this->lang_tags->count()));
return $result->toArray();
}
/**
@ -288,38 +259,13 @@ class Attribute implements \Countable, \ArrayAccess
*/
public function isDirty(): bool
{
return (($a=$this->values_old->dot()->filter())->keys()->count() !== ($b=$this->values->dot()->filter())->keys()->count())
|| ($a->count() !== $b->count())
|| ($a->diff($b)->count() !== 0);
return ($this->oldValues->count() !== $this->values->count())
|| ($this->values->diff($this->oldValues)->count() !== 0);
}
/**
* Are these values as a result of a dynamic attribute
*
* @return bool
*/
public function isDynamic(): bool
public function oldValues(array $array): void
{
return $this->schema->used_in_object_classes
->keys()
->intersect($this->oc)
->count() === 0;
}
/**
* Work out if this attribute is an RDN attribute
*
* @return bool
*/
public function isRDN(): bool
{
// If we dont have an DN, then we cant know
if (! $this->dn)
return FALSE;
$rdns = collect(explode('+',substr($this->dn,0,strpos($this->dn,','))));
return $rdns->filter(fn($item) => str_starts_with($item,$this->name.'='))->count() > 0;
$this->oldValues = collect($array);
}
/**
@ -328,98 +274,41 @@ class Attribute implements \Countable, \ArrayAccess
* @param bool $edit Render an edit form
* @param bool $old Use old value
* @param bool $new Enable adding values
* @param bool $updated Has the entry been updated (uses rendering highlights))
* @param Template|null $template
* @return View
*/
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
{
if ($this->is_internal)
// @note Internal attributes cannot be edited
return view('components.attribute.internal')
->with('o',$this);
$view = match ($this->schema?->syntax_oid) {
self::SYNTAX_CERTIFICATE => view('components.syntax.certificate'),
self::SYNTAX_CERTIFICATE_LIST => view('components.syntax.certificatelist'),
default => view()->exists($x='components.attribute.'.$this->name_lc)
? view($x)
: view('components.attribute'),
};
return $view
return view('components.attribute')
->with('o',$this)
->with('edit',$edit)
->with('old',$old)
->with('new',$new)
->with('template',$template)
->with('updated',$updated);
->with('new',$new);
}
/**
* Return the value of the original old values
*
* @param string $dotkey
* @return string|null
*/
public function render_item_old(string $dotkey): ?string
public function render_item_old(int $key): ?string
{
return match ($this->schema->syntax_oid) {
self::SYNTAX_CERTIFICATE => join("\n",str_split(base64_encode(Arr::get($this->values_old->dot(),$dotkey)),80)),
self::SYNTAX_CERTIFICATE_LIST => join("\n",str_split(base64_encode(Arr::get($this->values_old->dot(),$dotkey)),80)),
default => Arr::get($this->values_old->dot(),$dotkey),
};
return Arr::get($this->old_values,$key);
}
/**
* Return the value of the new values, which would include any pending udpates
*
* @param string $dotkey
* @return string|null
*/
public function render_item_new(string $dotkey): ?string
public function render_item_new(int $key): ?string
{
return Arr::get($this->values->dot(),$dotkey);
return Arr::get($this->values,$key);
}
/**
* Work out if this attribute is required by an objectClass the entry has
*
* @return Collection
*/
private function required(): Collection
{
// If we dont have any objectclasses then we cant know if it is required
return $this->oc->count()
? $this->oc->intersect($this->required_by->keys())->sort()
: collect();
}
/**
* Return the new values for this attribute, which would include any pending updates
* If this attribute has RFC3866 Language Tags, this will enable those values to be captured
*
* @param string $tag
* @return Collection
* @param array $value
* @return void
*/
public function tagValues(string $tag=Entry::TAG_NOTAG): Collection
public function setLangTag(string $tag,array $value): void
{
return collect($this->_values
->filter(fn($item,$key)=>($key===$tag))
->get($tag,[]));
$this->lang_tags->put($tag,$value);
}
/**
* Return the original values for this attribute, as stored in the LDAP server
*
* @param string $tag
* @return Collection
*/
public function tagValuesOld(string $tag=Entry::TAG_NOTAG): Collection
public function setRDN(): void
{
return collect($this->_values_old
->filter(fn($item,$key)=>($key===$tag))
->get($tag,[]));
$this->is_rdn = TRUE;
}
}

View File

@ -7,6 +7,6 @@ use App\Classes\LDAP\Attribute;
/**
* Represents an attribute whose values are binary
*/
abstract class Binary extends Attribute
class Binary extends Attribute
{
}

View File

@ -5,7 +5,6 @@ namespace App\Classes\LDAP\Attribute\Binary;
use Illuminate\Contracts\View\View;
use App\Classes\LDAP\Attribute\Binary;
use App\Classes\Template;
use App\Traits\MD5Updates;
/**
@ -15,14 +14,13 @@ final class JpegPhoto extends Binary
{
use MD5Updates;
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
{
return view('components.attribute.binary.jpegphoto')
->with('o',$this)
->with('edit',$edit)
->with('old',$old)
->with('new',$new)
->with('updated',$updated)
->with('f',new \finfo);
}
}

View File

@ -1,61 +0,0 @@
<?php
namespace App\Classes\LDAP\Attribute;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use App\Classes\LDAP\Attribute;
use App\Traits\MD5Updates;
/**
* Represents an attribute whose values is a binary user certificate
*/
final class Certificate extends Attribute
{
use MD5Updates;
private array $_object = [];
public function authority_key_identifier(int $key=0): string
{
$data = collect(explode("\n",$this->cert_info('extensions.authorityKeyIdentifier',$key)));
return $data
->filter(fn($item)=>Str::startsWith($item,'keyid:'))
->map(fn($item)=>Str::after($item,'keyid:'))
->first();
}
public function certificate(int $key=0): string
{
return sprintf("-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----",
join("\n",str_split(base64_encode(Arr::get($this->values_old,'binary.'.$key)),80))
);
}
public function cert_info(string $index,int $key=0): mixed
{
if (! array_key_exists($key,$this->_object))
$this->_object[$key] = openssl_x509_parse(openssl_x509_read($this->certificate($key)));
return Arr::get($this->_object[$key],$index);
}
public function expires(int $key=0): Carbon
{
return Carbon::createFromTimestampUTC($this->cert_info('validTo_time_t',$key));
}
public function subject(int $key=0): string
{
$subject = collect($this->cert_info('subject',$key))->reverse();
return $subject->map(fn($item,$key)=>sprintf("%s=%s",$key,$item))->join(',');
}
public function subject_key_identifier(int $key=0): string
{
return $this->cert_info('extensions.subjectKeyIdentifier',$key);
}
}

View File

@ -1,14 +0,0 @@
<?php
namespace App\Classes\LDAP\Attribute;
use App\Classes\LDAP\Attribute;
use App\Traits\MD5Updates;
/**
* Represents an attribute whose values is a binary user certificate
*/
final class CertificateList extends Attribute
{
use MD5Updates;
}

View File

@ -3,6 +3,7 @@
namespace App\Classes\LDAP\Attribute;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use App\Classes\LDAP\Attribute;
@ -19,45 +20,39 @@ class Factory
* Map of attributes to appropriate class
*/
public const map = [
'authorityrevocationlist' => CertificateList::class,
'cacertificate' => Certificate::class,
'certificaterevocationlist' => CertificateList::class,
'createtimestamp' => Internal\Timestamp::class,
'configcontext' => Schema\Generic::class,
'krblastfailedauth' => Attribute\NoAttrTags\Generic::class,
'krblastpwdchange' => Attribute\NoAttrTags\Generic::class,
'krblastsuccessfulauth' => Attribute\NoAttrTags\Generic::class,
'krbpasswordexpiration' => Attribute\NoAttrTags\Generic::class,
'krbloginfailedcount' => Attribute\NoAttrTags\Generic::class,
'krbprincipalkey' => KrbPrincipalKey::class,
'krbticketflags' => KrbTicketFlags::class,
'creatorsname' => Internal\DN::class,
'contextcsn' => Internal\CSN::class,
'entrycsn' => Internal\CSN::class,
'entrydn' => Internal\DN::class,
'entryuuid' => Internal\UUID::class,
'gidnumber' => GidNumber::class,
'hassubordinates' => Internal\HasSubordinates::class,
'jpegphoto' => Binary\JpegPhoto::class,
'modifytimestamp' => Internal\Timestamp::class,
'monitorcontext' => Schema\Generic::class,
'namingcontexts' => Schema\Generic::class,
'modifiersname' => Internal\DN::class,
'objectclass' => ObjectClass::class,
'structuralobjectclass' => Internal\StructuralObjectClass::class,
'subschemasubentry' => Internal\SubschemaSubentry::class,
'supportedcontrol' => Schema\OID::class,
'supportedextension' => Schema\OID::class,
'supportedfeatures' => Schema\OID::class,
'supportedldapversion' => Schema\Generic::class,
'supportedsaslmechanisms' => Schema\Mechanisms::class,
'usercertificate' => Certificate::class,
'userpassword' => Password::class,
];
/**
* Create the new Object for an attribute
*
* @param string $dn
* @param string $attribute
* @param array $values
* @param array $oc
* @return Attribute
*/
public static function create(string $dn,string $attribute,array $values,array $oc=[]): Attribute
public static function create(string $attribute,array $values): Attribute
{
$class = Arr::get(self::map,strtolower($attribute),Attribute::class);
return new $class($dn,$attribute,$values,$oc);
Log::debug(sprintf('%s:Creating LDAP Attribute [%s] as [%s]',static::LOGKEY,$attribute,$class));
return new $class($attribute,$values);
}
}

View File

@ -2,6 +2,8 @@
namespace App\Classes\LDAP\Attribute;
use Illuminate\Contracts\View\View;
use App\Classes\LDAP\Attribute;
/**
@ -9,6 +11,12 @@ use App\Classes\LDAP\Attribute;
*/
abstract class Internal extends Attribute
{
protected ?bool $_is_internal = TRUE;
protected(set) bool $no_attr_tags = TRUE;
protected bool $is_internal = TRUE;
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
{
// @note Internal attributes cannot be edited
return view('components.attribute.internal')
->with('o',$this);
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Classes\LDAP\Attribute\Internal;
use App\Classes\LDAP\Attribute\Internal;
/**
* Represents an CSN Attribute
*/
final class CSN extends Internal
{
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Classes\LDAP\Attribute\Internal;
use App\Classes\LDAP\Attribute\Internal;
/**
* Represents an DN Attribute
*/
final class DN extends Internal
{
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Classes\LDAP\Attribute\Internal;
use App\Classes\LDAP\Attribute\Internal;
/**
* Represents an HasSubordinates Attribute
*/
final class HasSubordinates extends Internal
{
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Classes\LDAP\Attribute\Internal;
use App\Classes\LDAP\Attribute\Internal;
/**
* Represents an StructuralObjectClass Attribute
*/
final class StructuralObjectClass extends Internal
{
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Classes\LDAP\Attribute\Internal;
use App\Classes\LDAP\Attribute\Internal;
/**
* Represents an SubschemaSubentry Attribute
*/
final class SubschemaSubentry extends Internal
{
}

View File

@ -5,14 +5,13 @@ namespace App\Classes\LDAP\Attribute\Internal;
use Illuminate\Contracts\View\View;
use App\Classes\LDAP\Attribute\Internal;
use App\Classes\Template;
/**
* Represents an attribute whose values are timestamps
*/
final class Timestamp extends Internal
{
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
{
// @note Internal attributes cannot be edited
return view('components.attribute.internal.timestamp')

View File

@ -0,0 +1,12 @@
<?php
namespace App\Classes\LDAP\Attribute\Internal;
use App\Classes\LDAP\Attribute\Internal;
/**
* Represents an UUID Attribute
*/
final class UUID extends Internal
{
}

View File

@ -1,43 +0,0 @@
<?php
namespace App\Classes\LDAP\Attribute;
use Illuminate\Contracts\View\View;
use App\Classes\LDAP\Attribute;
use App\Classes\Template;
use App\Traits\MD5Updates;
/**
* Represents an attribute whose values are passwords
*/
final class KrbPrincipalKey extends Attribute
{
use MD5Updates;
protected(set) bool $no_attr_tags = TRUE;
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
{
return view('components.attribute.krbprincipalkey')
->with('o',$this)
->with('edit',$edit)
->with('old',$old)
->with('new',$new)
->with('updated',$updated);
}
public function render_item_old(string $dotkey): ?string
{
return parent::render_item_old($dotkey)
? str_repeat('*',16)
: NULL;
}
public function render_item_new(string $dotkey): ?string
{
return parent::render_item_new($dotkey)
? str_repeat('*',16)
: NULL;
}
}

View File

@ -1,63 +0,0 @@
<?php
namespace App\Classes\LDAP\Attribute;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use App\Classes\LDAP\Attribute;
use App\Classes\Template;
/**
* Represents an attribute whose value is a Kerberos Ticket Flag
* See RFC4120
*/
final class KrbTicketFlags extends Attribute
{
protected(set) bool $no_attr_tags = TRUE;
private const DISALLOW_POSTDATED = 0x00000001;
private const DISALLOW_FORWARDABLE = 0x00000002;
private const DISALLOW_TGT_BASED = 0x00000004;
private const DISALLOW_RENEWABLE = 0x00000008;
private const DISALLOW_PROXIABLE = 0x00000010;
private const DISALLOW_DUP_SKEY = 0x00000020;
private const DISALLOW_ALL_TIX = 0x00000040;
private const REQUIRES_PRE_AUTH = 0x00000080;
private const REQUIRES_HW_AUTH = 0x00000100;
private const REQUIRES_PWCHANGE = 0x00000200;
private const DISALLOW_SVR = 0x00001000;
private const PWCHANGE_SERVICE = 0x00002000;
private static function helpers(): Collection
{
$helpers = collect([
log(self::DISALLOW_POSTDATED,2) => __('KRB_DISALLOW_POSTDATED'),
log(self::DISALLOW_FORWARDABLE,2) => __('KRB_DISALLOW_FORWARDABLE'),
log(self::DISALLOW_TGT_BASED,2) => __('KRB_DISALLOW_TGT_BASED'),
log(self::DISALLOW_RENEWABLE,2) => __('KRB_DISALLOW_RENEWABLE'),
log(self::DISALLOW_PROXIABLE,2) => __('KRB_DISALLOW_PROXIABLE'),
log(self::DISALLOW_DUP_SKEY,2) => __('KRB_DISALLOW_DUP_SKEY'),
log(self::DISALLOW_ALL_TIX,2) => __('KRB_DISALLOW_ALL_TIX'),
log(self::REQUIRES_PRE_AUTH,2) => __('KRB_REQUIRES_PRE_AUTH'),
log(self::REQUIRES_HW_AUTH,2) => __('KRB_REQUIRES_HW_AUTH'),
log(self::REQUIRES_PWCHANGE,2) => __('KRB_REQUIRES_PWCHANGE'),
log(self::DISALLOW_SVR,2) => __('KRB_DISALLOW_SVR'),
log(self::PWCHANGE_SERVICE,2) => __('KRB_PWCHANGE_SERVICE'),
])
->replace(config('pla.krb.bits',[]));
return $helpers;
}
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
{
return view('components.attribute.krbticketflags')
->with('o',$this)
->with('edit',$edit)
->with('old',$old)
->with('new',$new)
->with('updated',$updated)
->with('helper',static::helpers());
}
}

View File

@ -1,13 +0,0 @@
<?php
namespace App\Classes\LDAP\Attribute\NoAttrTags;
use App\Classes\LDAP\Attribute;
/**
* Represents an Attribute that doesnt have Lang Tags
*/
class Generic extends Attribute
{
protected(set) bool $no_attr_tags = TRUE;
}

View File

@ -6,57 +6,32 @@ use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use App\Classes\LDAP\Attribute;
use App\Classes\Template;
/**
* Represents an ObjectClass Attribute
*/
final class ObjectClass extends Attribute
{
protected(set) bool $no_attr_tags = TRUE;
// The schema ObjectClasses for this objectclass of a DN
protected Collection $oc_schema;
/**
* Create an ObjectClass Attribute
*
* @param string $dn DN this attribute is used in
* @param string $name Name of the attribute
* @param array $values Current Values
* @param array $oc The objectclasses that the DN of this attribute has (ignored for objectclasses)
*/
public function __construct(string $dn,string $name,array $values,array $oc=[])
public function __construct(string $name,array $values)
{
parent::__construct($dn,$name,$values,['top']);
parent::__construct($name,$values);
$this->set_oc_schema($this->tagValuesOld());
$this->oc_schema = config('server')
->schema('objectclasses')
->filter(fn($item)=>$this->values->contains($item->name));
}
public function __get(string $key): mixed
{
return match ($key) {
'structural' => $this->oc_schema->filter(fn($item)=>$item->isStructural()),
'structural' => $this->oc_schema->filter(fn($item) => $item->isStructural()),
default => parent::__get($key),
};
}
public function __set(string $key,mixed $values): void
{
switch ($key) {
case 'values':
parent::__set($key,$values);
// We need to populate oc_schema, if we are a new OC and thus dont have any old values
if (! $this->values_old->count() && $this->values->count())
$this->set_oc_schema($this->tagValues());
break;
default: parent::__set($key,$values);
}
}
/**
* Is a specific value the structural objectclass
*
@ -70,20 +45,12 @@ final class ObjectClass extends Attribute
->contains($value);
}
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
{
return view('components.attribute.objectclass')
->with('o',$this)
->with('edit',$edit)
->with('old',$old)
->with('new',$new)
->with('updated',$updated);
}
private function set_oc_schema(Collection $tv): void
{
$this->oc_schema = config('server')
->schema('objectclasses')
->filter(fn($item)=>$tv->contains($item->name));
->with('new',$new);
}
}

View File

@ -7,7 +7,6 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use App\Classes\LDAP\Attribute;
use App\Classes\Template;
use App\Traits\MD5Updates;
/**
@ -16,10 +15,6 @@ use App\Traits\MD5Updates;
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\\';
@ -80,35 +75,29 @@ final class Password extends Attribute
return ($helpers=static::helpers())->has($id) ? new ($helpers->get($id)) : NULL;
}
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
{
return view('components.attribute.password')
->with('o',$this)
->with('edit',$edit)
->with('old',$old)
->with('new',$new)
->with('template',$template)
->with('updated',$updated)
->with('helpers',static::helpers()->map(fn($item,$key)=>['id'=>$key,'value'=>$key])->sort());
}
public function render_item_old(string $dotkey): ?string
public function render_item_old(int $key): ?string
{
$pw = parent::render_item_old($dotkey);
$pw = Arr::get($this->oldValues,$key);
return $pw
? (((($x=$this->hash($pw)) && ($x::id() === '*clear*')) ? sprintf('{%s}',$x::shortid()) : '')
.str_repeat('*',16))
? (((($x=$this->hash($pw)) && ($x::id() !== '*clear*')) ? sprintf('{%s}',$x::shortid()) : '').str_repeat('*',16))
: NULL;
}
public function render_item_new(string $dotkey): ?string
public function render_item_new(int $key): ?string
{
$pw = parent::render_item_new($dotkey);
$pw = Arr::get($this->values,$key);
return $pw
? (((($x=$this->hash($pw)) && ($x::id() !== '*clear*')) ? sprintf('{%s}',$x::shortid()) : '')
.str_repeat('*',16))
? (((($x=$this->hash($pw)) && ($x::id() !== '*clear*')) ? sprintf('{%s}',$x::shortid()) : '').str_repeat('*',16))
: NULL;
}
}

View File

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

View File

@ -12,7 +12,7 @@ final class SMD5 extends Base
return $source === $this->encode($compare,$this->salted_salt($source));
}
public function encode(string $password,?string $salt=NULL): string
public function encode(string $password,string $salt=NULL): string
{
if (is_null($salt))
$salt = hex2bin(random_salt(self::salt));

View File

@ -12,7 +12,7 @@ final class SSHA extends Base
return $source === $this->encode($compare,$this->salted_salt($source));
}
public function encode(string $password,?string $salt=NULL): string
public function encode(string $password,string $salt=NULL): string
{
return sprintf('{%s}%s',self::key,$this->salted_hash($password,'sha1',self::salt,$salt));
}

View File

@ -6,7 +6,6 @@ use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use App\Classes\LDAP\Attribute;
use App\Classes\Template;
/**
* Represents the RDN for an Entry
@ -14,9 +13,6 @@ use App\Classes\Template;
final class RDN extends Attribute
{
private string $base;
protected(set) bool $no_attr_tags = TRUE;
private Collection $attrs;
public function __get(string $key): mixed
@ -28,14 +24,14 @@ final class RDN extends Attribute
};
}
public function hints(): Collection
public function hints(): array
{
return collect([
return [
'required' => __('RDN is required')
]);
];
}
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
{
return view('components.attribute.rdn')
->with('o',$this);

View File

@ -2,6 +2,7 @@
namespace App\Classes\LDAP\Attribute;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
@ -13,7 +14,6 @@ use App\Classes\LDAP\Attribute;
abstract class Schema extends Attribute
{
protected bool $internal = TRUE;
protected(set) bool $no_attr_tags = TRUE;
protected static function _get(string $filename,string $string,string $key): ?string
{
@ -30,7 +30,7 @@ abstract class Schema extends Attribute
while (! feof($f)) {
$line = trim(fgets($f));
if ((! $line) || preg_match('/^#/',$line))
if (! $line OR preg_match('/^#/',$line))
continue;
$fields = explode(':',$line);
@ -41,14 +41,18 @@ abstract class Schema extends Attribute
'desc'=>Arr::get($fields,3,__('No description available, can you help with one?')),
]);
}
fclose($f);
return $result;
});
return Arr::get(($array ? $array->get($string) : []),
$key,
__('No description available, can you help with one?'));
return Arr::get(($array ? $array->get($string) : []),$key);
}
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
{
// @note Schema attributes cannot be edited
return view('components.attribute.internal')
->with('o',$this);
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace App\Classes\LDAP\Attribute\Schema;
use Illuminate\Contracts\View\View;
use App\Classes\LDAP\Attribute\Schema;
use App\Classes\Template;
/**
* Represents a Generic Schema Attribute
*/
class Generic extends Schema
{
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
{
// @note Schema attributes cannot be edited
return view('components.attribute.schema.generic')
->with('o',$this);
}
}

View File

@ -5,7 +5,6 @@ namespace App\Classes\LDAP\Attribute\Schema;
use Illuminate\Contracts\View\View;
use App\Classes\LDAP\Attribute\Schema;
use App\Classes\Template;
/**
* Represents a Mechanisms Attribute
@ -34,7 +33,7 @@ final class Mechanisms extends Schema
return parent::_get(config_path('ldap_supported_saslmechanisms.txt'),$string,$key);
}
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
{
// @note Schema attributes cannot be edited
return view('components.attribute.schema.mechanisms')

View File

@ -5,7 +5,6 @@ namespace App\Classes\LDAP\Attribute\Schema;
use Illuminate\Contracts\View\View;
use App\Classes\LDAP\Attribute\Schema;
use App\Classes\Template;
/**
* Represents an OID Attribute
@ -35,7 +34,7 @@ final class OID extends Schema
return parent::_get(config_path('ldap_supported_oids.txt'),$string,$key);
}
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
{
// @note Schema attributes cannot be edited
return view('components.attribute.schema.oid')

View File

@ -29,7 +29,7 @@ abstract class Export
abstract public function __toString(): string;
protected function header(): string
protected function header()
{
$output = '';
@ -42,7 +42,7 @@ abstract class Export
//$output .= sprintf('# %s: %s',__('Search Filter'),$this->entry->dn).$this->br;
$output .= sprintf('# %s: %s',__('Total Entries'),$this->items->count()).$this->br;
$output .= '#'.$this->br;
$output .= sprintf('# %s %s (%s) on %s',__('Generated by'),config('app.name'),request()->root(),date('F j, Y g:i a')).$this->br;
$output .= sprintf('# %s %s (%s) on %s',__('Generated by'),config('app.name'),config('app.url'),date('F j, Y g:i a')).$this->br;
$output .= sprintf('# %s %s',__('Exported by'),Auth::user() ?: 'Anonymous').$this->br;
$output .= sprintf('# %s: %s',__('Version'),config('app.version')).$this->br;

View File

@ -5,7 +5,6 @@ namespace App\Classes\LDAP\Export;
use Illuminate\Support\Str;
use App\Classes\LDAP\Export;
use App\Ldap\Entry;
/**
* Export from LDAP using an LDIF format
@ -42,25 +41,13 @@ class LDIF extends Export
// Display Attributes
foreach ($o->getObjects() as $ao) {
if ($ao->no_attr_tags)
foreach ($ao->values as $value) {
$result .= $this->multiLineDisplay(
Str::isAscii($value)
? sprintf('%s: %s',$ao->name,$value)
: sprintf('%s:: %s',$ao->name,base64_encode($value))
,$this->br);
}
else
foreach ($ao->values as $tag => $tagvalues) {
foreach ($tagvalues as $value) {
$result .= $this->multiLineDisplay(
Str::isAscii($value)
? sprintf('%s: %s',$ao->name.(($tag !== Entry::TAG_NOTAG) ? ';'.$tag : ''),$value)
: sprintf('%s:: %s',$ao->name.(($tag !== Entry::TAG_NOTAG) ? ';'.$tag : ''),base64_encode($value))
,$this->br);
}
}
foreach ($ao->values as $value) {
$result .= $this->multiLineDisplay(
Str::isAscii($value)
? sprintf('%s: %s',$ao->name,$value)
: sprintf('%s:: %s',$ao->name,base64_encode($value))
,$this->br);
}
}
}

View File

@ -3,10 +3,9 @@
namespace App\Classes\LDAP;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use LdapRecord\LdapRecordException;
use App\Exceptions\Import\GeneralException;
use App\Exceptions\Import\ObjectExistsException;
use App\Ldap\Entry;
/**
@ -17,8 +16,6 @@ use App\Ldap\Entry;
*/
abstract class Import
{
private const LOGKEY = 'aI-';
// Valid LDIF commands
protected const LDAP_IMPORT_ADD = 1;
protected const LDAP_IMPORT_DELETE = 2;
@ -51,6 +48,7 @@ abstract class Import
* @param int $action
* @return Collection
* @throws GeneralException
* @throws ObjectExistsException
*/
final protected function commit(Entry $o,int $action): Collection
{
@ -59,30 +57,17 @@ abstract class Import
try {
$o->save();
} catch (LdapRecordException $e) {
Log::error(sprintf('%s:Import Commit Error',self::LOGKEY),['e'=>$e->getMessage(),'detailed'=>$e->getDetailedError()]);
if ($e->getDetailedError())
return collect([
'dn'=>$o->getDN(),
'result'=>sprintf('%d: %s (%s)',
($x=$e->getDetailedError())->getErrorCode(),
$x->getErrorMessage(),
$x->getDiagnosticMessage(),
)
]);
else
return collect([
'dn'=>$o->getDN(),
'result'=>sprintf('%d: %s',
$e->getCode(),
$e->getMessage(),
)
]);
} catch (\Exception $e) {
return collect([
'dn'=>$o->getDN(),
'result'=>sprintf('%d: %s (%s)',
($x=$e->getDetailedError())->getErrorCode(),
$x->getErrorMessage(),
$x->getDiagnosticMessage(),
)
]);
}
Log::debug(sprintf('%s:Import Commited',self::LOGKEY));
return collect(['dn'=>$o->getDN(),'result'=>__('Created')]);
default:

View File

@ -35,7 +35,7 @@ class LDIF extends Import
// @todo When renaming DNs, the hotlink should point to the new entry on success, or the old entry on failure.
foreach (preg_split('/(\r?\n|\r)/',$this->input) as $line) {
$c++;
Log::debug(sprintf('%s:LDIF Line [%s]',self::LOGKEY,$line));
Log::debug(sprintf('%s: LDIF Line [%s]',self::LOGKEY,$line));
$line = trim($line);
// If the line starts with a comment, ignore it
@ -46,9 +46,9 @@ class LDIF extends Import
if (! $line) {
if (! is_null($o)) {
// Add the last attribute;
$o->addAttributeItem($attribute,$base64encoded ? base64_decode($value) : $value);
$o->addAttribute($attribute,$base64encoded ? base64_decode($value) : $value);
Log::debug(sprintf('%s:- Committing Entry [%s]',self::LOGKEY,$o->getDN()));
Log::debug(sprintf('%s: Committing Entry [%s]',self::LOGKEY,$o->getDN()));
// Commit
$result->push($this->commit($o,$action));
@ -59,6 +59,8 @@ class LDIF extends Import
$base64encoded = FALSE;
$attribute = NULL;
$value = '';
// Else its a blank line
}
continue;
@ -67,7 +69,7 @@ class LDIF extends Import
$m = [];
preg_match('/^([a-zA-Z0-9;-]+)(:+)\s+(.*)$/',$line,$m);
switch (Arr::get($m,1)) {
switch ($x=Arr::get($m,1)) {
case 'changetype':
if ($m[2] !== ':')
throw new GeneralException(sprintf('ChangeType cannot be base64 encoded set at [%d]. (line %d)',$version,$c));
@ -95,7 +97,7 @@ class LDIF extends Import
// If $m is NULL, then this is the 2nd (or more) line of a base64 encoded value
if (! $m) {
$value .= $line;
Log::debug(sprintf('%s:- Attribute [%s] adding [%s] (%d)',self::LOGKEY,$attribute,$line,$c));
Log::debug(sprintf('%s: Attribute [%s] adding [%s] (%d)',self::LOGKEY,$attribute,$line,$c));
// add to last attr value
continue 2;
@ -108,7 +110,7 @@ class LDIF extends Import
throw new GeneralException(sprintf('Previous Entry not complete? (line %d)',$c));
$dn = $base64encoded ? base64_decode($value) : $value;
Log::debug(sprintf('%s:Creating new entry:',self::LOGKEY,$dn));
Log::debug(sprintf('%s: Creating new entry:',self::LOGKEY,$dn));
//$o = Entry::find($dn);
// If it doesnt exist, we'll create it
@ -120,10 +122,10 @@ class LDIF extends Import
$action = self::LDAP_IMPORT_ADD;
} else {
Log::debug(sprintf('%s:Adding Attribute [%s] value [%s] (%d)',self::LOGKEY,$attribute,$value,$c));
Log::debug(sprintf('%s: Adding Attribute [%s] value [%s] (%d)',self::LOGKEY,$attribute,$value,$c));
if ($value)
$o->addAttributeItem($attribute,$base64encoded ? base64_decode($value) : $value);
$o->addAttribute($attribute,$base64encoded ? base64_decode($value) : $value);
else
throw new GeneralException(sprintf('Attribute has no value [%s] (line %d)',$attribute,$c));
}
@ -131,10 +133,11 @@ class LDIF extends Import
// Start of a new attribute
$base64encoded = ($m[2] === '::');
// @todo Need to parse attributes with ';' options
$attribute = $m[1];
$value = $m[3];
Log::debug(sprintf('%s:- New Attribute [%s] with [%s] (%d)',self::LOGKEY,$attribute,$value,$c));
Log::debug(sprintf('%s: New Attribute [%s] with [%s] (%d)',self::LOGKEY,$attribute,$value,$c));
}
if ($version !== 1)
@ -144,9 +147,9 @@ class LDIF extends Import
// We may still have a pending action
if ($action) {
// Add the last attribute;
$o->addAttributeItem($attribute,$base64encoded ? base64_decode($value) : $value);
$o->addAttribute($attribute,$base64encoded ? base64_decode($value) : $value);
Log::debug(sprintf('%s:- Committing Entry [%s]',self::LOGKEY,$o->getDN()));
Log::debug(sprintf('%s: Committing Entry [%s]',self::LOGKEY,$o->getDN()));
// Commit
$result->push($this->commit($o,$action));
@ -156,8 +159,8 @@ class LDIF extends Import
return $result;
}
public function xreadEntry() {
static $haveVersion = FALSE;
public function readEntry() {
static $haveVersion = false;
if ($lines = $this->nextLines()) {
@ -176,7 +179,7 @@ class LDIF extends Import
} else
$changetype = 'add';
$this->template = new Template($this->server_id,NULL,NULL,$changetype);
$this->template = new Template($this->server_id,null,null,$changetype);
switch ($changetype) {
case 'add':
@ -198,7 +201,7 @@ class LDIF extends Import
return $this->error(sprintf('%s %s',_('DN does not exist'),$dn),$lines);
$this->template->setDN($dn);
$this->template->accept(FALSE,TRUE);
$this->template->accept(false,true);
return $this->getModifyDetails($lines);
@ -218,13 +221,13 @@ class LDIF extends Import
default:
if (! $server->dnExists($dn))
return $this->error(_('Unknown change type'),$lines);
return $this->error(_('Unkown change type'),$lines);
}
} else
return $this->error(_('A valid dn line is required'),$lines);
} else
return FALSE;
return false;
}
}

View File

@ -6,75 +6,302 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use App\Classes\LDAP\Attribute;
use App\Exceptions\InvalidUsage;
use App\Ldap\Entry;
/**
* Represents an LDAP AttributeType
*
* @package phpLDAPadmin
* @subpackage Schema
*/
final class AttributeType extends Base
{
private const LOGKEY = 'SAT';
final class AttributeType extends Base {
// The attribute from which this attribute inherits (if any)
private ?string $sup_attribute = NULL;
// An array of AttributeTypes which inherit from this one
private(set) Collection $children;
// Array of AttributeTypes which inherit from this one
private Collection $children;
// The equality rule used
private(set) ?string $equality = NULL;
// This attribute has been forced a MAY attribute by the configuration.
private(set) bool $forced_as_may = FALSE;
// boolean: is collective?
private(set) bool $is_collective = FALSE;
// Is this a must attribute
private(set) bool $is_must = FALSE;
// boolean: can use modify?
private(set) bool $is_no_user_modification = FALSE;
// boolean: is single valued only?
private(set) bool $is_single_value = FALSE;
// The max number of characters this attribute can be
private(set) ?int $max_length = NULL;
// An array of names (including aliases) that this attribute is known by
private(set) Collection $names;
private ?string $equality = NULL;
// The ordering of the attributeType
private(set) ?string $ordering = NULL;
// A list of object class names that require this attribute type.
private(set) Collection $required_by_object_classes;
// Which objectclass is defining this attribute for an Entry
public ?string $source = NULL;
private ?string $ordering = NULL;
// Supports substring matching?
private(set) ?string $sub_str_rule = NULL;
// The attribute from which this attribute inherits (if any)
private(set) ?string $sup_attribute = NULL;
private ?string $sub_str_rule = NULL;
// The full syntax string, ie 1.2.3.4{16}
private(set) ?string $syntax = NULL;
private(set) ?string $syntax_oid = NULL;
private ?string $syntax = NULL;
private ?string $syntax_oid = NULL;
// boolean: is single valued only?
private bool $is_single_value = FALSE;
// boolean: is collective?
private bool $is_collective = FALSE;
// boolean: can use modify?
private bool $is_no_user_modification = FALSE;
// The usage string set by the LDAP schema
private(set) ?string $usage = NULL;
private ?string $usage = NULL;
// An array of alias attribute names, strings
private Collection $aliases;
// The max number of characters this attribute can be
private ?int $max_length = NULL;
// A string description of the syntax type (taken from the LDAPSyntaxes)
/**
* @deprecated - reference syntaxes directly if possible
* @var string
*/
private ?string $type = NULL;
// An array of objectClasses which use this attributeType (must be set by caller)
private(set) Collection $used_in_object_classes;
private Collection $used_in_object_classes;
// A list of object class names that require this attribute type.
private Collection $required_by_object_classes;
// This attribute has been forced a MAY attribute by the configuration.
private bool $forced_as_may = FALSE;
/**
* Creates a new AttributeType object from a raw LDAP AttributeType string.
*
* eg: ( 2.5.4.0 NAME 'objectClass' DESC 'RFC4512: object classes of the entity' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )
*/
public function __construct(string $line) {
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('Parsing AttributeType [%s]',$line));
parent::__construct($line);
$strings = preg_split('/[\s,]+/',$line,-1,PREG_SPLIT_DELIM_CAPTURE);
// Init
$this->children = collect();
$this->aliases = collect();
$this->used_in_object_classes = collect();
$this->required_by_object_classes = collect();
for ($i=0; $i < count($strings); $i++) {
switch ($strings[$i]) {
case '(':
case ')':
break;
case 'NAME':
// @note Some schema's return a (' instead of a ( '
if ($strings[$i+1] != '(' && ! preg_match('/^\(/',$strings[$i+1])) {
do {
$this->name .= ($this->name ? ' ' : '').$strings[++$i];
} while (! preg_match("/\'$/s",$strings[$i]));
// This attribute has no aliases
//$this->aliases = collect();
} else {
$i++;
do {
// In case we came here becaues of a ('
if (preg_match('/^\(/',$strings[$i]))
$strings[$i] = preg_replace('/^\(/','',$strings[$i]);
else
$i++;
$this->name .= ($this->name ? ' ' : '').$strings[++$i];
} while (! preg_match("/\'$/s",$strings[$i]));
// Add alias names for this attribute
while ($strings[++$i] != ')') {
$alias = $strings[$i];
$alias = preg_replace("/^\'(.*)\'$/",'$1',$alias);
$this->addAlias($alias);
}
}
$this->name = preg_replace("/^\'(.*)\'$/",'$1',$this->name);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case NAME returned (%s)',$this->name),['aliases'=>$this->aliases]);
break;
case 'DESC':
do {
$this->description .= ($this->description ? ' ' : '').$strings[++$i];
} while (! preg_match("/\'$/s",$strings[$i]));
$this->description = preg_replace("/^\'(.*)\'$/",'$1',$this->description);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case DESC returned (%s)',$this->description));
break;
case 'OBSOLETE':
$this->is_obsolete = TRUE;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case OBSOLETE returned (%s)',$this->is_obsolete));
break;
case 'SUP':
$i++;
$this->sup_attribute = preg_replace("/^\'(.*)\'$/",'$1',$strings[$i]);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case SUP returned (%s)',$this->sup_attribute));
break;
case 'EQUALITY':
$this->equality = $strings[++$i];
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case EQUALITY returned (%s)',$this->equality));
break;
case 'ORDERING':
$this->ordering = $strings[++$i];
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case ORDERING returned (%s)',$this->ordering));
break;
case 'SUBSTR':
$this->sub_str_rule = $strings[++$i];
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case SUBSTR returned (%s)',$this->sub_str_rule));
break;
case 'SYNTAX':
$this->syntax = $strings[++$i];
$this->syntax_oid = preg_replace('/{\d+}$/','',$this->syntax);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('/ Evaluating SYNTAX returned (%s) [%s]',$this->syntax,$this->syntax_oid));
// Does this SYNTAX string specify a max length (ie, 1.2.3.4{16})
$m = [];
if (preg_match('/{(\d+)}$/',$this->syntax,$m))
$this->max_length = $m[1];
else
$this->max_length = NULL;
if ($i < count($strings) - 1 && $strings[$i+1] == '{')
do {
$this->name .= ' '.$strings[++$i];
} while ($strings[$i] != '}');
$this->syntax = preg_replace("/^\'(.*)\'$/",'$1',$this->syntax);
$this->syntax_oid = preg_replace("/^\'(.*)\'$/",'$1',$this->syntax_oid);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case SYNTAX returned (%s) [%s] {%d}',$this->syntax,$this->syntax_oid,$this->max_length));
break;
case 'SINGLE-VALUE':
$this->is_single_value = TRUE;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case SINGLE-VALUE returned (%s)',$this->is_single_value));
break;
case 'COLLECTIVE':
$this->is_collective = TRUE;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case COLLECTIVE returned (%s)',$this->is_collective));
break;
case 'NO-USER-MODIFICATION':
$this->is_no_user_modification = TRUE;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case NO-USER-MODIFICATION returned (%s)',$this->is_no_user_modification));
break;
case 'USAGE':
$this->usage = $strings[++$i];
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case USAGE returned (%s)',$this->usage));
break;
// @note currently not captured
case 'X-ORDERED':
if (static::DEBUG_VERBOSE)
Log::error(sprintf('- Case X-ORDERED returned (%s)',$strings[++$i]));
break;
// @note currently not captured
case 'X-ORIGIN':
$value = '';
do {
$value .= ($value ? ' ' : '').$strings[++$i];
} while (! preg_match("/\'$/s",$strings[$i]));
if (static::DEBUG_VERBOSE)
Log::error(sprintf('- Case X-ORIGIN returned (%s)',$value));
break;
default:
if (preg_match('/[\d\.]+/i',$strings[$i]) && ($i === 1)) {
$this->oid = $strings[$i];
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case default returned (%s)',$this->oid));
} elseif ($strings[$i])
Log::alert(sprintf('! Case default discovered a value NOT parsed (%s)',$strings[$i]),['line'=>$line]);
}
}
}
public function __clone()
{
// When we clone, we need to break the reference too
$this->aliases = clone $this->aliases;
}
public function __get(string $key): mixed
{
return match ($key) {
'names_lc' => $this->names->map('strtolower'),
default => parent::__get($key)
};
switch ($key) {
case 'aliases': return $this->aliases;
case 'children': return $this->children;
case 'forced_as_may': return $this->forced_as_may;
case 'is_collective': return $this->is_collective;
case 'is_editable': return ! $this->is_no_user_modification;
case 'is_no_user_modification': return $this->is_no_user_modification;
case 'is_single_value': return $this->is_single_value;
case 'equality': return $this->equality;
case 'max_length': return $this->max_length;
case 'ordering': return $this->ordering;
case 'required_by_object_classes': return $this->required_by_object_classes;
case 'sub_str_rule': return $this->sub_str_rule;
case 'sup_attribute': return $this->sup_attribute;
case 'syntax': return $this->syntax;
case 'syntax_oid': return $this->syntax_oid;
case 'type': return $this->type;
case 'usage': return $this->usage;
case 'used_in_object_classes': return $this->used_in_object_classes;
default: return parent::__get($key);
}
}
/**
* Adds an attribute name to the alias array.
*
* @param string $alias The name of a new attribute to add to this attribute's list of aliases.
*/
public function addAlias(string $alias): void
{
$this->aliases->push($alias);
}
/**
@ -85,8 +312,7 @@ final class AttributeType extends Base
*/
public function addChild(string $child): void
{
$this->children
->push($child);
$this->children->push($child);
}
/**
@ -94,12 +320,11 @@ final class AttributeType extends Base
* that is the list of objectClasses which must have this attribute.
*
* @param string $name The name of the objectClass to add.
* @param bool $structural
*/
public function addRequiredByObjectClass(string $name,bool $structural): void
public function addRequiredByObjectClass(string $name): void
{
if (! $this->required_by_object_classes->has($name))
$this->required_by_object_classes->put($name,$structural);
if (! $this->required_by_object_classes->contains($name))
$this->required_by_object_classes->push($name);
}
/**
@ -107,7 +332,6 @@ final class AttributeType extends Base
* that is the list of objectClasses which provide this attribute.
*
* @param string $name The name of the objectClass to add.
* @param bool $structural
*/
public function addUsedInObjectClass(string $name,bool $structural): void
{
@ -115,176 +339,179 @@ final class AttributeType extends Base
$this->used_in_object_classes->put($name,$structural);
}
private function factory(): Attribute
/**
* Gets the names of attributes that are an alias for this attribute (if any).
*
* @return Collection An array of names of attributes which alias this attribute or
* an empty array if no attribute aliases this object.
* @deprecated use class->aliases
*/
public function getAliases(): Collection
{
return Attribute\Factory::create(
dn:'',
attribute:$this->name,
values:[]);
return $this->aliases;
}
/**
* For a list of objectclasses return all parent objectclasses as well
* Gets this attribute's equality string
*
* @param Collection $ocs
* @return Collection
* @return string
* @deprecated use $this->equality
*/
private function heirachy(Collection $ocs): Collection
public function getEquality()
{
$result = collect();
foreach ($ocs as $oc) {
$item = config('server')
->schema('objectclasses',$oc);
$result = $result
->merge($item
->getParents(TRUE)
->pluck('oid'))
->push($item->oid);
}
return $result;
return $this->equality;
}
/**
* Creates a new AttributeType object from a raw LDAP AttributeType string.
* Gets whether this attribute is collective.
*
* eg: ( 2.5.4.0 NAME 'objectClass' DESC 'RFC4512: object classes of the entity' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )
* @return boolean Returns TRUE if this attribute is collective and FALSE otherwise.
* @deprecated use $this->is_collective
*/
protected function parse(string $line): void
public function getIsCollective(): bool
{
Log::debug(sprintf('%s:Parsing AttributeType [%s]',self::LOGKEY,$line));
// Init
$this->names = collect();
$this->children = collect();
$this->used_in_object_classes = collect();
$this->required_by_object_classes = collect();
parent::parse($line);
return $this->is_collective;
}
protected function parse_chunk(array $strings,int &$i): void
/**
* Gets whether this attribute is not modifiable by users.
*
* @return boolean Returns TRUE if this attribute is not modifiable by users.
* @deprecated use $this->is_no_user_modification
*/
public function getIsNoUserModification(): bool
{
switch ($strings[$i]) {
case 'NAME':
$name = '';
return $this->is_no_user_modification;
}
// @note Some schema's return a (' instead of a ( '
// @note This attribute format has no aliases
if ($strings[$i+1] !== '(' && ! preg_match('/^\(/',$strings[$i+1])) {
do {
$name .= ($name ? ' ' : '').$strings[++$i];
/**
* Gets whether this attribute is single-valued. If this attribute only supports single values, TRUE
* is returned. If this attribute supports multiple values, FALSE is returned.
*
* @return boolean Returns TRUE if this attribute is single-valued or FALSE otherwise.
* @deprecated use class->is_single_value
*/
public function getIsSingleValue(): bool
{
return $this->is_single_value;
}
} while (! preg_match("/\'$/s",$strings[$i]));
/**
* Gets this attribute's the maximum length. If no maximum is defined by the LDAP server, NULL is returned.
*
* @return int The maximum length (in characters) of this attribute or NULL if no maximum is specified.
* @deprecated use $this->max_length;
*/
public function getMaxLength()
{
return $this->max_length;
}
} else {
$i++;
/**
* Gets this attribute's ordering specification.
*
* @return string
* @deprecated use $this->ordering
*/
public function getOrdering(): string
{
return $this->ordering;
}
do {
// In case we came here because of a ('
if (preg_match('/^\(/',$strings[$i]))
$strings[$i] = preg_replace('/^\(/','',$strings[$i]);
else
$i++;
/**
* Gets this attribute's substring matching specification
*
* @return string
* @deprecated use $this->sub_str_rule;
*/
public function getSubstr() {
return $this->sub_str_rule;
}
$name .= ($name ? ' ' : '').$strings[++$i];
/**
* Gets this attribute's parent attribute (if any). If this attribute does not
* inherit from another attribute, NULL is returned.
*
* @return string
* @deprecated use $class->sup_attribute directly
*/
public function getSupAttribute() {
return $this->sup_attribute;
}
} while (! preg_match("/\'$/s",$strings[$i]));
/**
* Gets this attribute's syntax OID. Differs from getSyntaxString() in that this
* function only returns the actual OID with any length specification removed.
* Ie, if the syntax string is "1.2.3.4{16}", this function only retruns
* "1.2.3.4".
*
* @return string The syntax OID string.
* @deprecated use $this->syntax_oid;
*/
public function getSyntaxOID()
{
return $this->syntax_oid;
}
// Add alias names for this attribute
while ($strings[++$i] !== ')') {
$alias = preg_replace("/^\'(.*)\'$/",'$1',$strings[$i]);
$this->names->push($alias);
}
}
/**
* Gets this attribute's usage string as defined by the LDAP server
*
* @return string
* @deprecated use $this->usage
*/
public function getUsage()
{
return $this->usage;
}
$this->names = $this->names->push(preg_replace("/^\'(.*)\'$/",'$1',$name))->sort();
$this->forced_as_may = $this->names_lc
->intersect(array_map('strtolower',config('pla.force_may',[])))
->count() > 0;
/**
* Gets the list of "used in" objectClasses, that is the list of objectClasses
* which provide this attribute.
*
* @return Collection An array of names of objectclasses (strings) which provide this attribute
* @deprecated use $this->used_in_object_classes
*/
public function getUsedInObjectClasses(): Collection
{
return $this->used_in_object_classes;
}
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case NAME returned (%s)',self::LOGKEY,$this->name),['names'=>$this->names]);
break;
/**
* @return bool
* @deprecated use $this->forced_as_may
*/
public function isForceMay(): bool
{
return $this->forced_as_may;
}
case 'SUP':
$this->sup_attribute = preg_replace("/^\'(.*)\'$/",'$1',$strings[++$i]);
/**
* Removes an attribute name from this attribute's alias array.
*
* @param string $alias The name of the attribute to remove.
*/
public function removeAlias(string $alias): void
{
if (($x=$this->aliases->search($alias)) !== FALSE)
$this->aliases->forget($x);
}
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case SUP returned (%s)',self::LOGKEY,$this->sup_attribute));
break;
/**
* Sets this attribute's list of aliases.
*
* @param Collection $aliases The array of alias names (strings)
* @deprecated use $this->aliases =
*/
public function setAliases(Collection $aliases): void
{
$this->aliases = $aliases;
}
case 'EQUALITY':
$this->equality = $strings[++$i];
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case EQUALITY returned (%s)',self::LOGKEY,$this->equality));
break;
case 'ORDERING':
$this->ordering = $strings[++$i];
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case ORDERING returned (%s)',self::LOGKEY,$this->ordering));
break;
case 'SUBSTR':
$this->sub_str_rule = $strings[++$i];
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case SUBSTR returned (%s)',self::LOGKEY,$this->sub_str_rule));
break;
case 'SYNTAX':
$this->syntax = preg_replace("/^\'(.*)\'$/",'$1',$strings[++$i]);
$this->syntax_oid = preg_replace("/^\'?(.*){\d+}\'?$/",'$1',$this->syntax);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:/ Evaluating SYNTAX returned (%s) [%s]',self::LOGKEY,$this->syntax,$this->syntax_oid));
// Does this SYNTAX string specify a max length (ie, 1.2.3.4{16})
$m = [];
$this->max_length = preg_match('/{(\d+)}$/',$this->syntax,$m)
? $m[1]
: NULL;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case SYNTAX returned (%s) [%s] {%d}',self::LOGKEY,$this->syntax,$this->syntax_oid,$this->max_length));
break;
case 'SINGLE-VALUE':
$this->is_single_value = TRUE;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case SINGLE-VALUE returned (%s)',self::LOGKEY,$this->is_single_value));
break;
case 'COLLECTIVE':
$this->is_collective = TRUE;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case COLLECTIVE returned (%s)',self::LOGKEY,$this->is_collective));
break;
case 'NO-USER-MODIFICATION':
$this->is_no_user_modification = TRUE;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case NO-USER-MODIFICATION returned (%s)',self::LOGKEY,$this->is_no_user_modification));
break;
case 'USAGE':
$this->usage = $strings[++$i];
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case USAGE returned (%s)',self::LOGKEY,$this->usage));
break;
default:
parent::parse_chunk($strings,$i);
}
/**
* This function will mark this attribute as a forced MAY attribute
*/
public function setForceMay() {
$this->forced_as_may = TRUE;
}
/**
@ -298,28 +525,13 @@ final class AttributeType extends Base
}
/**
* If this is a MUST attribute to the objectclass that defines it
* Sets this attribute's SUP attribute (ie, the attribute from which this attribute inherits).
*
* @return void
* @param string $attr The name of the new parent (SUP) attribute
*/
public function setMust(): void
public function setSupAttribute(string $attr): void
{
$this->is_must = TRUE;
}
/**
* Sets this attribute's name.
*
* @param string $name The new name to give this attribute.
* @throws InvalidUsage
*/
public function setName(string $name): void
{
// Quick validation
if ($this->names_lc->count() && (! $this->names_lc->contains(strtolower($name))))
throw new InvalidUsage(sprintf('Cannot set attribute name to [%s], its not an alias for [%s]',$name,$this->names->join(',')));
$this->name = $name;
$this->sup_attribute = trim($attr);
}
/**
@ -332,22 +544,21 @@ final class AttributeType extends Base
*/
public function validation(array $array): ?array
{
// For each item in array, we need to get the OC hierarchy
$heirachy = $this->heirachy(collect($array)
// For each item in array, we need to get the OC heirachy
$heirachy = collect($array)
->filter()
->map(fn($item)=>config('server')
->schema('objectclasses',$item)
->getSupClasses()
->push($item))
->flatten()
->filter());
->unique();
// Get any config validation
$validation = collect(Arr::get(config('ldap.validation'),$this->name_lc,[]));
$nolangtag = sprintf('%s.%s.0',$this->name_lc,Entry::TAG_NOTAG);
// Add in schema required by conditions
if (($heirachy->intersect($this->required_by_object_classes->keys())->count() > 0)
if (($heirachy->intersect($this->required_by_object_classes)->count() > 0)
&& (! collect($validation->get($this->name_lc))->contains('required'))) {
$validation
->prepend(array_merge(['required','min:1'],$validation->get($nolangtag,[])),$nolangtag)
->prepend(array_merge(['required','array','min:1',($this->factory()->no_attr_tags ? 'max:1' : NULL)],$validation->get($this->name_lc,[])),$this->name_lc);
$validation->put($this->name_lc,array_merge(['required','min:1'],$validation->get($this->name_lc,[])))
->put($this->name_lc.'.*',array_merge(['required','min:1'],$validation->get($this->name_lc.'.*',[])));
}
return $validation->toArray();

View File

@ -2,8 +2,6 @@
namespace App\Classes\LDAP\Schema;
use Illuminate\Support\Facades\Log;
use App\Exceptions\InvalidUsage;
/**
@ -12,38 +10,38 @@ use App\Exceptions\InvalidUsage;
* A schema item is an ObjectClass, an AttributeBype, a MatchingRule, or a Syntax.
* All schema items have at least two things in common: An OID and a Description.
*/
abstract class Base
{
private const LOGKEY = 'Sb-';
abstract class Base {
protected const DEBUG_VERBOSE = FALSE;
// Record the LDAP String
private(set) string $line;
private string $line;
// The schema item's name.
protected(set) string $name = '';
protected string $name = '';
// The OID of this schema item.
protected(set) string $oid = '';
protected string $oid;
# The description of this schema item.
protected(set) string $description = '';
protected string $description = '';
// Boolean value indicating whether this objectClass is obsolete
private(set) bool $is_obsolete = FALSE;
private bool $is_obsolete = FALSE;
public function __construct(string $line)
{
$this->line = $line;
$this->parse($line);
}
public function __get(string $key): mixed
{
switch ($key) {
case 'description': return $this->description;
case 'is_obsolete': return $this->is_obsolete;
case 'line': return $this->line;
case 'name': return $this->name;
case 'name_lc': return strtolower($this->name);
case 'oid': return $this->oid;
default:
throw new InvalidUsage('Unknown key:'.$key);
@ -56,95 +54,69 @@ abstract class Base
}
public function __toString(): string
{
return $this->name;
}
/**
* @return string
* @deprecated replace with $class->description
*/
public function getDescription(): string
{
return $this->description;
}
/**
* Gets whether this item is flagged as obsolete by the LDAP server.
*
* @deprecated replace with $this->is_obsolete
*/
public function getIsObsolete(): bool
{
return $this->is_obsolete;
}
/**
* Return the objects name.
*
* @param boolean $lower Return the name in lower case (default)
* @return string The name
* @deprecated use object->name
*/
public function getName(bool $lower=TRUE): string
{
return $lower ? strtolower($this->name) : $this->name;
}
/**
* Return the objects name.
*
* @return string The name
* @deprecated use object->oid
*/
public function getOID(): string
{
return $this->oid;
}
protected function parse(string $line): void
public function setDescription(string $desc): void
{
$strings = preg_split('/[\s,]+/',$line,-1,PREG_SPLIT_DELIM_CAPTURE);
for ($i=0; $i < count($strings); $i++) {
$this->parse_chunk($strings,$i);
}
$this->description = $desc;
}
protected function parse_chunk(array $strings,int &$i): void
/**
* Sets this attribute's name.
*
* @param string $name The new name to give this attribute.
*/
public function setName($name): void
{
switch ($strings[$i]) {
case '(':
case ')':
break;
$this->name = $name;
}
case 'NAME':
if ($strings[$i+1] !== '(') {
do {
$this->name .= (strlen($this->name) ? ' ' : '').$strings[++$i];
} while (! preg_match('/\'$/s',$strings[$i]));
} else {
$i++;
do {
$this->name .= (strlen($this->name) ? ' ' : '').$strings[++$i];
} while (! preg_match('/\'$/s',$strings[$i]));
do {
$i++;
} while (! preg_match('/\)+\)?/',$strings[$i]));
}
$this->name = preg_replace("/^\'(.*)\'$/",'$1',$this->name);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case NAME returned (%s)',self::LOGKEY,$this->name));
break;
case 'DESC':
do {
$this->description .= (strlen($this->description) ? ' ' : '').$strings[++$i];
} while (! preg_match('/\'$/s',$strings[$i]));
$this->description = preg_replace("/^\'(.*)\'$/",'$1',$this->description);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case DESC returned (%s)',self::LOGKEY,$this->description));
break;
case 'OBSOLETE':
$this->is_obsolete = TRUE;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case OBSOLETE returned (%s)',self::LOGKEY,$this->is_obsolete));
break;
// @note currently not captured
case 'X-SUBST':
case 'X-ORDERED':
case 'X-EQUALITY':
case 'X-ORIGIN':
$value = '';
do {
$value .= ($value ? ' ' : '').preg_replace('/^\'(.+)\'$/','$1',$strings[++$i]);
} while (! preg_match("/\'$/s",$strings[$i]));
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case [%s] returned (%s) - IGNORED',self::LOGKEY,$strings[$i],$value));
break;
default:
if (preg_match('/[\d\.]+/i',$strings[$i]) && ($i === 1)) {
$this->oid = $strings[$i];
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case default returned OID (%s)',self::LOGKEY,$this->oid));
} elseif ($strings[$i])
Log::alert(sprintf('%s:! Case default discovered a value NOT parsed (%s)',self::LOGKEY,$strings[$i]));
}
public function setOID(string $oid): void
{
$this->oid = $oid;
}
}

View File

@ -6,49 +6,74 @@ use Illuminate\Support\Facades\Log;
/**
* Represents an LDAP Syntax
*
* @package phpLDAPadmin
* @subpackage Schema
*/
final class LDAPSyntax extends Base
{
private const LOGKEY = 'SLS';
final class LDAPSyntax extends Base {
// Is human readable?
private(set) ?bool $is_not_human_readable = NULL;
private ?bool $is_not_human_readable = NULL;
// Binary transfer required?
private(set) ?bool $binary_transfer_required = NULL;
private ?bool $binary_transfer_required = NULL;
/**
* Creates a new Syntax object from a raw LDAP syntax string.
*/
protected function parse(string $line): void
{
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:Parsing LDAPSyntax [%s]',self::LOGKEY,$line));
public function __construct(string $line) {
Log::debug(sprintf('Parsing LDAPSyntax [%s]',$line));
parent::parse($line);
}
parent::__construct($line);
$strings = preg_split('/[\s,]+/',$line,-1,PREG_SPLIT_DELIM_CAPTURE);
protected function parse_chunk(array $strings,int &$i): void
{
for ($i=0; $i<count($strings); $i++) {
switch($strings[$i]) {
case '(':
case ')':
break;
case 'DESC':
do {
$this->description .= (strlen($this->description) ? ' ' : '').$strings[++$i];
} while (! preg_match("/\'$/s",$strings[$i]));
$this->description = preg_replace("/^\'(.*)\'$/",'$1',$this->description);
Log::debug(sprintf('- Case DESC returned (%s)',$this->description));
break;
case 'X-BINARY-TRANSFER-REQUIRED':
$this->binary_transfer_required = (str_replace("'",'',$strings[++$i]) === 'TRUE');
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case X-BINARY-TRANSFER-REQUIRED returned (%s)',self::LOGKEY,$this->binary_transfer_required));
Log::debug(sprintf('- Case X-BINARY-TRANSFER-REQUIRED returned (%s)',$this->binary_transfer_required));
break;
case 'X-NOT-HUMAN-READABLE':
$this->is_not_human_readable = (str_replace("'",'',$strings[++$i]) === 'TRUE');
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case X-NOT-HUMAN-READABLE returned (%s)',self::LOGKEY,$this->is_not_human_readable));
Log::debug(sprintf('- Case X-NOT-HUMAN-READABLE returned (%s)',$this->is_not_human_readable));
break;
default:
parent::parse_chunk($strings,$i);
if (preg_match('/[\d\.]+/i',$strings[$i]) && ($i === 1)) {
$this->oid = $strings[$i];
Log::debug(sprintf('- Case default returned (%s)',$this->oid));
} elseif ($strings[$i])
Log::alert(sprintf('! Case default discovered a value NOT parsed (%s)',$strings[$i]),['line'=>$line]);
}
}
}
public function __get(string $key): mixed
{
switch ($key) {
case 'binary_transfer_required': return $this->binary_transfer_required;
case 'is_not_human_readable': return $this->is_not_human_readable;
default: return parent::__get($key);
}
}
}

View File

@ -7,16 +7,106 @@ use Illuminate\Support\Facades\Log;
/**
* Represents an LDAP MatchingRule
*
* @package phpLDAPadmin
* @subpackage Schema
*/
final class MatchingRule extends Base
{
private const LOGKEY = 'SMR';
final class MatchingRule extends Base {
// This rule's syntax OID
private(set) ?string $syntax = NULL;
private ?string $syntax = NULL;
// An array of attribute names who use this MatchingRule
private(set) Collection $used_by_attrs;
private Collection $used_by_attrs;
/**
* Creates a new MatchingRule object from a raw LDAP MatchingRule string.
*/
function __construct(string $line) {
Log::debug(sprintf('Parsing MatchingRule [%s]',$line));
parent::__construct($line);
$strings = preg_split('/[\s,]+/',$line,-1,PREG_SPLIT_DELIM_CAPTURE);
// Init
$this->used_by_attrs = collect();
for ($i=0; $i<count($strings); $i++) {
switch ($strings[$i]) {
case '(':
case ')':
break;
case 'NAME':
if ($strings[$i+1] != '(') {
do {
$this->name .= (strlen($this->name) ? ' ' : '').$strings[++$i];
} while (! preg_match("/\'$/s",$strings[$i]));
} else {
$i++;
do {
$this->name .= (strlen($this->name) ? ' ' : '').$strings[++$i];
} while (! preg_match("/\'$/s",$strings[$i]));
do {
$i++;
} while (! preg_match('/\)+\)?/',$strings[$i]));
}
$this->name = preg_replace("/^\'/",'',$this->name);
$this->name = preg_replace("/\'$/",'',$this->name);
Log::debug(sprintf(sprintf('- Case NAME returned (%s)',$this->name)));
break;
case 'DESC':
do {
$this->description .= (strlen($this->description) ? ' ' : '').$strings[++$i];
} while (! preg_match("/\'$/s",$strings[$i]));
$this->description = preg_replace("/^\'(.*)\'$/",'$1',$this->description);
Log::debug(sprintf('- Case DESC returned (%s)',$this->description));
break;
case 'OBSOLETE':
$this->is_obsolete = TRUE;
Log::debug(sprintf('- Case OBSOLETE returned (%s)',$this->is_obsolete));
break;
case 'SYNTAX':
$this->syntax = $strings[++$i];
Log::debug(sprintf('- Case SYNTAX returned (%s)',$this->syntax));
break;
default:
if (preg_match('/[\d\.]+/i',$strings[$i]) && ($i === 1)) {
$this->oid = $strings[$i];
Log::debug(sprintf('- Case default returned (%s)',$this->oid));
} elseif ($strings[$i])
Log::alert(sprintf('! Case default discovered a value NOT parsed (%s)',$strings[$i]),['line'=>$line]);
}
}
}
public function __get(string $key): mixed
{
switch ($key) {
case 'syntax': return $this->syntax;
case 'used_by_attrs': return $this->used_by_attrs;
default: return parent::__get($key);
}
}
/**
* Adds an attribute name to the list of attributes who use this MatchingRule
@ -30,33 +120,23 @@ final class MatchingRule extends Base
}
/**
* Creates a new MatchingRule object from a raw LDAP MatchingRule string.
* Gets an array of attribute names (strings) which use this MatchingRule
*
* @param string $line
* @return void
* @return array The array of attribute names (strings).
* @deprecated use $this->used_by_attrs
*/
protected function parse(string $line): void
public function getUsedByAttrs()
{
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:Parsing MatchingRule [%s]',self::LOGKEY,$line));
// Init
$this->used_by_attrs = collect();
parent::parse($line);
return $this->used_by_attrs;
}
protected function parse_chunk(array $strings,int &$i): void
/**
* Sets the list of used_by_attrs to the array specified by $attrs;
*
* @param Collection $attrs The array of attribute names (strings) which use this MatchingRule
*/
public function setUsedByAttrs(Collection $attrs): void
{
switch ($strings[$i]) {
case 'SYNTAX':
$this->syntax = $strings[++$i];
Log::debug(sprintf('- Case SYNTAX returned (%s)',$this->syntax));
break;
default:
parent::parse_chunk($strings,$i);
}
$this->used_by_attrs = $attrs;
}
}

View File

@ -0,0 +1,99 @@
<?php
namespace App\Classes\LDAP\Schema;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* Represents an LDAP schema matchingRuleUse entry
*
* @package phpLDAPadmin
* @subpackage Schema
*/
final class MatchingRuleUse extends Base {
// An array of attribute names who use this MatchingRule
private Collection $used_by_attrs;
function __construct(string $line) {
Log::debug(sprintf('Parsing MatchingRuleUse [%s]',$line));
parent::__construct($line);
$strings = preg_split('/[\s,]+/',$line,-1,PREG_SPLIT_DELIM_CAPTURE);
// Init
$this->used_by_attrs = collect();
for ($i=0; $i<count($strings); $i++) {
switch ($strings[$i]) {
case '(':
case ')':
break;
case 'NAME':
if ($strings[$i+1] != '(') {
do {
$this->name .= (strlen($this->name) ? ' ' : '').$strings[++$i];
} while (! preg_match("/\'$/s",$strings[$i]));
} else {
$i++;
do {
$this->name .= (strlen($this->name) ? ' ' : '').$strings[++$i];
} while (! preg_match("/\'$/s",$strings[$i]));
do {
$i++;
} while (! preg_match('/\)+\)?/',$strings[$i]));
}
$this->name = preg_replace("/^\'(.*)\'$/",'$1',$this->name);
Log::debug(sprintf(sprintf('- Case NAME returned (%s)',$this->name)));
break;
case 'APPLIES':
if ($strings[$i+1] != '(') {
// Has a single attribute name
$this->used_by_attrs = collect($strings[++$i]);
} else {
// Has multiple attribute names
while ($strings[++$i] != ')') {
$new_attr = $strings[++$i];
$new_attr = preg_replace("/^\'(.*)\'$/",'$1',$new_attr);
$this->used_by_attrs->push($new_attr);
}
}
Log::debug(sprintf('- Case APPLIES returned (%s)',$this->used_by_attrs->join(',')));
break;
default:
if (preg_match('/[\d\.]+/i',$strings[$i]) && ($i === 1)) {
$this->oid = $strings[$i];
Log::debug(sprintf('- Case default returned (%s)',$this->oid));
} elseif ($strings[$i])
Log::alert(sprintf('! Case default discovered a value NOT parsed (%s)',$strings[$i]),['line'=>$line]);
}
}
}
/**
* Gets an array of attribute names (strings) which use this MatchingRuleUse object.
*
* @return array The array of attribute names (strings).
* @deprecated use $this->used_by_attrs
*/
public function getUsedByAttrs()
{
return $this->used_by_attrs;
}
}

View File

@ -10,28 +10,206 @@ use App\Exceptions\InvalidUsage;
/**
* Represents an LDAP Schema objectClass
*
* @package phpLDAPadmin
* @subpackage Schema
*/
final class ObjectClass extends Base
{
private const LOGKEY = 'SOC';
// Array of objectClasses which inherit from this one
private(set) Collection $child_classes;
// Array of objectClass names from which this objectClass inherits
private(set) Collection $sup_classes;
private Collection $sup_classes;
// One of STRUCTURAL, ABSTRACT, or AUXILIARY
private int $type;
// Attributes that this objectclass defines
private(set) Collection $attributes;
// Arrays of attribute names that this objectClass requires
private Collection $must_attrs;
// Arrays of attribute names that this objectClass allows, but does not require
private Collection $may_attrs;
// Arrays of attribute names that this objectClass has been forced to MAY attrs, due to configuration
private Collection $may_force;
// Array of objectClasses which inherit from this one
private Collection $child_objectclasses;
private bool $is_obsolete;
/**
* Creates a new ObjectClass object given a raw LDAP objectClass string.
*
* eg: ( 2.5.6.0 NAME 'top' DESC 'top of the superclass chain' ABSTRACT MUST objectClass )
*
* @param string $line Schema Line
* @param Server $server
* @todo Change $server to $connection, no need to store the server object here
*/
public function __construct(string $line,Server $server)
{
parent::__construct($line);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('Parsing ObjectClass [%s]',$line));
$strings = preg_split('/[\s,]+/',$line,-1,PREG_SPLIT_DELIM_CAPTURE);
// Init
$this->may_attrs = collect();
$this->may_force = collect();
$this->must_attrs = collect();
$this->sup_classes = collect();
$this->child_objectclasses = collect();
for ($i=0; $i < count($strings); $i++) {
switch ($strings[$i]) {
case '(':
case ')':
break;
case 'NAME':
if ($strings[$i+1] != '(') {
do {
$this->name .= (strlen($this->name) ? ' ' : '').$strings[++$i];
} while (! preg_match('/\'$/s',$strings[$i]));
} else {
$i++;
do {
$this->name .= (strlen($this->name) ? ' ' : '').$strings[++$i];
} while (! preg_match('/\'$/s',$strings[$i]));
do {
$i++;
} while (! preg_match('/\)+\)?/',$strings[$i]));
}
$this->name = preg_replace("/^\'(.*)\'$/",'$1',$this->name);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf(sprintf('- Case NAME returned (%s)',$this->name)));
break;
case 'DESC':
do {
$this->description .= (strlen($this->description) ? ' ' : '').$strings[++$i];
} while (! preg_match('/\'$/s',$strings[$i]));
$this->description = preg_replace("/^\'(.*)\'$/",'$1',$this->description);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case DESC returned (%s)',$this->description));
break;
case 'OBSOLETE':
$this->is_obsolete = TRUE;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case OBSOLETE returned (%s)',$this->is_obsolete));
break;
case 'SUP':
if ($strings[$i+1] != '(') {
$this->sup_classes->push(preg_replace("/'/",'',$strings[++$i]));
} else {
$i++;
do {
$i++;
if ($strings[$i] != '$')
$this->sup_classes->push(preg_replace("/'/",'',$strings[$i]));
} while (! preg_match('/\)+\)?/',$strings[$i+1]));
}
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case SUP returned (%s)',$this->sup_classes->join(',')));
break;
case 'ABSTRACT':
$this->type = Server::OC_ABSTRACT;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case ABSTRACT returned (%s)',$this->type));
break;
case 'STRUCTURAL':
$this->type = Server::OC_STRUCTURAL;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case STRUCTURAL returned (%s)',$this->type));
break;
case 'AUXILIARY':
$this->type = Server::OC_AUXILIARY;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case AUXILIARY returned (%s)',$this->type));
break;
case 'MUST':
$attrs = collect();
$i = $this->parseList(++$i,$strings,$attrs);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('= parseList returned %d (%s)',$i,$attrs->join(',')));
foreach ($attrs as $string) {
$attr = new ObjectClassAttribute($string,$this->name);
if ($server->isForceMay($attr->getName())) {
$this->may_force->push($attr);
$this->may_attrs->push($attr);
} else
$this->must_attrs->push($attr);
}
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case MUST returned (%s) (%s)',$this->must_attrs->join(','),$this->may_force->join(',')));
break;
case 'MAY':
$attrs = collect();
$i = $this->parseList(++$i,$strings,$attrs);
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('parseList returned %d (%s)',$i,$attrs->join(',')));
foreach ($attrs as $string) {
$attr = new ObjectClassAttribute($string,$this->name);
$this->may_attrs->push($attr);
}
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case MAY returned (%s)',$this->may_attrs->join(',')));
break;
default:
if (preg_match('/[\d\.]+/i',$strings[$i]) && ($i === 1)) {
$this->oid = $strings[$i];
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('- Case default returned (%s)',$this->oid));
} elseif ($strings[$i])
Log::alert(sprintf('! Case default discovered a value NOT parsed (%s)',$strings[$i]),['line'=>$line]);
}
}
}
public function __get(string $key): mixed
{
return match ($key) {
'all_attributes' => $this->getMustAttrs(TRUE)
->merge($this->getMayAttrs(TRUE)),
'attributes' => $this->getAllAttrs(TRUE),
'sup' => $this->sup_classes,
'type_name' => match ($this->type) {
Server::OC_STRUCTURAL => 'Structural',
Server::OC_ABSTRACT => 'Abstract',
@ -42,6 +220,23 @@ final class ObjectClass extends Base
};
}
/**
* Return a list of attributes that this objectClass provides
*
* @param bool $parents
* @return Collection
* @throws InvalidUsage
*/
public function getAllAttrs(bool $parents=FALSE): Collection
{
return $this->getMustAttrs($parents)
->transform(function($item) {
$item->required = true;
return $item;
})
->merge($this->getMayAttrs($parents));
}
/**
* Adds an objectClass to the list of objectClasses that inherit
* from this objectClass.
@ -50,8 +245,57 @@ final class ObjectClass extends Base
*/
public function addChildObjectClass(string $name): void
{
if (! $this->child_classes->contains($name))
$this->child_classes->push($name);
if (! $this->child_objectclasses->contains($name))
$this->child_objectclasses->push($name);
}
/**
* Returns the array of objectClass names which inherit from this objectClass.
*
* @return Collection Names of objectClasses which inherit from this objectClass.
* @deprecated use $this->child_objectclasses
*/
public function getChildObjectClasses(): Collection
{
return $this->child_objectclasses;
}
/**
* Behaves identically to addMustAttrs, but it operates on the MAY
* attributes of this objectClass.
*
* @param array $attr An array of attribute names (strings) to add.
*/
private function addMayAttrs(array $attr): void
{
if (! is_array($attr) || ! count($attr))
return;
$this->may_attrs = $this->may_attrs->merge($attr)->unique();
}
/**
* Adds the specified array of attributes to this objectClass' list of
* MUST attributes. The resulting array of must attributes will contain
* unique members.
*
* @param array $attr An array of attribute names (strings) to add.
*/
private function addMustAttrs(array $attr): void
{
if (! is_array($attr) || ! count($attr))
return;
$this->must_attrs = $this->must_attrs->merge($attr)->unique();
}
/**
* @return Collection
* @deprecated use $this->may_force
*/
public function getForceMayAttrs(): Collection
{
return $this->may_force;
}
/**
@ -69,26 +313,42 @@ final class ObjectClass extends Base
*/
public function getMayAttrs(bool $parents=FALSE): Collection
{
$attrs = $this->attributes
->filter(fn($item)=>! $item->is_must)
->transform(function($item) {
$item->source = $this->name;
return $item;
});
// If we dont need our parents, then we'll just return ours.
if (! $parents)
return $this->may_attrs
->sortBy(fn($item)=>strtolower($item->name.$item->source));
if ($parents)
foreach ($this->getParents() as $object_class)
$attrs = $attrs->merge($object_class
->getMayAttrs($parents)
->transform(function($item) use ($object_class) {
$item->source = $item->source ?: $object_class->name;
return $item;
}));
$attrs = $this->may_attrs;
foreach ($this->getParents() as $object_class)
$attrs = $attrs->merge($object_class->getMayAttrs($parents));
// Remove any duplicates
$attrs = $attrs->unique(function($item) { return $item->name; });
// Return a sorted list
return $attrs
->unique(fn($item)=>$item->name)
->sortBy(fn($item)=>$item->name);
return $attrs->sortBy(function($item) { return strtolower($item->name.$item->source); });
}
/**
* Gets an array of attribute names (strings) that entries of this ObjectClass must define.
* This differs from getMayAttrs in that it returns an array of strings rather than
* array of AttributeType objects
*
* @param bool $parents An array of ObjectClass objects to use when traversing
* the inheritance tree. This presents some what of a bootstrapping problem
* as we must fetch all objectClasses to determine through inheritance which
* attributes this objectClass provides.
* @return Collection The array of allowed attribute names (strings).
*
* @throws InvalidUsage
* @see getMustAttrs
* @see getMayAttrs
* @see getMustAttrNames
*/
public function getMayAttrNames(bool $parents=FALSE): Collection
{
return $this->getMayAttrs($parents)->ppluck('name');
}
/**
@ -105,26 +365,41 @@ final class ObjectClass extends Base
*/
public function getMustAttrs(bool $parents=FALSE): Collection
{
$attrs = $this->attributes
->filter(fn($item)=>$item->is_must)
->transform(function($item) {
$item->source = $this->name;
return $item;
});
// If we dont need our parents, then we'll just return ours.
if (! $parents)
return $this->must_attrs->sortBy(function($item) { return strtolower($item->name.$item->source); });
if ($parents)
foreach ($this->getParents() as $object_class)
$attrs = $attrs->merge($object_class
->getMustAttrs($parents)
->transform(function($item) use ($object_class) {
$item->source = $item->source ?: $object_class->name;
return $item;
}));
$attrs = $this->must_attrs;
foreach ($this->getParents() as $object_class)
$attrs = $attrs->merge($object_class->getMustAttrs($parents));
// Remove any duplicates
$attrs = $attrs->unique(function($item) { return $item->name; });
// Return a sorted list
return $attrs
->unique(fn($item)=>$item->name)
->sortBy(fn($item)=>$item->name);
return $attrs->sortBy(function($item) { return strtolower($item->name.$item->source); });
}
/**
* Gets an array of attribute names (strings) that entries of this ObjectClass must define.
* This differs from getMustAttrs in that it returns an array of strings rather than
* array of AttributeType objects
*
* @param bool $parents An array of ObjectClass objects to use when traversing
* the inheritance tree. This presents some what of a bootstrapping problem
* as we must fetch all objectClasses to determine through inheritance which
* attributes this objectClass provides.
* @return Collection The array of allowed attribute names (strings).
*
* @throws InvalidUsage
* @see getMustAttrs
* @see getMayAttrs
* @see getMayAttrNames
*/
public function getMustAttrNames(bool $parents=FALSE): Collection
{
return $this->getMustAttrs($parents)->ppluck('name');
}
/**
@ -151,6 +426,27 @@ final class ObjectClass extends Base
return $result;
}
/**
* Gets the objectClass names from which this objectClass inherits.
*
* @return Collection An array of objectClass names (strings)
* @deprecated use $this->sup_classes;
*/
public function getSupClasses(): Collection
{
return $this->sup_classes;
}
/**
* Gets the type of this objectClass: STRUCTURAL, ABSTRACT, or AUXILIARY.
*
* @deprecated use $this->type_name
*/
public function getType()
{
return $this->type;
}
/**
* Return if this objectclass is auxiliary
*
@ -161,107 +457,37 @@ final class ObjectClass extends Base
return $this->type === Server::OC_AUXILIARY;
}
public function isStructural(): bool
/**
* Determine if an array is listed in the may_force attrs
*/
public function isForceMay(string $attr): bool
{
return $this->type === Server::OC_STRUCTURAL;
return $this->may_force->ppluck('name')->contains($attr);
}
/**
* Creates a new ObjectClass object given a raw LDAP objectClass string.
* Return if this objectClass is related to $oclass
*
* eg: ( 2.5.6.0 NAME 'top' DESC 'top of the superclass chain' ABSTRACT MUST objectClass )
*
* @param string $line Schema Line
* @param array $oclass ObjectClasses that this attribute may be related to
* @return bool
* @throws InvalidUsage
*/
protected function parse(string $line): void
public function isRelated(array $oclass): bool
{
Log::debug(sprintf('%s:Parsing ObjectClass [%s]',self::LOGKEY,$line));
// If I am in the array, we'll just return false
if (in_array_ignore_case($this->name,$oclass))
return FALSE;
// Init
$this->attributes = collect();
$this->sup_classes = collect();
$this->child_classes = collect();
foreach ($oclass as $object_class)
if ($object_class->isStructural() && in_array_ignore_case($this->name,$object_class->getParents()->pluck('name')))
return TRUE;
parent::parse($line);
return FALSE;
}
protected function parse_chunk(array $strings,int &$i): void
public function isStructural(): bool
{
switch ($strings[$i]) {
case 'SUP':
if ($strings[$i+1] !== '(') {
$this->sup_classes->push(preg_replace("/'/",'',$strings[++$i]));
} else {
$i++;
do {
$i++;
if ($strings[$i] !== '$')
$this->sup_classes->push(preg_replace("/'/",'',$strings[$i]));
} while (! preg_match('/\)+\)?/',$strings[$i+1]));
}
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case SUP returned (%s)',self::LOGKEY,$this->sup_classes->join(',')));
break;
case 'ABSTRACT':
$this->type = Server::OC_ABSTRACT;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case ABSTRACT returned (%s)',self::LOGKEY,$this->type));
break;
case 'STRUCTURAL':
$this->type = Server::OC_STRUCTURAL;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case STRUCTURAL returned (%s)',self::LOGKEY,$this->type));
break;
case 'AUXILIARY':
$this->type = Server::OC_AUXILIARY;
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case AUXILIARY returned (%s)',self::LOGKEY,$this->type));
break;
case 'MUST':
$attrs = collect();
$i = $this->parseList(++$i,$strings,$attrs);
foreach ($attrs as $string) {
$attr = clone config('server')->schema('attributetypes',$string);
if (! $attr->forced_as_may)
$attr->setMust();
$this->attributes->push($attr);
}
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case MUST returned (%s) (%s)',self::LOGKEY,$attrs->join(','),$this->forced_as_may ? 'FORCED MAY' : 'MUST'));
break;
case 'MAY':
$attrs = collect();
$i = $this->parseList(++$i,$strings,$attrs);
foreach ($attrs as $string)
$this->attributes->push(config('server')->schema('attributetypes',$string));
if (static::DEBUG_VERBOSE)
Log::debug(sprintf('%s:- Case MAY returned (%s)',self::LOGKEY,$attrs->join(',')));
break;
default:
parent::parse_chunk($strings,$i);
}
return $this->type === Server::OC_STRUCTURAL;
}
/**

View File

@ -0,0 +1,40 @@
<?php
namespace App\Classes\LDAP\Schema;
/**
* A simple class for representing AttributeTypes used only by the ObjectClass class.
*
* Users should never instantiate this class. It represents an attribute internal to
* an ObjectClass. If PHP supported inner-classes and variable permissions, this would
* be interior to class ObjectClass and flagged private. The reason this class is used
* and not the "real" class AttributeType is because this class supports the notion of
* a "source" objectClass, meaning that it keeps track of which objectClass originally
* specified it. This class is therefore used by the class ObjectClass to determine
* inheritance.
*/
final class ObjectClassAttribute extends Base {
// This Attribute's root.
private string $source;
public bool $required = FALSE;
/**
* Creates a new ObjectClassAttribute with specified name and source objectClass.
*
* @param string $name the name of the new attribute.
* @param string $source the name of the ObjectClass which specifies this attribute.
*/
public function __construct($name,$source)
{
$this->name = $name;
$this->source = $source;
}
public function __get(string $key): mixed
{
return match ($key) {
'source' => $this->source,
default => parent::__get($key),
};
}
}

View File

@ -8,51 +8,51 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Cookie;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use LdapRecord\LdapRecordException;
use LdapRecord\Models\Model;
use LdapRecord\Query\Builder;
use LdapRecord\Query\Collection as LDAPCollection;
use LdapRecord\Query\ObjectNotFoundException;
use App\Classes\LDAP\Schema\{AttributeType,Base,LDAPSyntax,MatchingRule,ObjectClass};
use App\Classes\LDAP\Schema\{AttributeType,Base,LDAPSyntax,MatchingRule,MatchingRuleUse,ObjectClass};
use App\Exceptions\InvalidUsage;
use App\Ldap\Entry;
final class Server
{
private const LOGKEY = 'SVR';
// Connection information used for these object and children
private ?string $connection;
// This servers schema objectclasses
private Collection $attributetypes;
private Collection $ldapsyntaxes;
private Collection $matchingrules;
private Collection $matchingruleuse;
private Collection $objectclasses;
private Model $rootDSE;
/* ObjectClass Types */
public const OC_STRUCTURAL = 0x01;
public const OC_ABSTRACT = 0x02;
public const OC_AUXILIARY = 0x03;
public function __construct()
public function __construct(?string $connection=NULL)
{
$this->rootDSE = self::rootDSE();
$this->attributetypes = collect();
$this->ldapsyntaxes = collect();
$this->matchingrules = collect();
$this->objectclasses = collect();
$this->connection = $connection;
}
public function __get(string $key): mixed
{
return match($key) {
'config' => config(sprintf('ldap.connections.%s',config('ldap.default'))),
'attributetypes' => $this->attributetypes,
'connection' => $this->connection,
'ldapsyntaxes' => $this->ldapsyntaxes,
'matchingrules' => $this->matchingrules,
'objectclasses' => $this->objectclasses,
'config' => config('ldap.connections.'.config('ldap.default')),
'name' => Arr::get($this->config,'name',__('No Server Name Yet')),
default => throw new Exception('Unknown key:'.$key),
default => throw new Exception('Unknown key:' . $key),
};
}
@ -62,14 +62,20 @@ final class Server
* Gets the root DN of the specified LDAPServer, or throws an exception if it
* can't find it.
*
* @param string|null $connection Return a collection of baseDNs
* @param bool $objects Return a collection of Entry Models
* @return Collection
* @throws ObjectNotFoundException
* @testedin GetBaseDNTest::testBaseDNExists();
* @todo Need to allow for the scenario if the baseDN is not readable by ACLs
*/
public static function baseDNs(bool $objects=TRUE): Collection
public static function baseDNs(?string $connection=NULL,bool $objects=TRUE): Collection
{
$cachetime = Carbon::now()
->addSeconds(Config::get('ldap.cache.time'));
try {
$namingcontexts = collect(config('pla.base_dns') ?: self::rootDSE()?->namingcontexts);
$base = self::rootDSE($connection,$cachetime);
/**
* LDAP Error Codes:
@ -167,6 +173,16 @@ final class Server
} catch (LdapRecordException $e) {
switch ($e->getDetailedError()?->getErrorCode()) {
case 49:
// Since we failed authentication, we should delete our auth cookie
if (Cookie::has('password_encrypt')) {
Log::alert('Clearing user credentials and logging out');
Cookie::queue(Cookie::forget('password_encrypt'));
Cookie::queue(Cookie::forget('username_encrypt'));
Session::invalidate();
}
abort(401,$e->getDetailedError()->getErrorMessage());
default:
@ -175,107 +191,71 @@ final class Server
}
if (! $objects)
return $namingcontexts;
return collect($base->namingcontexts);
return Cache::remember('basedns'.Session::id(),config('ldap.cache.time'),function() use ($namingcontexts) {
$result = collect();
/**
* @note While we are caching our baseDNs, it seems if we have more than 1,
* our caching doesnt generate a hit on a subsequent call to this function (before the cache expires).
* IE: If we have 5 baseDNs, it takes 5 calls to this function to case them all.
* @todo Possibly a bug wtih ldaprecord, so need to investigate
*/
$result = collect();
foreach ($base->namingcontexts as $dn)
$result->push((new Entry)->cache($cachetime)->findOrFail($dn));
// @note: Incase our rootDSE didnt return a namingcontext, we'll have no base DNs
foreach ($namingcontexts as $dn)
$result->push(self::get($dn)->read()->find($dn));
return $result->filter()->sort(fn($item)=>$item->sort_key);
});
}
/**
* Work out if we should flush the cache when retrieving an entry
*
* @param string $dn
* @return bool
* @note: We dont need to flush the cache for internal LDAP attributes, as we dont change them
*/
private static function cacheflush(string $dn): bool
{
$cache = (! config('ldap.cache.enabled'))
|| match (strtolower($dn)) {
'','cn=schema','cn=subschema' => FALSE,
default => TRUE,
};
Log::debug(sprintf('%s:%s - %s',self::LOGKEY,$cache ? 'DN CACHEABLE' : 'DN NOT cacheable',$dn));
return $cache;
}
/**
* Return our cache time as per the configuration
*
* @return Carbon
*/
private static function cachetime(): Carbon
{
return Carbon::now()
->addSeconds(Config::get('ldap.cache.time') ?: 0);
}
/**
* Generic Builder method to setup our queries consistently - mainly to ensure we cache results
*
* @param string $dn
* @param array $attrs
* @return Builder
*/
private static function get(string $dn,array $attrs=['*','+']): Builder
{
return Entry::query()
->setDN($dn)
->cache(
until: self::cachetime(),
flush: self::cacheflush($dn)
)
->select($attrs);
return $result;
}
/**
* Obtain the rootDSE for the server, that gives us server information
*
* @return Model
* @param null $connection
* @return Entry|null
* @throws ObjectNotFoundException
* @testedin TranslateOidTest::testRootDSE();
* @note While we are using a static variable for in session performance, we'll also cache the result normally
*/
public static function rootDSE(): Model
public static function rootDSE(?string $connection=NULL,?Carbon $cachetime=NULL): ?Model
{
static $rootdse = NULL;
$e = new Entry;
if (is_null($rootdse))
$rootdse = self::get('',['+','*'])
->read()
->firstOrFail();
return $rootdse;
return Entry::on($connection ?? $e->getConnectionName())
->cache($cachetime)
->in(NULL)
->read()
->select(['+'])
->whereHas('objectclass')
->firstOrFail();
}
/* METHODS */
/**
* Get the Schema DN
*
* @param $connection
* @return string
* @throws ObjectNotFoundException
*/
public static function schemaDN(?string $connection=NULL): string
{
$cachetime = Carbon::now()->addSeconds(Config::get('ldap.cache.time'));
return collect(self::rootDSE($connection,$cachetime)->subschemasubentry)->first();
}
/**
* Query the server for a DN and return its children and if those children have children.
*
* @param string $dn
* @param array $attrs
* @return LDAPCollection|NULL
*/
public function children(string $dn,array $attrs=['dn']): ?LDAPCollection
public function children(string $dn): ?LDAPCollection
{
return $this
->get(
dn: $dn,
attrs: array_merge($attrs,[
'hassubordinates', // Needed for the tree to know if an entry has children
'c' // Needed for the tree to show icons for countries
]))
return ($x=(new Entry)
->on($this->connection)
->cache(Carbon::now()->addSeconds(Config::get('ldap.cache.time')))
->select(['*','hassubordinates'])
->setDn($dn)
->list()
->get() ?: NULL;
->get()) ? $x : NULL;
}
/**
@ -283,47 +263,26 @@ final class Server
*
* @param string $dn
* @param array $attrs
* @return Model|null
* @return Entry|null
*/
public function fetch(string $dn,array $attrs=['*','+']): ?Model
public function fetch(string $dn,array $attrs=['*','+']): ?Entry
{
return $this->get($dn,$attrs)
->read()
->first() ?: NULL;
return ($x=(new Entry)
->on($this->connection)
->cache(Carbon::now()->addSeconds(Config::get('ldap.cache.time')))
->select($attrs)
->find($dn)) ? $x : NULL;
}
/**
* Get an attribute key for an attributetype name
* This function determines if the specified attribute is contained in the force_may list
* as configured in config.php.
*
* @param string $key
* @return int|bool
* @throws InvalidUsage
* @return boolean True if the specified attribute is configured to be force as a may attribute
*/
public function get_attr_id(string $key): int|bool
public function isForceMay($attr_name): bool
{
static $attributes = $this->schema('attributetypes');
$attrid = $attributes->search(fn($item)=>$item->names->contains($key));
// Second chance search using lowercase items (our Entry attribute keys are lowercase)
if ($attrid === FALSE)
$attrid = $attributes->search(fn($item)=>$item->names_lc->contains(strtolower($key)));
return $attrid;
}
/**
* Given an OID, return the ldapsyntax for the OID
*
* @param string $oid
* @return LDAPSyntax|null
* @throws InvalidUsage
*/
public function get_syntax(string $oid): ?LDAPSyntax
{
return (($id=$this->schema('ldapsyntaxes')->search(fn($item)=>$item->oid === $oid)) !== FALSE)
? $this->ldapsyntaxes[$id]
: NULL;
return in_array($attr_name,config('pla.force_may',[]));
}
/**
@ -344,167 +303,235 @@ final class Server
* @param string $item Schema Item to Fetch
* @param string|null $key
* @return Collection|LDAPSyntax|Base|NULL
* @throws InvalidUsage
*/
public function schema(string $item,?string $key=NULL): Collection|LDAPSyntax|Base|NULL
{
// Ensure our item to fetch is lower case
$item = strtolower($item);
if ($key)
$key = strtolower($key);
if (! $this->{$item}->count()) {
$this->{$item} = Cache::remember('schema.'.$item,config('ldap.cache.time'),function() use ($item) {
// Try to get the schema DN from the specified entry.
$schema_dn = $this->schemaDN();
// @note: 389DS does not return subschemaSubentry unless it is requested
$schema = $this->fetch($schema_dn,['*','+','subschemaSubentry']);
// If our schema's null, we didnt find it.
if (! $schema)
throw new Exception('Couldnt find schema at:'.$schema_dn);
switch ($item) {
case 'attributetypes':
Log::debug(sprintf('%s:Attribute Types',self::LOGKEY));
// build the array of attribueTypes
//$syntaxes = $this->SchemaSyntaxes($dn);
foreach ($schema->{$item} as $line) {
if (is_null($line) || ! strlen($line))
continue;
$o = new AttributeType($line);
$this->attributetypes->push($o);
}
foreach ($this->attributetypes as $o) {
// Now go through and reference the parent/child relationships
if ($o->sup_attribute) {
$attrid = $this->get_attr_id($o->sup_attribute);
if (! $this->attributetypes[$attrid]->children->contains($o->oid))
$this->attributetypes[$attrid]->addChild($o->oid);
}
// go through any children and add details if the child doesnt have them (ie, cn inherits name)
foreach ($o->children as $child) {
$attrid = $this->attributetypes->search(fn($o)=>$o->oid === $child);
/* only overwrite the child's SINGLE-VALUE property if the parent has it set, and the child doesnt
* (note: All LDAP attributes default to multi-value if not explicitly set SINGLE-VALUE) */
if (! is_null($o->is_single_value) && is_null($this->attributetypes[$attrid]->is_single_value))
$this->attributetypes[$attrid]->setIsSingleValue($o->is_single_value);
}
}
$result = Cache::remember('schema'.$item,config('ldap.cache.time'),function() use ($item) {
// First pass if we have already retrieved the schema item
switch ($item) {
case 'attributetypes':
if (isset($this->attributetypes))
return $this->attributetypes;
else
$this->attributetypes = collect();
case 'ldapsyntaxes':
Log::debug(sprintf('%s:LDAP Syntaxes',self::LOGKEY));
foreach ($schema->{$item} as $line) {
if (is_null($line) || ! strlen($line))
continue;
$o = new LDAPSyntax($line);
$this->ldapsyntaxes->push($o);
}
break;
case 'ldapsyntaxes':
if (isset($this->ldapsyntaxes))
return $this->ldapsyntaxes;
else
$this->ldapsyntaxes = collect();
case 'matchingrules':
Log::debug(sprintf('%s:Matching Rules',self::LOGKEY));
foreach ($schema->{$item} as $line) {
if (is_null($line) || ! strlen($line))
continue;
$o = new MatchingRule($line);
$this->matchingrules->push($o);
}
foreach ($this->schema('attributetypes') as $attr) {
$rule_id = $this->matchingrules->search(fn($item)=>$item->oid === $attr->equality);
if ($rule_id !== FALSE)
$this->matchingrules[$rule_id]->addUsedByAttr($attr->name);
}
break;
case 'matchingrules':
if (isset($this->matchingrules))
return $this->matchingrules;
else
$this->matchingrules = collect();
case 'objectclasses':
Log::debug(sprintf('%s:Object Classes',self::LOGKEY));
break;
foreach ($schema->{$item} as $line) {
case 'objectclasses':
if (isset($this->objectclasses))
return $this->objectclasses;
else
$this->objectclasses = collect();
break;
// This error message is not localized as only developers should ever see it
default:
throw new InvalidUsage('Invalid request to fetch schema: '.$item);
}
// Try to get the schema DN from the specified entry.
$schema_dn = $this->schemaDN($this->connection);
$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) {
case 'attributetypes':
Log::debug('Attribute Types');
// build the array of attribueTypes
//$syntaxes = $this->SchemaSyntaxes($dn);
foreach ($schema->{$item} as $line) {
if (is_null($line) || ! strlen($line))
continue;
$o = new AttributeType($line);
$this->attributetypes->put($o->name_lc,$o);
}
// go back and add data from aliased attributeTypes
foreach ($this->attributetypes as $o) {
/* foreach of the attribute's aliases, create a new entry in the attrs array
* with its name set to the alias name, and all other data copied.*/
if ($o->aliases->count()) {
Log::debug(sprintf('\ Attribute [%s] has the following aliases [%s]',$o->name,$o->aliases->join(',')));
foreach ($o->aliases as $alias) {
$new_attr = clone $o;
$new_attr->setName($alias);
$new_attr->addAlias($o->name);
$new_attr->removeAlias($alias);
$this->attributetypes->put(strtolower($alias),$new_attr);
}
}
}
// Now go through and reference the parent/child relationships
foreach ($this->attributetypes as $o)
if ($o->sup_attribute) {
$parent = strtolower($o->sup_attribute);
if ($this->attributetypes->has($parent) !== FALSE)
$this->attributetypes[$parent]->addChild($o->name);
}
// go through any children and add details if the child doesnt have them (ie, cn inherits name)
// @todo This doesnt traverse children properly, so children of children may not get the settings they should
foreach ($this->attributetypes as $parent) {
foreach ($parent->children as $child) {
$child = strtolower($child);
/* only overwrite the child's SINGLE-VALUE property if the parent has it set, and the child doesnt
* (note: All LDAP attributes default to multi-value if not explicitly set SINGLE-VALUE) */
if (! is_null($parent->is_single_value) && is_null($this->attributetypes[$child]->is_single_value))
$this->attributetypes[$child]->setIsSingleValue($parent->is_single_value);
}
}
// Add the used in and required_by values.
foreach ($this->schema('objectclasses') as $object_class) {
$must_attrs = $object_class->getMustAttrNames();
$may_attrs = $object_class->getMayAttrNames();
$oclass_attrs = $must_attrs->merge($may_attrs)->unique();
// Add Used In.
foreach ($oclass_attrs as $attr_name)
if ($this->attributetypes->has(strtolower($attr_name)))
$this->attributetypes[strtolower($attr_name)]->addUsedInObjectClass($object_class->name,$object_class->isStructural());
// Add Required By.
foreach ($must_attrs as $attr_name)
if ($this->attributetypes->has(strtolower($attr_name)))
$this->attributetypes[strtolower($attr_name)]->addRequiredByObjectClass($object_class->name);
// Force May
foreach ($object_class->getForceMayAttrs() as $attr_name)
if ($this->attributetypes->has(strtolower($attr_name->name)))
$this->attributetypes[strtolower($attr_name->name)]->setForceMay();
}
return $this->attributetypes;
case 'ldapsyntaxes':
Log::debug('LDAP Syntaxes');
foreach ($schema->{$item} as $line) {
if (is_null($line) || ! strlen($line))
continue;
$o = new LDAPSyntax($line);
$this->ldapsyntaxes->put(strtolower($o->oid),$o);
}
return $this->ldapsyntaxes;
case 'matchingrules':
Log::debug('Matching Rules');
$this->matchingruleuse = collect();
foreach ($schema->{$item} as $line) {
if (is_null($line) || ! strlen($line))
continue;
$o = new MatchingRule($line);
$this->matchingrules->put($o->name_lc,$o);
}
/*
* For each MatchingRuleUse entry, add the attributes who use it to the
* MatchingRule in the $rules array.
*/
if ($schema->matchingruleuse) {
foreach ($schema->matchingruleuse as $line) {
if (is_null($line) || ! strlen($line))
continue;
$o = new ObjectClass($line);
$this->objectclasses->push($o);
$o = new MatchingRuleUse($line);
$this->matchingruleuse->put($o->name_lc,$o);
if ($this->matchingrules->has($o->name_lc) !== FALSE)
$this->matchingrules[$o->name_lc]->setUsedByAttrs($o->getUsedByAttrs());
}
foreach ($this->objectclasses as $o) {
// Now go through and reference the parent/child relationships
foreach ($o->sup_classes as $sup) {
$oc_id = $this->objectclasses->search(fn($item)=>$item->name === $sup);
} else {
/* No MatchingRuleUse entry in the subschema, so brute-forcing
* the reverse-map for the "$rule->getUsedByAttrs()" data.*/
foreach ($this->schema('attributetypes') as $attr) {
$rule_key = strtolower($attr->getEquality());
if (($oc_id !== FALSE) && (! $this->objectclasses[$oc_id]->child_classes->contains($o->name)))
$this->objectclasses[$oc_id]->addChildObjectClass($o->name);
}
if ($this->matchingrules->has($rule_key) !== FALSE)
$this->matchingrules[$rule_key]->addUsedByAttr($attr->name);
}
}
// Add the used in and required_by values for attributes.
foreach ($o->attributes as $attribute) {
if (($attrid = $this->schema('attributetypes')->search(fn($item)=>$item->oid === $attribute->oid)) !== FALSE) {
// Add Used In.
$this->attributetypes[$attrid]->addUsedInObjectClass($o->oid,$o->isStructural());
return $this->matchingrules;
// Add Required By.
if ($attribute->is_must)
$this->attributetypes[$attrid]->addRequiredByObjectClass($o->oid,$o->isStructural());
}
}
case 'objectclasses':
Log::debug('Object Classes');
foreach ($schema->{$item} as $line) {
if (is_null($line) || ! strlen($line))
continue;
$o = new ObjectClass($line,$this);
$this->objectclasses->put($o->name_lc,$o);
}
// Now go through and reference the parent/child relationships
foreach ($this->objectclasses as $o)
foreach ($o->getSupClasses() as $parent) {
$parent = strtolower($parent);
if (! $this->objectclasses->contains($parent))
$this->objectclasses[$parent]->addChildObjectClass($o->name);
}
// Put the updated attributetypes back in the cache
Cache::put('schema.attributetypes',$this->attributetypes,config('ldap.cache.time'));
return $this->objectclasses;
return $this->objectclasses;
// Shouldnt get here
default:
throw new InvalidUsage('Invalid request to fetch schema: '.$item);
}
});
// Shouldnt get here
default:
throw new InvalidUsage('Invalid request to fetch schema: '.$item);
}
});
}
if (is_null($key))
return $this->{$item};
switch ($item) {
case 'attributetypes':
$attrid = $this->get_attr_id($key);
$attr = ($attrid === FALSE)
? new AttributeType($key)
: clone $this->{$item}->get($attrid);
$attr->setName($attr->names->get($attr->names_lc->search(strtolower($key))) ?: $key);
return $attr;
default:
return $this->{$item}->get($key)
?: $this->{$item}->first(fn($item)=>$item->name_lc === strtolower($key));
}
return is_null($key) ? $result : $result->get($key);
}
/**
* Get the Schema DN
* Given an OID, return the ldapsyntax for the OID
*
* @return string
* @throws ObjectNotFoundException
* @param string $oid
* @return LDAPSyntax|null
* @throws InvalidUsage
*/
public function schemaDN(): string
public function schemaSyntaxName(string $oid): ?LDAPSyntax
{
return Arr::get($this->rootDSE->subschemasubentry,0);
return $this->schema('ldapsyntaxes',$oid);
}
}

View File

@ -1,450 +0,0 @@
<?php
namespace App\Classes;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use App\Ldap\Entry;
class Template
{
private const LOGKEY = 'T--';
private const LOCK_TIME = 600;
private(set) string $file;
private Collection $template;
private(set) bool $invalid = FALSE;
private(set) string $reason = '';
private Collection $on_change_target;
private Collection $on_change_attribute;
private bool $on_change_processed = FALSE;
public function __construct(string $file)
{
$td = Storage::disk(config('pla.template.dir'));
$this->on_change_attribute = collect();
$this->on_change_target = collect();
$this->file = $file;
try {
// @todo Load in the proper attribute objects and objectclass objects
// @todo Make sure we have a structural objectclass, or make the template invalid
$this->template = collect(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','objectclasses' => collect($this->template->get($key)),
'enabled' => $this->template->get($key,FALSE) && (! $this->invalid),
'icon','regexp','title' => $this->template->get($key),
'name' => Str::replaceEnd('.json','',$this->file),
'order' => $this->attributes->map(fn($item)=>Arr::get($item,'order')),
default => throw new \Exception('Unknown key: '.$key),
};
}
public function __isset(string $key): bool
{
return $this->template->has($key);
}
/**
* Return the configuration for an attribute
*
* @param string $attribute
* @return array|NULL
*/
public function attribute(string $attribute): Collection|NULL
{
$key = $this->attributes->search(fn($item,$key)=>! strcasecmp($key,$attribute));
return collect($this->attributes->get($key));
}
/**
* Return an template attributes select options
*
* @param string $attribute
* @return Collection|NULL
*/
public function attributeOptions(string $attribute): Collection|NULL
{
return ($x=$this->attribute($attribute)?->get('options'))
? collect($x)->map(fn($item,$key)=>['id'=>$key,'value'=>$item])
: NULL;
}
/**
* If the attribute has been marked as read-only
*
* @param string $attribute
* @return bool
*/
public function attributeReadOnly(string $attribute): bool
{
return ($x=$this->attribute($attribute)?->get('readonly')) && $x;
}
/**
* Return the title we should use for an attribute
*
* @param string $attribute
* @return string|NULL
*/
public function attributeTitle(string $attribute): string|NULL
{
return $this->attribute($attribute)?->get('display');
}
/**
* Return the title we should use for an attribute
*
* @param string $attribute
* @return string|NULL
*/
public function attributeType(string $attribute): string|NULL
{
return $this->attribute($attribute)?->get('type');
}
public function attributeValue(string $attribute): string|NULL
{
if ($x=$this->attribute($attribute)->get('value')) {
list($command,$args) = preg_split('/^=([a-zA-Z]+)\((.+)\)$/',$x,-1,PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
return match ($command) {
'getNextNumber' => $this->getNextNumber($args),
default => NULL,
};
}
return NULL;
}
/**
* Get next number for an attribute
*
* As part of getting the next number, we'll use a lock to avoid any potential clashes. The lock is obtained by
* two lock files:
* a: Read a session lock (our session id), use that number if it exists, otherwise,
* b: Query the ldap server for the attribute, sort by number
* c: Read a system lock, if it exists, and use that as our start base (otherwise use a config() base)
* d: Starting at base, find the next free number
* e: When number identified, put it in the system lock with our session id
* f: Put the number in our session lock, with a timeout
* g: Read the system lock, make sure our session id is still in it, if not, go to (d) with our number as the base
* h: Remove our session id from the system lock (our number is unique)
*
* When using the number to create an entry:
* + Read our session lock, confirm the number is still in it, if not fail validation and bounce back
* + Create the entry
* + Delete our session lock
*
* @param string $arg
* @return int|NULL
*/
private function getNextNumber(string $arg): int|NULL
{
if (! preg_match('/;/',$arg)) {
Log::alert(sprintf('%s:Invalid argument given to getNextNumber [%s]',self::LOGKEY,$arg));
return NULL;
}
list($start,$attr) = preg_split('(([^,]+);(\w+))',$arg,-1,PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
$attr = strtolower($attr);
// If we recently got a number, return it
if ($number=Cache::get($attr.':'.Session::id()))
return $number;
$cache = Cache::get($attr.':system');
Log::debug(sprintf('%s:System Cache has',self::LOGKEY),['cache'=>$cache]);
if (! Arr::get($cache,'number'))
$number = config('pla.template.getnextnumber.'.$attr,0);
else
$number = Arr::get($cache,'number')+1;
Log::debug(sprintf('%s:Starting with [%d] for [%s]',self::LOGKEY,$number,$attr));
$o = config('server');
$bases = ($start === '/') ? $o->baseDNs() : collect($start);
$result = collect();
$complete = [];
do {
$sizelimit = FALSE;
// Get the current numbers
foreach ($bases as $base) {
if (Arr::get($complete,$dn=$base->getDN()))
continue;
$query = Entry::query()
->setDN($base)
->select([$attr])
->where($attr,'*')
->notFilter(fn($q)=>$q->where($attr,'<=',$number-1));
if ($result->count())
$query->notFilter(fn($q)=>$q->where($attr,'>=',$result->min()));
$result = $result->merge(($x=$query
->search()
->orderBy($attr)
->get())
->pluck($attr)
->flatten());
// If we hit a sizelimit on this run
$base_sizelimit = $query->getModel()->hasMore();
Log::debug(sprintf('%s:Query in [%s] returned [%d] entries and has more [%s]',self::LOGKEY,$base,$x->count(),$base_sizelimit ? 'TRUE' : 'FALSE'));
if (! $base_sizelimit)
$complete[$dn] = TRUE;
else
Log::info(sprintf('%s:Size Limit alert for [%s]',self::LOGKEY,$dn));
$sizelimit = $sizelimit || $base_sizelimit;
}
$result = $result
->sort()
->unique();
Log::debug(sprintf('%s:Result has [%s]',self::LOGKEY,$result->join('|')));
if ($result->count())
foreach ($result as $item) {
Log::debug(sprintf('%s:Checking [%d] against [%s]',self::LOGKEY,$number,$item));
if ($number < $item)
break;
$number += 1;
}
else
$number += 1;
// Remove redundant entries
$result = $result->filter(fn($item)=>$item>$number);
if ($sizelimit)
Log::debug(sprintf('%s:We got a sizelimit.',self::LOGKEY),['number'=>$number,'result_min'=>$result->min(),'result_count'=>$result->count()]);
/*
* @todo This might need some additional work:
* EG: if sizelimit is 5
* if result has 1,2,3,4,20 [size limit]
* we re-enquire (4=>20) and get 7,8,9,10,11 [size limit]
* we re-enquire (4=>7) and get 5,6 [no size limit]
* we calculate 12, and accept it because no size limit, but we didnt test for 12
*/
} while ($sizelimit);
// We found our number
Log::debug(sprintf('%s:Autovalue for Attribute [%s] in Session [%s] Storing [%d]',self::LOGKEY,$attr,Session::id(),$number));
Cache::put($attr.':system',['number'=>$number,'session'=>Session::id(),self::LOCK_TIME*2]);
Cache::put($attr.':'.Session::id(),$number,self::LOCK_TIME);
sleep(1);
// If the session still has our session ID, then our number is ours
return (Arr::get(Cache::get($attr.':system'),'session') === Session::id())
? $number
: NULL;
}
/**
* Return the onChange JavaScript for an attribute
*
* @param string $attribute
* @return Collection|NULL
*/
public function onChange(string $attribute): Collection|NULL
{
if (! $this->on_change_processed)
$this->onChangeProcessing();
return $this->on_change_attribute
->get(strtolower($attribute));
}
/**
* Is this attribute's value populated by any onChange processing rules
*
* @param string $attribute
* @return bool
*/
public function onChangeAttribute(string $attribute): bool
{
if (! $this->on_change_processed)
$this->onChangeProcessing();
return $this->on_change_attribute
->has(strtolower($attribute));
}
/**
* Process the attributes for onChange JavaScript
*/
/**
* Return the onchange JavaScript for attribute
*
* @return Collection
*/
private function onChangeProcessing(): void
{
foreach (Arr::get($this->template,'attributes',[]) as $attribute => $detail) {
$result = collect();
foreach (Arr::get($detail,'onchange',[]) as $item) {
list($command,$args) = preg_split('/^=([a-zA-Z]+)\((.+)\)$/',$item,-1,PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
switch ($command) {
case 'autoFill':
$result->push($this->autofill($args));
break;
}
}
if ($result->count())
$this->on_change_attribute->put(strtolower($attribute),$result);
}
$this->on_change_processed = TRUE;
}
/**
* Is this attribute's value populated by any onChange processing rules
*
* @param string $attribute
* @return bool
*/
public function onChangeTarget(string $attribute): bool
{
if (! $this->on_change_processed)
$this->onChangeProcessing();
return $this->on_change_target
->has(strtolower($attribute));
}
/**
* autoFill - javascript to have one attribute fill the value of another
*
* args: is a literal string, with two parts, delimited by a semi-colon ;
* + The first part is the attribute that will be populated
* + The second part may contain many fields like %attr|start-end/flags|additionalcontrolchar%
* to substitute values read from other fields.
* + |start-end is optional, but must be present if the k flag is used
* + /flags is optional
* + |additionalcontrolchar is optional, and specific to a flag being used
*
* + flags may be:
* T:(?) Read display text from selection item (drop-down list), otherwise, read the value of the field
* For fields that aren't selection items, /T shouldn't be used, and the field value will always be read
* k:(?) Tokenize:
* If the "k" flag is not given:
* + A |start-end instruction will perform a sub-string operation upon the value of the attr, passing
* character positions start-end through
* + start can be 0 for first character, or any other integer
* + end can be 0 for last character, or any other integer for a specific position
* If the "k" flag is given:
* + The string read will be split into fields, using : as a delimiter
* + start indicates which field number to pass through
*
* If additionalcontrolchar is given, it will be used as delimiter (e.g. this allows for splitting
* e-mail addresses into domain and domain-local part)
* l: Make the result lower case
* U: Make the result upper case
* A:(?) Remap special characters to their corresponding ASCII value
*
* @note Attributes rendered on the page are lowercase, eg: <attribute id="gidnumber"> for gidNumber
* @note JavaScript generated here depends on js/template.js
* (?) = to test
*/
private function autofill(string $arg): string
{
if (! preg_match('/;/',$arg)) {
Log::alert(sprintf('%s:Invalid argument given to autofill [%s]',self::LOGKEY,$arg));
return '';
}
$result = '';
// $attr has our attribute to update, $string is the format to use when updating it
list($attr,$string) = preg_split('(([^,]+);(.*))',$arg,-1,PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
$this->on_change_target->put(strtolower($attr),$string);
$output = $string;
//$result .= sprintf("\n// %s\n",$arg);
$m = [];
// MATCH : 0 = highlevel match, 1 = attr, 2 = subst, 3 = mod, 4 = delimiter
preg_match_all('/%(\w+)(?:\|(\d*-\d)+)?(?:\/([klTUA]+))?(?:\|(.)?)?%/U',$string,$m);
foreach ($m[0] as $index => $null) {
$match_attr = strtolower($m[1][$index]);
$match_subst = $m[2][$index];
$match_mod = $m[3][$index];
$match_delim = $m[4][$index];
$substrarray = [];
$result .= sprintf("var %s;\n",$match_attr);
if (str_contains($match_mod,'k')) {
preg_match_all('/(\d+)/',trim($match_subst),$substrarray);
$delimiter = ($match_delim === '') ? ' ' : preg_quote($match_delim);
$result .= sprintf(" %s = %s.split('%s')[%s];\n",$match_attr,$match_attr,$delimiter,$substrarray[1][0] ?? '0');
} else {
// Work out the start and end chars needed from this value if we have a range specifier
preg_match_all('/(\d*)-(\d+)/',$match_subst,$substrarray);
if ((isset($substrarray[1][0]) && $substrarray[1][0]) || (isset($substrarray[2][0]) && $substrarray[2][0])) {
$result .= sprintf("%s = get_attribute('%s',%d,%s);\n",
$match_attr,$match_attr,
$substrarray[1][0] ?? '0',
$substrarray[2][0] ?: sprintf('%s.length',$match_attr));
} else {
$result .= sprintf("%s = get_attribute('%s');\n",$match_attr,$match_attr);
}
}
if (str_contains($match_mod,'l'))
$result .= sprintf("%s = %s.toLowerCase();\n",$match_attr,$match_attr);
if (str_contains($match_mod,'U'))
$result .= sprintf("%s = %s.toUpperCase();\n",$match_attr,$match_attr);
if (str_contains($match_mod,'A'))
$result .= sprintf("%s = toAscii(%s);\n",$match_attr,$match_attr);
// For debugging
//$result .= sprintf("console.log('%s will return:'+%s);\n",$match_attr,$match_attr);
// Reformat out output into JS variables
$output = preg_replace('/'.preg_quote($m[0][$index],'/').'/','\'+'.$match_attr.'+\'',$output);
}
$result .= sprintf("put_attribute('%s','%s');\n",strtolower($attr),$output);
$result .= "\n";
return $result;
}
}

View File

@ -10,10 +10,8 @@ use Illuminate\Support\Collection;
use App\Classes\LDAP\Server;
class AjaxController extends Controller
class APIController extends Controller
{
private const LOGKEY = 'CAc';
/**
* Get the LDAP server BASE DNs
*
@ -22,14 +20,17 @@ class AjaxController extends Controller
*/
public function bases(): Collection
{
return Server::baseDNs()
->map(fn($item)=> [
'title'=>$item->getRdn(),
'item'=>$item->getDNSecure(),
'lazy'=>TRUE,
'icon'=>'fa-fw fas fa-sitemap',
'tooltip'=>$item->getDn(),
])->values();
$base = Server::baseDNs() ?: collect();
return $base
->transform(fn($item)=>
[
'title'=>$item->getRdn(),
'item'=>$item->getDNSecure(),
'lazy'=>TRUE,
'icon'=>'fa-fw fas fa-sitemap',
'tooltip'=>$item->getDn(),
]);
}
/**
@ -38,13 +39,14 @@ class AjaxController extends Controller
*/
public function children(Request $request): Collection
{
$dn = Crypt::decryptString($request->post('_key'));
$levels = $request->query('depth',1);
$dn = Crypt::decryptString($request->query('key'));
// Sometimes our key has a command, so we'll ignore it
if (str_starts_with($dn,'*') && ($x=strpos($dn,'|')))
$dn = substr($dn,$x+1);
Log::debug(sprintf('%s:Query [%s]',self::LOGKEY,$dn));
Log::debug(sprintf('%s: Query [%s] - Levels [%d]',__METHOD__,$dn,$levels));
return (config('server'))
->children($dn)
@ -57,18 +59,13 @@ class AjaxController extends Controller
'tooltip'=>$item->getDn(),
])
->prepend(
$request->create
? [
'title'=>sprintf('[%s]',__('Create Entry')),
'item'=>Crypt::encryptString(sprintf('*%s|%s','create',$dn)),
'lazy'=>FALSE,
'icon'=>'fas fa-fw fa-square-plus text-warning',
'tooltip'=>__('Create new LDAP item here'),
]
: []
)
->filter()
->values();
[
'title'=>sprintf('[%s]',__('Create Entry')),
'item'=>Crypt::encryptString(sprintf('*%s|%s','create',$dn)),
'lazy'=>FALSE,
'icon'=>'fas fa-fw fa-square-plus text-warning',
'tooltip'=>__('Create new LDAP item here'),
]);
}
public function schema_view(Request $request)
@ -101,10 +98,11 @@ class AjaxController extends Controller
/**
* Return the required and additional attributes for an object class
*
* @param Request $request
* @param string $objectclass
* @return array
*/
public function schema_objectclass_attrs(string $objectclass): array
public function schema_objectclass_attrs(Request $request,string $objectclass): array
{
$oc = config('server')->schema('objectclasses',$objectclass);

View File

@ -5,46 +5,41 @@ namespace App\Http\Controllers\Auth;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use LdapRecord\Auth\BindException;
use LdapRecord\Container;
use Illuminate\Support\Facades\Cookie;
use App\Http\Controllers\Controller;
use App\Ldap\Entry;
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = '/';
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = '/';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest')
->except('logout');
}
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest')->except('logout');
}
protected function credentials(Request $request): array
{
@ -54,45 +49,6 @@ class LoginController extends Controller
];
}
/**
* When attempt to login
*
* @param Request $request
* @return bool
* @throws \LdapRecord\ConnectionException
* @throws \LdapRecord\ContainerException
*/
public function attemptLogin(Request $request)
{
$attempt = $this->guard()->attempt(
$this->credentials($request), $request->boolean('remember')
);
// If the login failed, and PLA is set to use DN login, check if the entry exists.
// If the entry doesnt exist, it might be the root DN, which cannot be used to login
if ((! $attempt) && $request->dn && config('pla.login.alert_rootdn',TRUE)) {
// Double check our credentials, and see if they authenticate
try {
Container::getInstance()
->getConnection()
->auth()
->bind($request->get(login_attr_name()),$request->get('password'));
} catch (BindException $e) {
// Password incorrect, fail anyway
return FALSE;
}
$dn = config('server')->fetch($request->dn);
$o = new Entry;
if (! $dn && $o->getConnection()->getLdapConnection()->errNo() === 32)
abort(501,'Authentication succeeded, but the DN doesnt exist');
}
return $attempt;
}
/**
* We need to delete our encrypted username/password cookies
*
@ -102,14 +58,17 @@ class LoginController extends Controller
*/
public function logout(Request $request)
{
$user = Auth::user();
// Delete our LDAP authentication cookies
Cookie::queue(Cookie::forget('username_encrypt'));
Cookie::queue(Cookie::forget('password_encrypt'));
$this->guard()->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
if ($response = $this->loggedOut($request)) {
Log::info(sprintf('Logged out [%s]',$user->dn));
return $response;
}
@ -141,4 +100,4 @@ class LoginController extends Controller
{
return login_attr_name();
}
}
}

View File

@ -2,83 +2,88 @@
namespace App\Http\Controllers;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Session;
use LdapRecord\Exceptions\InsufficientAccessException;
use LdapRecord\LdapRecordException;
use LdapRecord\Query\ObjectNotFoundException;
use Nette\NotImplementedException;
use App\Classes\LDAP\Attribute\{Factory,Password};
use App\Classes\LDAP\Attribute\Factory;
use App\Classes\LDAP\{Attribute,Server};
use App\Classes\LDAP\Import\LDIF as LDIFImport;
use App\Classes\LDAP\Export\LDIF as LDIFExport;
use App\Exceptions\Import\{GeneralException,VersionException};
use App\Exceptions\InvalidUsage;
use App\Http\Requests\{EntryRequest,EntryAddRequest,ImportRequest};
use App\Ldap\Entry;
use App\View\Components\AttributeType;
class HomeController extends Controller
{
private const LOGKEY = 'CHc';
private function bases(): Collection
{
$base = Server::baseDNs() ?: collect();
private const INTERNAL_POST = ['_auto_value','_key','_rdn','_rdn_new','_rdn_value','_step','_template','_token','_userpassword_hash'];
return $base->transform(function($item) {
return [
'title'=>$item->getRdn(),
'item'=>$item->getDNSecure(),
'lazy'=>TRUE,
'icon'=>'fa-fw fas fa-sitemap',
'tooltip'=>$item->getDn(),
];
});
}
/**
* Debug Page
*
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function debug()
{
return view('debug');
}
/**
* Create a new object in the LDAP server
*
* @param EntryAddRequest $request
* @return \Illuminate\View\View
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
* @throws InvalidUsage
*/
public function entry_add(EntryAddRequest $request): \Illuminate\View\View
public function entry_add(EntryAddRequest $request)
{
if (! old('_step',$request->validated('_step')))
if (! old('step',$request->validated('step')))
abort(404);
$key = $this->request_key($request,collect(old()));
$template = NULL;
$o = new Entry;
$o->setRDNBase($key['dn']);
foreach (collect(old())->except(self::INTERNAL_POST) as $old => $value)
$o->{$old} = array_filter($value);
if (count(array_filter($x=old('objectclass',$request->objectclass)))) {
$o->objectclass = $x;
if (old('_template',$request->validated('template'))) {
$template = $o->template(old('_template',$request->validated('template')));
foreach($o->getAvailableAttributes()->filter(fn($item)=>$item->required) as $ao)
$o->addAttribute($ao,'');
$o->objectclass = [Entry::TAG_NOTAG=>$template->objectclasses->toArray()];
foreach ($o->getAvailableAttributes()
->filter(fn($item)=>$item->names_lc->intersect($template->attributes->keys()->map('strtolower'))->count())
->sortBy(fn($item)=>Arr::get($template->order,$item->name)) as $ao)
{
$o->{$ao->name} = [Entry::TAG_NOTAG=>''];
}
} elseif (count($x=collect(old('objectclass',$request->validated('objectclass')))->dot()->filter())) {
$o->objectclass = Arr::undot($x);
// Also add in our required attributes
foreach ($o->getAvailableAttributes()->filter(fn($item)=>$item->is_must) as $ao)
$o->{$ao->name} = [Entry::TAG_NOTAG=>''];
$o->setRDNBase($key['dn']);
}
$step = $request->get('_step') ? $request->get('_step')+1 : old('_step');
$step = $request->step ? $request->step+1 : old('step');
return view('frame')
->with('subframe','create')
->with('bases',$this->bases())
->with('o',$o)
->with('step',$step)
->with('template',$template)
->with('container',old('container',$key['dn']));
}
@ -87,50 +92,35 @@ class HomeController extends Controller
*
* @param Request $request
* @param string $id
* @return \Illuminate\View\View
* @return \Closure|\Illuminate\Contracts\View\View|string
*/
public function entry_attr_add(Request $request,string $id): \Illuminate\View\View
public function entry_attr_add(Request $request,string $id): string
{
$xx = new \stdClass;
$xx = new \stdClass();
$xx->index = 0;
$dn = $request->dn ? Crypt::decrypt($request->dn) : '';
$o = Factory::create(dn: $dn,attribute: $id,values: [],oc: $request->objectclasses);
$view = $request->noheader
? view(sprintf('components.attribute.widget.%s',$id))
$x = $request->noheader
? (string)view(sprintf('components.attribute.widget.%s',$id))
->with('o',new Attribute($id,[]))
->with('value',$request->value)
->with('loop',$xx)
: view('components.attribute-type')
->with('new',TRUE)
->with('edit',TRUE);
: (new AttributeType(new Attribute($id,[]),TRUE,collect($request->oc ?: [])))->render();
return $view
->with('o',$o)
->with('langtag',Entry::TAG_NOTAG)
->with('template',NULL)
->with('updated',FALSE);
return $x;
}
public function entry_create(EntryAddRequest $request): \Illuminate\Http\RedirectResponse
public function entry_create(EntryAddRequest $request)
{
$key = $this->request_key($request,collect(old()));
$dn = sprintf('%s=%s,%s',$request->get('_rdn'),$request->get('_rdn_value'),$key['dn']);
$dn = sprintf('%s=%s,%s',$request->rdn,$request->rdn_value,$key['dn']);
$o = new Entry;
$o->setDn($dn);
foreach ($request->except(self::INTERNAL_POST) as $key => $value)
foreach ($request->except(['_token','key','step','rdn','rdn_value']) as $key => $value)
$o->{$key} = array_filter($value);
// We need to process and encrypt the password
if ($request->userpassword)
$o->userpassword = $this->password(
$o->getObject('userpassword'),
$request->userpassword,
$request->get('_userpassword_hash'));
try {
$o->save();
@ -141,56 +131,13 @@ class HomeController extends Controller
case 50:
return Redirect::to('/')
->withInput()
->with('failed',sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
default:
abort(599,$e->getDetailedError()->getErrorMessage());
}
// @todo when we create an entry, and it already exists, enable a redirect to it
} catch (LdapRecordException $e) {
return Redirect::back()
->withInput()
->with('failed',sprintf('%s: %s - %s: %s',
__('LDAP Server Error Code'),
$e->getDetailedError()->getErrorCode(),
__($e->getDetailedError()->getErrorMessage()),
$e->getDetailedError()->getDiagnosticMessage(),
));
}
// If there are an _auto_value attributes, we need to invalid those
foreach ($request->get('_auto_value',[]) as $attr => $value) {
Log::debug(sprintf('%s:Removing auto_value attr [%s]',self::LOGKEY,$attr));
Cache::delete($attr.':'.Session::id());
}
return Redirect::to('/')
->withFragment($o->getDNSecure());
}
public function entry_delete(Request $request): \Illuminate\Http\RedirectResponse
{
$dn = Crypt::decryptString($request->dn);
$o = config('server')->fetch($dn);
try {
$o->delete();
} catch (InsufficientAccessException $e) {
$request->flash();
switch ($x=$e->getDetailedError()->getErrorCode()) {
case 50:
return Redirect::to('/')
->withInput()
->with('failed',sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
->withErrors(sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
default:
abort(599,$e->getDetailedError()->getErrorMessage());
}
// @todo To test and valide this Exception is caught
} catch (LdapRecordException $e) {
$request->flash();
@ -198,7 +145,7 @@ class HomeController extends Controller
case 8:
return Redirect::to('/')
->withInput()
->with('failed',sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
->withErrors(sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
default:
abort(599,$e->getDetailedError()->getErrorMessage());
@ -206,14 +153,17 @@ class HomeController extends Controller
}
return Redirect::to('/')
->with('success',sprintf('%s: %s',__('Deleted'),$dn));
->withFragment($o->getDNSecure());
}
public function entry_export(Request $request,string $id): \Illuminate\View\View
public function entry_export(Request $request,string $id)
{
$dn = Crypt::decryptString($id);
$result = Entry::query()
$result = (new Entry)
->query()
//->cache(Carbon::now()->addSeconds(Config::get('ldap.cache.time')))
//->select(['*'])
->setDn($dn)
->recursive()
->get();
@ -225,13 +175,12 @@ class HomeController extends Controller
/**
* Render an available list of objectclasses for an Entry
*
* @param Request $request
* @return Collection
* @param string $id
* @return mixed
*/
public function entry_objectclass_add(Request $request): Collection
public function entry_objectclass_add(Request $request)
{
$dn = $request->get('_key') ? Crypt::decryptString($request->dn) : '';
$oc = Factory::create($dn,'objectclass',$request->oc);
$oc = Factory::create('objectclass',$request->oc);
$ocs = $oc
->structural
@ -253,7 +202,7 @@ class HomeController extends Controller
]);
}
public function entry_password_check(Request $request): Collection
public function entry_password_check(Request $request)
{
$dn = Crypt::decryptString($request->dn);
$o = config('server')->fetch($dn);
@ -261,7 +210,7 @@ class HomeController extends Controller
$password = $o->getObject('userpassword');
$result = collect();
foreach ($password->values->dot() as $key => $value) {
foreach ($password as $key => $value) {
$hash = $password->hash($value);
$compare = Arr::get($request->password,$key);
//Log::debug(sprintf('comparing [%s] with [%s] type [%s]',$value,$compare,$hash::id()),['object'=>$hash]);
@ -276,72 +225,55 @@ class HomeController extends Controller
* Show a confirmation to update a DN
*
* @param EntryRequest $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Foundation\Application|\Illuminate\Http\RedirectResponse
* @throws ObjectNotFoundException
*/
public function entry_pending_update(EntryRequest $request): \Illuminate\Http\RedirectResponse|\Illuminate\View\View
public function entry_pending_update(EntryRequest $request)
{
$dn = Crypt::decryptString($request->dn);
$o = config('server')->fetch($dn);
foreach ($request->except(['_token','dn','_userpassword_hash','userpassword']) as $key => $value)
foreach ($request->except(['_token','dn','userpassword_hash','userpassword']) as $key => $value)
$o->{$key} = array_filter($value,fn($item)=>! is_null($item));
// @todo Need to handle incoming attributes that were modified by MD5Updates Trait (eg: jpegphoto)
// We need to process and encrypt the password
if ($request->userpassword)
$o->userpassword = $this->password(
$o->getObject('userpassword'),
$request->userpassword,
$request->get('_userpassword_hash'));
if ($request->userpassword) {
$passwords = [];
foreach ($request->userpassword as $key => $value) {
// If the password is still the MD5 of the old password, then it hasnt changed
if (($old=Arr::get($o->userpassword,$key)) && ($value === md5($old))) {
array_push($passwords,$old);
continue;
}
if ($value) {
$type = Arr::get($request->userpassword_hash,$key);
array_push($passwords,Attribute\Password::hash_id($type)->encode($value));
}
}
$o->userpassword = $passwords;
}
if (! $o->getDirty())
return Redirect::back()
return back()
->withInput()
->with('note',__('No attributes changed'));
return view('update')
->with('bases',$this->bases())
->with('dn',$dn)
->with('o',$o);
}
public function entry_rename(Request $request): \Illuminate\Http\RedirectResponse|\Illuminate\View\View
{
$from_dn = Crypt::decryptString($request->post('dn'));
Log::info(sprintf('%s:Renaming [%s] to [%s]',self::LOGKEY,$from_dn,$request->post('_rdn_new')));
$o = config('server')->fetch($from_dn);
if (! $o)
return Redirect::back()
->withInput()
->with('note',__('DN doesnt exist'));
try {
$o->rename($request->post('_rdn_new'));
} catch (\Exception $e) {
return Redirect::to('/')
->with('failed',$e->getMessage());
}
return Redirect::to('/')
->withInput(['_key'=>Crypt::encryptString('*dn|'.$o->getDN())])
->with('success',sprintf('%s: %s',__('Entry renamed'),$from_dn));
}
/**
* Update a DN entry
*
* @param EntryRequest $request
* @return \Illuminate\Http\RedirectResponse
* @throws ObjectNotFoundException
* @todo When removing an attribute value, from a multi-value attribute, we have a ghost record showing after the update
* @todo Need to check when removing a single attribute value, do we have a ghost as well? Might be because we are redirecting with input?
*/
public function entry_update(EntryRequest $request): \Illuminate\Http\RedirectResponse
public function entry_update(EntryRequest $request)
{
$dn = Crypt::decryptString($request->dn);
@ -351,7 +283,7 @@ class HomeController extends Controller
$o->{$key} = array_filter($value);
if (! $dirty=$o->getDirty())
return Redirect::back()
return back()
->withInput()
->with('note',__('No attributes changed'));
@ -365,29 +297,29 @@ class HomeController extends Controller
case 50:
return Redirect::to('/')
->withInput()
->with('failed',sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
->withErrors(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()
->with('failed',sprintf('%s: %s - %s: %s',
__('LDAP Server Error Code'),
$e->getDetailedError()->getErrorCode(),
__($e->getDetailedError()->getErrorMessage()),
$e->getDetailedError()->getDiagnosticMessage(),
));
$request->flash();
switch ($x=$e->getDetailedError()->getErrorCode()) {
case 8:
return Redirect::to('/')
->withInput()
->withErrors(sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
default:
abort(599,$e->getDetailedError()->getErrorMessage());
}
}
return Redirect::to('/')
->withInput()
->with('updated',collect($dirty)
->map(fn($item,$key)=>$o->getObject(collect(explode(';',$key))->first()))
->values()
->unique());
->with('updated',collect($dirty)->map(fn($key,$item)=>$o->getObject($item)));
}
/**
@ -396,10 +328,9 @@ class HomeController extends Controller
*
* @param Request $request
* @param Collection|null $old
* @return \Illuminate\View\View
* @throws InvalidUsage
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|View
*/
public function frame(Request $request,?Collection $old=NULL): \Illuminate\View\View
public function frame(Request $request,?Collection $old=NULL): View
{
// If our index was not render from a root url, then redirect to it
if (($request->root().'/' !== url()->previous()) && $request->method() === 'POST')
@ -407,40 +338,19 @@ class HomeController extends Controller
$key = $this->request_key($request,$old);
$view = $old
$view = ($old
? view('frame')->with('subframe',$key['cmd'])
: view('frames.'.$key['cmd']);
// If we are rendering a DN, rebuild our object
if ($key['cmd'] === 'create') {
$o = new Entry;
$o->setRDNBase($key['dn']);
} elseif ($key['dn']) {
// @todo Need to handle if DN is null, for example if the user's session expired and the ACLs dont let them retrieve $key['dn']
$o = config('server')->fetch($key['dn']);
foreach (collect(old())->except(array_merge(self::INTERNAL_POST,['dn'])) as $attr => $value)
$o->{$attr} = $value;
}
: view('frames.'.$key['cmd']))
->with('bases',$this->bases());
return match ($key['cmd']) {
'create' => $view
->with('container',old('container',$key['dn']))
->with('o',$o)
->with('template',NULL)
->with('step',1),
'dn' => $view
->with('dn',$key['dn'])
->with('o',$o)
->with('page_actions',collect([
'copy'=>FALSE,
'create'=>($x=($o->getObjects()->except('entryuuid')->count() > 0)),
'delete'=>(! is_null($xx=$o->getObject('hassubordinates')->value)) && ($xx === 'FALSE'),
'edit'=>$x,
'export'=>$x,
])),
->with('page_actions',collect(['edit'=>TRUE,'copy'=>TRUE])),
'import' => $view,
@ -451,12 +361,13 @@ class HomeController extends Controller
/**
* This is the main page render function
*/
public function home(Request $request): \Illuminate\View\View
public function home(Request $request)
{
// Did we come here as a result of a redirect
return count(old())
? $this->frame($request,collect(old()))
: view('home');
: view('home')
->with('bases',$this->bases());
}
/**
@ -464,16 +375,15 @@ class HomeController extends Controller
*
* @param ImportRequest $request
* @param string $type
* @return \Illuminate\View\View
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Foundation\Application
* @throws GeneralException
* @throws VersionException
*/
public function import(ImportRequest $request,string $type): \Illuminate\View\View
public function import(ImportRequest $request,string $type)
{
switch ($type) {
case 'ldif':
$import = new LDIFImport($x=($request->text ?: $request->file->get()));
Log::debug('Processing LDIF import',['data'=>$x,'import'=>$import]);
break;
default:
@ -492,30 +402,25 @@ class HomeController extends Controller
return view('frame')
->with('subframe','import_result')
->with('bases',$this->bases())
->with('result',$result)
->with('ldif',htmlspecialchars($x));
}
private function password(Password $po,array $values,array $hash): array
public function import_frame()
{
// We need to process and encrypt the password
$passwords = [];
return view('frames.import');
}
foreach (Arr::dot($values) as $dotkey => $value) {
// If the password is still the MD5 of the old password, then it hasnt changed
if (($old=Arr::get($po,$dotkey)) && ($value === md5($old))) {
$passwords[$dotkey] = $value;
continue;
}
if ($value) {
$type = Arr::get($hash,$dotkey);
$passwords[$dotkey] = Password::hash_id($type)
->encode($value);
}
}
return Arr::undot($passwords);
/**
* LDAP Server INFO
*
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function info()
{
return view('frames.info')
->with('s',config('server'));
}
/**
@ -530,8 +435,8 @@ class HomeController extends Controller
// Setup
$cmd = NULL;
$dn = NULL;
$key = ($x=$request->get('_key',old('_key')))
? Crypt::decryptString($x)
$key = $request->get('key',old('key'))
? Crypt::decryptString($request->get('key',old('key')))
: NULL;
// Determine if our key has a command
@ -543,9 +448,9 @@ class HomeController extends Controller
$dn = ($m[2] !== '_NOP') ? $m[2] : NULL;
}
} elseif ($x=old('dn',$request->get('_key'))) {
} elseif (old('dn',$request->get('key'))) {
$cmd = 'dn';
$dn = Crypt::decryptString($x);
$dn = Crypt::decryptString(old('dn',$request->get('key')));
}
return ['cmd'=>$cmd,'dn'=>$dn];
@ -556,18 +461,18 @@ class HomeController extends Controller
*
* @note Our route will validate that types are valid.
* @param Request $request
* @return \Illuminate\View\View
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
* @throws InvalidUsage
*/
public function schema_frame(Request $request): \Illuminate\View\View
public function schema_frame(Request $request)
{
// If an invalid key, we'll 404
if ($request->type && $request->get('_key') && (! config('server')->schema($request->type)->has($request->get('_key'))))
if ($request->type && $request->key && (! config('server')->schema($request->type)->has($request->key)))
abort(404);
return view('frames.schema')
->with('type',$request->type)
->with('key',$request->get('_key'));
->with('key',$request->key);
}
/**
@ -585,9 +490,9 @@ class HomeController extends Controller
* Return the image for the logged in user or anonymous
*
* @param Request $request
* @return \Illuminate\Http\Response
* @return mixed
*/
public function user_image(Request $request): \Illuminate\Http\Response
public function user_image(Request $request)
{
$image = NULL;
$content = NULL;
@ -605,4 +510,4 @@ class HomeController extends Controller
return response($image)
->header('Content-Type',$content);
}
}
}

View File

@ -1,59 +0,0 @@
<?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(FALSE) 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,strtolower($request->term)));
return $attrs
->map(fn($item)=>[
'name'=>$item->name,
'value'=>'',
'category'=>__('Select attribute...')
])
->values();
}
}
}

View File

@ -1,46 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class AcceptLanguage
{
private const LOGKEY = 'MAL';
public function handle(Request $request,Closure $next): mixed
{
if ($locale=$this->parseHttpLocale($request)) {
Log::debug(sprintf('%s:Accept Language changed from [%s] to [%s] from Browser (%s)',self::LOGKEY,app()->getLocale(),$locale,$request->header('Accept-Language')));
app()->setLocale($locale);
}
return $next($request);
}
private function parseHttpLocale(Request $request): string
{
$list = explode(',',$request->server('HTTP_ACCEPT_LANGUAGE',''));
$locales = Collection::make($list)
->map(function ($locale) {
$parts = explode(';',$locale);
$mapping = [];
$mapping['locale'] = trim($parts[0]);
$mapping['factor'] = isset($parts[1])
? Arr::get(explode('=',$parts[1]),1)
: 1;
return $mapping;
})
->sortByDesc(fn($locale)=>$locale['factor']);
return Arr::get($locales->first(),'locale');
}
}

View File

@ -4,7 +4,7 @@ namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Cookie;
class AllowAnonymous
{
@ -17,9 +17,7 @@ class AllowAnonymous
*/
public function handle(Request $request,Closure $next): mixed
{
if ((! config('pla.allow_guest',FALSE))
&& ($request->path() !== 'login')
&& ((! Session::has('username_encrypt')) || (! Session::has('password_encrypt'))))
if (((! Cookie::has('username_encrypt')) || (! Cookie::has('password_encrypt'))) && (! config('pla.allow_guest',FALSE)))
return redirect()
->to('/login');

View File

@ -6,12 +6,13 @@ use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use App\Classes\LDAP\Server;
use App\Ldap\User;
/**
* This sets up our application session with any required values, ultimately for cache optimisation reasons
*/
class ViewVariables
class ApplicationSession
{
/**
* Handle an incoming request.
@ -22,8 +23,9 @@ class ViewVariables
*/
public function handle(Request $request,Closure $next): mixed
{
view()->share('server',Config::get('server'));
view()->share('user',auth()->user() ?: new User);
Config::set('server',new Server);
view()->share('user', auth()->user() ?: new User);
return $next($request);
}

View File

@ -2,16 +2,14 @@
namespace App\Http\Middleware;
use App\Classes\LDAP\Server;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Cookie;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use LdapRecord\Container;
use App\Ldap\Guard;
use App\Ldap\Connection;
class SwapinAuthUser
{
@ -27,24 +25,25 @@ class SwapinAuthUser
{
$key = config('ldap.default');
if (! array_key_exists($key,config('ldap.connections')))
abort(599,sprintf('LDAP default server [%s] configuration doesnt exist?',$key));
if (($request->path() !== 'logout') && Session::has('username_encrypt') && Session::has('password_encrypt')) {
/*
// Rebuild our connection with the authenticated user.
if (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')));
Log::debug('Swapping out configured LDAP credentials with the user\'s session.',['key'=>$key]);
} else
*/
// @todo it seems sometimes we have cookies that show the logged in user, but Auth::user() has expired?
if (Cookie::has('username_encrypt') && Cookie::has('password_encrypt')) {
Config::set('ldap.connections.'.$key.'.username',Cookie::get('username_encrypt'));
Config::set('ldap.connections.'.$key.'.password',Cookie::get('password_encrypt'));
Log::debug('Swapping out configured LDAP credentials with the user\'s cookie.',['key'=>$key,'user'=>Cookie::get('username_encrypt')]);
}
// We need to override our Connection object so that we can store and retrieve the logged in user and swap out the credentials to use them.
$c = Container::getInstance()
->getConnection($key);
$c->setConfiguration(config('ldap.connections.'.$key));
$c->setGuardResolver(fn()=>new Guard($c->getLdapConnection(),$c->getConfiguration()));
Config::set('server',new Server);
Container::getInstance()->addConnection(new Connection(config('ldap.connections.'.$key)),$key);
return $next($request);
}

View File

@ -3,17 +3,12 @@
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use App\Rules\{DNExists,HasStructuralObjectClass};
class EntryAddRequest extends FormRequest
{
private const LOGKEY = 'EAR';
/**
* Get the error messages for the defined validation rules.
*
@ -22,8 +17,8 @@ class EntryAddRequest extends FormRequest
public function messages(): array
{
return [
'_rdn' => __('RDN is required.'),
'_rdn_value' => __('RDN value is required.'),
'rdn' => __('RDN is required.'),
'rdn_value' => __('RDN value is required.'),
];
}
@ -39,24 +34,14 @@ class EntryAddRequest extends FormRequest
if (request()->method() === 'GET')
return [];
$r = request() ?: collect();
$rk = array_keys($r->all());
return config('server')
->schema('attributetypes')
->filter(fn($item)=>$item->names_lc->intersect($rk)->count())
->transform(function($item) use ($rk) {
// Set the attributetype name
if (($x=$item->names_lc->intersect($rk))->count() === 1)
$item->setName($x->pop());
return $item;
})
->map(fn($item)=>$item->validation($r->get('objectclass',[])))
->intersectByKeys($this->request)
->map(fn($item)=>$item->validation(request()->get('objectclass')))
->filter()
->flatMap(fn($item)=>$item)
->merge([
'_key' => [
'key' => [
'required',
new DNExists,
function (string $attribute,mixed $value,\Closure $fail) {
@ -71,68 +56,14 @@ class EntryAddRequest extends FormRequest
}
},
],
'_rdn' => 'required_if:_step,2|string|min:1',
'_rdn_value' => 'required_if:_step,2|string|min:1',
'_step' => 'int|min:1|max:2',
'rdn' => 'required_if:step,2|string|min:1',
'rdn_value' => 'required_if:step,2|string|min:1',
'step' => 'int|min:1|max:2',
'objectclass'=>[
'required',
'array',
'min:1',
'max:1',
],
'objectclass._null_' => [
function (string $attribute,mixed $value,\Closure $fail) {
$oc = collect($value)->dot()->filter();
// If this is step 1 and there is no objectclass, and no template, then fail
if ((! $oc->count())
&& (request()->post('_step') == 1)
&& (! request()->post('template')))
{
$fail(__('Select an objectclass or a template'));
}
// Cant have both an objectclass and a template
if (request()->post('template') && $oc->count())
$fail(__('You cannot select a template and an objectclass'));
},
'array',
'min:1',
new HasStructuralObjectClass,
],
'template' => [
function (string $attribute,mixed $value,\Closure $fail) {
$oc = collect(request()->post('objectclass'))->dot()->filter();
// If this is step 1 and there is no objectclass, and no template, then fail
if ((! collect($value)->filter()->count())
&& (request()->post('_step') == 1)
&& (! $oc->count()))
{
$fail(__('Select an objectclass or a template'));
}
// Cant have both an objectclass and a template
if ($oc->count() && strlen($value))
$fail(__('You cannot select a template and an objectclass'));
},
],
'_auto_value' => 'nullable|array|min:1',
'_auto_value.*' => [
'nullable',
function (string $attribute,mixed $value,\Closure $fail) {
$attr = preg_replace('/^_auto_value\./','',$attribute);
// If the value has been overritten, then our auto_value is invalid
if (! collect(request()->get($attr))->dot()->contains($value))
return;
$cache = Cache::get($attr.':'.Session::id());
Log::debug(sprintf('%s:Autovalue for Attribute [%s] in Session [%s] Retrieved [%d](%d)',self::LOGKEY,$attr,Session::id(),$cache,$value));
if ($cache !== (int)$value)
$fail(__('Lock expired, please re-submit.'));
}
]
])
->toArray();

View File

@ -10,25 +10,13 @@ class EntryRequest extends FormRequest
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*/
public function rules(): array
{
$r = request() ?: collect();
$rk = array_keys($r->all());
return config('server')
->schema('attributetypes')
->filter(fn($item)=>$item->names_lc->intersect($rk)->count())
->transform(function($item) use ($rk) {
// Set the attributetype name
if (($x=$item->names_lc->intersect($rk))->count() === 1)
$item->setName($x->pop());
return $item;
})
->map(fn($item)=>$item->validation($r->get('objectclass',[])))
->intersectByKeys($this->request)
->map(fn($item)=>$item->validation(request()?->get('objectclass') ?: []))
->filter()
->flatMap(fn($item)=>$item)
->toArray();

20
app/Ldap/Connection.php Normal file
View File

@ -0,0 +1,20 @@
<?php
namespace App\Ldap;
use LdapRecord\Connection as ConnectionBase;
use LdapRecord\LdapInterface;
class Connection extends ConnectionBase
{
public function __construct($config = [], LdapInterface $ldap = null)
{
parent::__construct($config,$ldap);
// We need to override this so that we use our own Guard, that stores the users credentials in the session
$this->authGuardResolver = function () {
return new Guard($this->ldap, $this->configuration);
};
}
}

View File

@ -3,40 +3,21 @@
namespace App\Ldap;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use LdapRecord\Support\Arr;
use LdapRecord\Models\Model;
use LdapRecord\Query\Model\Builder;
use App\Classes\Template;
use App\Classes\LDAP\Attribute;
use App\Classes\LDAP\Attribute\Factory;
use App\Classes\LDAP\Export\LDIF;
use App\Exceptions\Import\AttributeException;
use App\Exceptions\InvalidUsage;
/**
* An Entry in an LDAP server
*
* @notes https://ldap.com/ldap-dns-and-rdns
*/
class Entry extends Model
{
private const TAG_CHARS = 'a-zA-Z0-9-';
public const LANG_TAG_PREFIX = 'lang-';
public const TAG_CHARS_LANG = self::LANG_TAG_PREFIX.'['.self::TAG_CHARS.']+';
public const TAG_NOTAG = '_null_';
// Our Attribute objects
private Collection $objects;
// Templates that apply to this entry
private(set) Collection $templates;
private bool $noObjectAttributes = FALSE;
// For new entries, this is the container that this entry will be stored in
private string $rdnbase;
@ -44,31 +25,9 @@ class Entry extends Model
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->objects = collect();
// Load any templates
$this->templates = Cache::remember('templates'.Session::id(),config('ldap.cache.time'),function() {
$template_dir = Storage::disk(config('pla.template.dir'));
$templates = collect();
foreach (array_filter($template_dir->files('.',TRUE),fn($item)=>Str::endsWith($item,'.json')) as $file) {
if (config('pla.template.exclude_system',FALSE) && Str::doesntContain($file,'/'))
continue;
$to = new Template($file);
if ($to->invalid) {
Log::debug(sprintf('Template [%s] is not valid (%s) - ignoring',$file,$to->reason));
} else {
$templates->put($file,new Template($file));
}
}
return $templates;
});
parent::__construct($attributes);
}
public function discardChanges(): static
@ -84,56 +43,63 @@ class Entry extends Model
/**
* This function overrides getAttributes to use our collection of Attribute objects instead of the models attributes.
*
* This returns an array that should be consistent with $this->attributes
*
* @return array
* @note $this->attributes may not be updated with changes
*/
public function getAttributes(): array
{
return $this->objects
->flatMap(fn($item)=>
($item->no_attr_tags)
? [strtolower($item->name)=>$item->values]
: $item->values
->flatMap(fn($v,$k)=>[strtolower($item->name.($k !== self::TAG_NOTAG ? ';'.$k : ''))=>$v]))
->map(fn($item)=>$item->values)
->toArray();
}
/**
* Determine if the new and old values for a given key are equivalent.
*
* @todo This function barfs on language tags, eg: key = givenname;lang-ja
*/
protected function originalIsEquivalent(string $key): bool
{
$key = $this->normalizeAttributeKey($key);
list($attribute,$tag) = $this->keytag($key);
// @todo Silently ignore keys of language tags - we should work with them
if (str_contains($key,';'))
return TRUE;
return ((! array_key_exists($key,$this->original)) && (! $this->objects->has($attribute)))
|| (! $this->getObject($attribute)->isDirty());
return ((! array_key_exists($key,$this->original)) && (! $this->objects->has($key)))
|| (! $this->getObject($key)->isDirty());
}
public static function query(bool $noattrs=false): Builder
{
$o = new static;
if ($noattrs)
$o->noObjectAttributes();
return $o->newQuery();
}
/**
* As attribute values are updated, or new ones created, we need to mirror that
* into our $objects. This is called when we $o->key = $value
*
* This function should update $this->attributes and correctly reflect changes in $this->objects
* into our $objects
*
* @param string $key
* @param mixed $value
* @return $this
*/
public function setAttribute(string $key,mixed $value): static
public function setAttribute(string $key, mixed $value): static
{
foreach ($value as $k => $v)
parent::setAttribute($key.($k !== self::TAG_NOTAG ? ';'.$k : ''),$v);
parent::setAttribute($key,$value);
$key = $this->normalizeAttributeKey($key);
list($attribute,$tags) = $this->keytag($key);
$o = $this->objects->get($attribute) ?: Factory::create($this->dn ?: '',$attribute,[],Arr::get($this->attributes,'objectclass',[]));
$o->values = collect($value);
if ((! $this->objects->get($key)) && $value) {
$this->objects->put($key,Factory::create($key,$value));
$this->objects->put($key,$o);
} elseif ($this->objects->get($key)) {
$this->objects->get($key)->value = $this->attributes[$key];
}
return $this;
}
@ -158,17 +124,6 @@ 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->enabled
&& (! $item->objectclasses
->map('strtolower')
->diff(array_map('strtolower',Arr::get($this->attributes,'objectclass')))
->count()))
->sortBy(fn($item)=>$item->title);
}
return $this;
}
@ -178,105 +133,68 @@ class Entry extends Model
* Return a key to use for sorting
*
* @return string
* @todo This should be the DN in reverse order
*/
public function getSortKeyAttribute(): string
{
return collect(explode(',',$this->getDn()))->reverse()->join(',');
return $this->getDn();
}
/* METHODS */
/**
* Add an attribute to this entry, if the attribute already exists, then we'll add the value to the existing item.
*
* This is primarily used by LDIF imports, where attributes have multiple entries over multiple lines
*
* @param string $key
* @param mixed $value
* @return void
* @throws AttributeException
* @note Attributes added this way dont have objectclass information, and the Model::attributes are not populated
*/
public function addAttributeItem(string $key,mixed $value): void
public function addAttribute(string $key,mixed $value): void
{
// While $value is mixed, it can only be a string
if (! is_string($value))
throw new \Exception('value should be a string');
$key = $this->normalizeAttributeKey(strtolower($key));
$key = $this->normalizeAttributeKey($key);
// If the attribute name has tags
list($attribute,$tag) = $this->keytag($key);
if (! config('server')->schema('attributetypes')->has($key))
throw new AttributeException(sprintf('Schema doesnt have attribute [%s]',$key));
if (config('server')->get_attr_id($attribute) === FALSE)
throw new AttributeException(sprintf('Schema doesnt have attribute [%s]',$attribute));
if ($x=$this->objects->get($key)) {
$x->addValue($value);
$o = $this->objects->get($attribute) ?: Attribute\Factory::create($this->dn ?: '',$attribute,[]);
$o->addValue($tag,[$value]);
$this->objects->put($key,$o);
}
/**
* Export this record
*
* @param string $method
* @param string $scope
* @return string
* @throws \Exception
*/
public function export(string $method,string $scope): string
{
// @todo To implement
switch ($scope) {
case 'base':
case 'one':
case 'sub':
break;
default:
throw new \Exception('Export scope unknown:'.$scope);
}
switch ($method) {
case 'ldif':
return new LDIF(collect($this));
default:
throw new \Exception('Export method not implemented:'.$method);
} else {
$this->objects->put($key,Attribute\Factory::create($key,Arr::wrap($value)));
}
}
/**
* Convert all our attribute values into an array of Objects
*
* @param array $attributes
* @return Collection
*/
private function getAttributesAsObjects(): Collection
public function getAttributesAsObjects(): Collection
{
$result = collect();
$entry_oc = Arr::get($this->attributes,'objectclass',[]);
foreach ($this->attributes as $attrtag => $values) {
list($attribute,$tags) = $this->keytag($attrtag);
foreach ($this->attributes as $attribute => $value) {
// If the attribute name has language tags
$matches = [];
if (preg_match('/^([a-zA-Z]+)(;([a-zA-Z-;]+))+/',$attribute,$matches)) {
$attribute = $matches[1];
$orig = Arr::get($this->original,$attrtag,[]);
// If the attribute doesnt exist we'll create it
$o = Arr::get($result,$attribute,Factory::create($attribute,[]));
$o->setLangTag($matches[3],$value);
// If the attribute doesnt exist we'll create it
$o = Arr::get(
$result,
$attribute,
Factory::create(
$this->dn,
$attribute,
[$tags=>$orig],
$entry_oc,
));
} else {
$o = Factory::create($attribute,$value);
}
$o->addValue($tags,$values);
$o->addValueOld($tags,Arr::get($this->original,$attrtag));
if (! $result->has($attribute)) {
// Set the rdn flag
if (preg_match('/^'.$attribute.'=/i',$this->dn))
$o->setRDN();
$result->put($attribute,$o);
// Store our original value to know if this attribute has changed
$o->oldValues(Arr::get($this->original,$attribute,[]));
$result->put($attribute,$o);
}
}
$sort = collect(config('pla.attr_display_order',[]))->map(fn($item)=>strtolower($item));
@ -317,8 +235,8 @@ class Entry extends Model
{
$result = collect();
foreach (($this->getObject('objectclass')?->values ?: []) as $oc)
$result = $result->merge(config('server')->schema('objectclasses',$oc)->all_attributes);
foreach ($this->objectclass as $oc)
$result = $result->merge(config('server')->schema('objectclasses',$oc)->attributes);
return $result;
}
@ -344,31 +262,6 @@ class Entry extends Model
->filter(fn($item)=>$item->is_internal);
}
/**
* Identify the language tags (RFC 3866) used by this entry
*
* @return Collection
*/
public function getLangTags(): Collection
{
return $this->getObjects()
->map(fn($item)=>$item->langtags);
}
/**
* Of all the items with lang tags, which ones have more than 1 lang tag
*
* @return Collection
*/
public function getLangMultiTags(): Collection
{
return $this->getLangTags()
->map(fn($item)=>$item->values()
->map(fn($item)=>explode(';',$item))
->filter(fn($item)=>count($item) > 1))
->filter(fn($item)=>$item->count());
}
/**
* Get an attribute as an object
*
@ -394,29 +287,6 @@ class Entry extends Model
return $this->objects;
}
/**
* Find other attribute tags used by this entry
*
* @return Collection
*/
public function getOtherTags(): Collection
{
return $this->getObjects()
->filter(fn($item)=>! $item->no_attr_tags)
->map(fn($item)=>$item
->values
->keys()
->filter(fn($item)=>
$item && collect(explode(';',$item))->filter(
fn($item)=>
(! preg_match(sprintf('/^%s$/',self::TAG_NOTAG),$item))
&& (! preg_match(sprintf('/^%s$/',self::TAG_CHARS_LANG),$item))
)
->count())
)
->filter(fn($item)=>$item->count());
}
/**
* Return a list of attributes without any values
*
@ -430,10 +300,10 @@ class Entry extends Model
private function getRDNObject(): Attribute\RDN
{
$o = new Attribute\RDN('','dn',['']);
// @todo for an existing object, rdnbase would be null, so dynamically get it from the DN.
$o = new Attribute\RDN('dn',['']);
// @todo for an existing object, return the base.
$o->setBase($this->rdnbase);
$o->setAttributes($this->getAvailableAttributes()->filter(fn($item)=>$item->is_must));
$o->setAttributes($this->getAvailableAttributes()->filter(fn($item)=>$item->required));
return $o;
}
@ -441,22 +311,12 @@ class Entry extends Model
/**
* Return this list of user attributes
*
* @param string $tag If null return all tags
* @return Collection
*/
public function getVisibleAttributes(string $tag=''): Collection
public function getVisibleAttributes(): Collection
{
static $cache = [];
if (! Arr::get($cache,$tag ?: '_all_')) {
$ot = $this->getOtherTags();
$cache[$tag ?: '_all_'] = $this->objects
->filter(fn($item)=>(! $item->is_internal) && ((! $item->no_attr_tags) || (! $tag) || ($tag === Entry::TAG_NOTAG)))
->filter(fn($item)=>(! $tag) || $ot->has($item->name_lc) || count($item->tagValues($tag)) > 0);
}
return $cache[$tag ?: '_all_'];
return $this->objects
->filter(fn($item)=>! $item->is_internal);
}
public function hasAttribute(int|string $key): bool
@ -466,18 +326,33 @@ class Entry extends Model
}
/**
* Did this query generate a size limit exception
* Export this record
*
* @return bool
* @throws \LdapRecord\ContainerException
* @param string $method
* @param string $scope
* @return string
* @throws \Exception
*/
public function hasMore(): bool
public function export(string $method,string $scope): string
{
return $this->getConnectionContainer()
->getConnection()
->getLdapConnection()
->getDetailedError()
?->getErrorCode() === 4;
// @todo To implement
switch ($scope) {
case 'base':
case 'one':
case 'sub':
break;
default:
throw new \Exception('Export scope unknown:'.$scope);
}
switch ($method) {
case 'ldif':
return new LDIF(collect($this));
default:
throw new \Exception('Export method not implemented:'.$method);
}
}
/**
@ -487,65 +362,59 @@ class Entry extends Model
*/
public function icon(): string
{
$objectclasses = ($x=$this->getObject('objectclass'))
? $x->tagValues()
->map(fn($item)=>strtolower($item))
: collect();
$objectclasses = array_map('strtolower',$this->objectclass);
// Return icon based upon objectClass value
if ($objectclasses->intersect([
'account',
'inetorgperson',
'organizationalperson',
'person',
'posixaccount',
])->count())
if (in_array('person',$objectclasses) ||
in_array('organizationalperson',$objectclasses) ||
in_array('inetorgperson',$objectclasses) ||
in_array('account',$objectclasses) ||
in_array('posixaccount',$objectclasses))
return 'fas fa-user';
elseif ($objectclasses->contains('organization'))
elseif (in_array('organization',$objectclasses))
return 'fas fa-university';
elseif ($objectclasses->contains('organizationalunit'))
elseif (in_array('organizationalunit',$objectclasses))
return 'fas fa-object-group';
elseif ($objectclasses->intersect([
'posixgroup',
'groupofnames',
'groupofuniquenames',
'group',
])->count())
elseif (in_array('posixgroup',$objectclasses) ||
in_array('groupofnames',$objectclasses) ||
in_array('groupofuniquenames',$objectclasses) ||
in_array('group',$objectclasses))
return 'fas fa-users';
elseif ($objectclasses->intersect([
'dcobject',
'domainrelatedobject',
'domain',
'builtindomain',
])->count())
elseif (in_array('dcobject',$objectclasses) ||
in_array('domainrelatedobject',$objectclasses) ||
in_array('domain',$objectclasses) ||
in_array('builtindomain',$objectclasses))
return 'fas fa-network-wired';
elseif ($objectclasses->contains('alias'))
elseif (in_array('alias',$objectclasses))
return 'fas fa-theater-masks';
elseif ($objectclasses->contains('country'))
return sprintf('flag %s',strtolower(Arr::get($this->c ?: [],0)));
elseif (in_array('country',$objectclasses))
return sprintf('flag %s',strtolower(Arr::get($this->c,0)));
elseif ($objectclasses->contains('device'))
elseif (in_array('device',$objectclasses))
return 'fas fa-mobile-alt';
elseif ($objectclasses->contains('document'))
elseif (in_array('document',$objectclasses))
return 'fas fa-file-alt';
elseif ($objectclasses->contains('iphost'))
elseif (in_array('iphost',$objectclasses))
return 'fas fa-wifi';
elseif ($objectclasses->contains('room'))
elseif (in_array('room',$objectclasses))
return 'fas fa-door-open';
elseif ($objectclasses->contains('server'))
elseif (in_array('server',$objectclasses))
return 'fas fa-server';
elseif ($objectclasses->contains('openldaprootdse'))
elseif (in_array('openldaprootdse',$objectclasses))
return 'fas fa-info';
// Default
@ -553,25 +422,15 @@ class Entry extends Model
}
/**
* Given an LDAP attribute, this will return the attribute name and the tag
* eg: description;lang-cn will return [description,lang-cn]
* Dont convert our $this->attributes to $this->objects when creating a new Entry::class
*
* @param string $key
* @return array
* @return $this
*/
private function keytag(string $key): array
public function noObjectAttributes(): static
{
$matches = [];
if (preg_match(sprintf('/^([%s]+);+([%s;]+)/',self::TAG_CHARS,self::TAG_CHARS),$key,$matches)) {
$attribute = $matches[1];
$tags = $matches[2];
$this->noObjectAttributes = TRUE;
} else {
$attribute = $key;
$tags = self::TAG_NOTAG;
}
return [$attribute,$tags];
return $this;
}
public function setRDNBase(string $bdn): void
@ -580,13 +439,5 @@ class Entry extends Model
throw new InvalidUsage('Cannot set RDN base on existing entries');
$this->rdnbase = $bdn;
$this->templates = $this->templates
->filter(fn($item)=>(! $item->regexp) || preg_match($item->regexp,$bdn));
}
public function template(string $item): Template|Null
{
return Arr::get($this->templates,$item);
}
}

View File

@ -2,20 +2,26 @@
namespace App\Ldap;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cookie;
// use Illuminate\Support\Facades\Crypt;
use LdapRecord\Auth\Guard as GuardBase;
class Guard extends GuardBase
{
public function attempt(string $username, string $password, bool $stayBound = false): bool
{
Log::info(sprintf('Attempting login for [%s] with password [%s]',$username,($password ? str_repeat('*',16) : str_repeat('?',16))));
if ($result = parent::attempt($username,$password,$stayBound)) {
// Store user details so we can swap in auth details in SwapinAuthUser
session()->put('username_encrypt',Crypt::encryptString($username));
session()->put('password_encrypt',Crypt::encryptString($password));
/*
* We can either use our session or cookies to store this. If using session, then Http/Kernel needs to be
* updated to start a session for API calls.
// We need to store our password so that we can swap in the user in during SwapinAuthUser::class middleware
request()->session()->put('username_encrypt',Crypt::encryptString($username));
request()->session()->put('password_encrypt',Crypt::encryptString($password));
*/
// For our API calls, we store the cookie - which our cookies are already encrypted
Cookie::queue('username_encrypt',$username);
Cookie::queue('password_encrypt',$password);
}
return $result;

View File

@ -31,7 +31,7 @@ class LdapUserRepository extends LdapUserRepositoryBase
return $this->query()->find($credentials['dn']);
// Look for a user using all our baseDNs
foreach (Server::baseDNs(FALSE) as $base) {
foreach (Server::baseDNs() as $base) {
$query = $this->query()->setBaseDn($base);
foreach ($credentials as $key => $value) {
@ -67,7 +67,7 @@ class LdapUserRepository extends LdapUserRepositoryBase
public function findByGuid($guid): ?Model
{
// Look for a user using all our baseDNs
foreach (Server::baseDNs(FALSE) as $base) {
foreach (Server::baseDNs() as $base) {
$user = $this->query()->setBaseDn($base)->findByGuid($guid);
if ($user)

View File

@ -14,13 +14,10 @@ use LdapRecord\Models\Model as LdapRecord;
*/
class LoginObjectclassRule implements Rule
{
public function passes(LdapRecord $user,?Eloquent $model=NULL): bool
public function passes(LdapRecord $user, Eloquent $model = null): bool
{
if ($x=config('pla.login.objectclass')) {
return count(array_intersect(
array_map('strtolower',$user?->objectclass ?: []),
array_map('strtolower',$x)
));
return count(array_intersect($user->objectclass,$x));
// Otherwise allow the user to login
} else {

View File

@ -2,6 +2,7 @@
namespace App\Providers;
use Illuminate\Support\Collection;
use Illuminate\Support\ServiceProvider;
use LdapRecord\Configuration\DomainConfiguration;
use LdapRecord\Laravel\LdapRecord;
@ -28,5 +29,11 @@ class AppServiceProvider extends ServiceProvider
public function boot(): void
{
$this->loadViewsFrom(__DIR__.'/../../resources/themes/architect/views/','architect');
// Enable pluck on collections to work on private values
Collection::macro('ppluck',
fn($attr)=>$this
->map(fn($item)=>$item->{$attr})
->values());
}
}

View File

@ -20,11 +20,10 @@ class HasStructuralObjectClass implements ValidationRule
*/
public function validate(string $attribute,mixed $value,Closure $fail): void
{
foreach (collect($value)->dot() as $item)
foreach ($value as $item)
if ($item && config('server')->schema('objectclasses',$item)->isStructural())
return;
if (collect($value)->dot()->filter()->count())
$fail(__('There isnt a Structural Objectclass.'));
$fail('There isnt a Structural Objectclass.');
}
}

View File

@ -11,9 +11,8 @@ trait MD5Updates
{
public function isDirty(): bool
{
foreach ($this->values_old->dot()->keys()->merge($this->values->dot()->keys())->unique() as $dotkey)
if ((Arr::get($this->values_old->dot(),$dotkey) !== Arr::get($this->values->dot(),$dotkey))
&& (md5(Arr::get($this->values_old->dot(),$dotkey)) !== Arr::get($this->values->dot(),$dotkey)))
foreach ($this->values->diff($this->oldValues) as $key => $value)
if (md5(Arr::get($this->oldValues,$key)) !== $value)
return TRUE;
return FALSE;

View File

@ -2,11 +2,9 @@
namespace App\View\Components;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
use App\Classes\LDAP\Attribute as LDAPAttribute;
use App\Classes\Template;
class Attribute extends Component
{
@ -14,37 +12,30 @@ class Attribute extends Component
public bool $edit;
public bool $new;
public bool $old;
public bool $updated;
public ?Template $template;
public ?string $na;
/**
* Create a new component instance.
*/
public function __construct(?LDAPAttribute $o,bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL)
{
/**
* Create a new component instance.
*/
public function __construct(?LDAPAttribute $o,bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,?string $na=NULL)
{
$this->o = $o;
$this->edit = $edit;
$this->old = $old;
$this->new = $new;
$this->updated = $updated;
$this->template = $template;
}
$this->na = $na;
}
/**
* Get the view / contents that represent the component.
*
* @return View|string
*/
public function render(): View|string
{
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\Contracts\View\View|\Closure|string
*/
public function render()
{
return $this->o
? $this->o
->render(
edit: $this->edit,
old: $this->old,
new: $this->new,
updated: $this->updated,
template: $this->template)
: __('Unknown');
}
->render($this->edit,$this->old,$this->new)
: $this->na;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use Illuminate\View\Component;
use App\Classes\LDAP\Attribute as LDAPAttribute;
class AttributeType extends Component
{
public Collection $oc;
public LDAPAttribute $o;
public bool $new;
/**
* Create a new component instance.
*/
public function __construct(LDAPAttribute $o,bool $new=FALSE,?Collection $oc=NULL)
{
$this->o = $o;
$this->oc = $oc;
$this->new = $new;
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.attribute-type')
->with('o',$this->o)
->with('oc',$this->oc)
->with('new',$this->new);
}
}

View File

@ -1,31 +1,35 @@
<?php
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use App\Http\Middleware\{AcceptLanguage,AllowAnonymous,CheckUpdate,SwapinAuthUser,ViewVariables};
use App\Http\Middleware\{AllowAnonymous,ApplicationSession,CheckUpdate,SwapinAuthUser};
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->appendToGroup(
group: 'web',
middleware: [
AcceptLanguage::class,
AllowAnonymous::class,
SwapinAuthUser::class,
ViewVariables::class,
CheckUpdate::class,
]);
$middleware->appendToGroup('web', [
ApplicationSession::class,
SwapinAuthUser::class,
CheckUpdate::class,
]);
$middleware->prependToGroup('api', [
EncryptCookies::class,
ApplicationSession::class,
SwapinAuthUser::class,
AllowAnonymous::class,
]);
$middleware->trustProxies(at: [
'10.0.0.0/8',
'127.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/12',
]);

View File

@ -1,5 +1,5 @@
<?php
return [
App\Providers\AppServiceProvider::class,
App\Providers\AppServiceProvider::class,
];

View File

@ -7,15 +7,13 @@
"require": {
"ext-fileinfo": "*",
"ext-ldap": "*",
"ext-openssl": "*",
"php": "^8.4",
"directorytree/ldaprecord-laravel": "^3.0",
"laravel/framework": "^12.0",
"laravel/framework": "^11.9",
"laravel/sanctum": "^4.0",
"laravel/ui": "^4.5"
},
"require-dev": {
"amirami/localizator": "^0.14@dev",
"barryvdh/laravel-debugbar": "^3.6",
"fakerphp/faker": "^1.23",
"mockery/mockery": "^1.6",

1253
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -43,6 +43,19 @@ return [
'debug' => (bool) env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| the application so that it's available within Artisan commands.
|
*/
'url' => env('APP_URL', 'http://localhost'),
/*
|--------------------------------------------------------------------------
| Application Timezone

View File

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

View File

@ -15,7 +15,7 @@ return [
|
*/
'default' => env('LDAP_CONNECTION', 'ldap'),
'default' => env('LDAP_CONNECTION', 'openldap'),
/*
|--------------------------------------------------------------------------
@ -30,51 +30,53 @@ return [
'connections' => [
'ldap' => [
'openldap' => [
'hosts' => [env('LDAP_HOST', '127.0.0.1')],
'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'),
'password' => env('LDAP_PASSWORD', 'secret'),
'port' => env('LDAP_PORT', 389),
'base_dn' => env('LDAP_BASE_DN', 'dc=local,dc=com'),
'timeout' => env('LDAP_TIMEOUT', 5),
'use_ssl' => env('LDAP_SSL', false),
'use_tls' => env('LDAP_TLS', false),
'name' => env('LDAP_NAME','LDAP Server'),
],
'ldaps' => [
'openldaps' => [
'hosts' => [env('LDAP_HOST', '127.0.0.1')],
'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'),
'password' => env('LDAP_PASSWORD', 'secret'),
'port' => env('LDAP_PORT', 636),
'base_dn' => env('LDAP_BASE_DN', 'dc=local,dc=com'),
'timeout' => env('LDAP_TIMEOUT', 5),
'use_ssl' => env('LDAP_SSL', true),
'use_tls' => env('LDAP_TLS', false),
'name' => env('LDAP_NAME','LDAPS Server'),
],
'starttls' => [
'openldaptls' => [
'hosts' => [env('LDAP_HOST', '127.0.0.1')],
'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'),
'password' => env('LDAP_PASSWORD', 'secret'),
'port' => env('LDAP_PORT', 389),
'base_dn' => env('LDAP_BASE_DN', 'dc=local,dc=com'),
'timeout' => env('LDAP_TIMEOUT', 5),
'use_ssl' => env('LDAP_SSL', false),
'use_tls' => env('LDAP_TLS', true),
'name' => env('LDAP_NAME','LDAP-TLS 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',
],
*/
],
@ -118,51 +120,58 @@ return [
*/
'validation' => [
'objectclass' => [
'objectclass.*'=>[
'objectclass'=>[
'required',
'array',
'min:1',
new HasStructuralObjectClass,
]
],
'gidnumber' => [
'gidnumber.*'=> [
'gidnumber'=> [
'sometimes',
'array',
'max:1'
],
'gidnumber.*.*' => [
'gidnumber.*' => [
'nullable',
'integer',
'max:65535'
]
],
'mail' => [
'mail.*'=>[
'mail'=>[
'sometimes',
'array',
'min:1'
],
'mail.*.*' => [
'mail.*' => [
'nullable',
'email'
]
],
'userpassword' => [
'userpassword.*' => [
'userpassword' => [
'sometimes',
'array',
'min:1'
],
'userpassword.*.*' => [
'userpassword.*' => [
'nullable',
'min:8'
]
],
'uidnumber' => [
'uidnumber.*' => [
'uidnumber' => [
'sometimes',
'array',
'max:1'
],
'uidnumber.*.*' => [
'uidnumber.*' => [
'nullable',
'integer',
'max:65535'
]
],
],
];
];

View File

@ -54,7 +54,6 @@
1.3.6.1.4.1.42.2.27.8.5.1:passwordPolicyRequest
1.3.6.1.4.1.42.2.27.9.5.2:GetEffectiveRights control::May be used to determine what operations a given user may perform on a specified entry.
1.3.6.1.4.1.1466.101.119.1:Dynamic Directory Services Refresh Request:RFC 2589
1.3.6.1.4.1.1466.115.121.1.25:"guide" syntax-name:RFC 4517
1.3.6.1.4.1.1466.20036:LDAP_NOTICE_OF_DISCONNECTION
1.3.6.1.4.1.1466.20037:Transport Layer Security Extension:RFC 2830:This operation provides for TLS establishment in an LDAP association and is defined in terms of an LDAP extended request.
1.3.6.1.4.1.1466.29539.1:LDAP_CONTROL_ATTR_SIZELIMIT

View File

@ -1,59 +0,0 @@
<?php
return [
/**
* Localize types of translation strings.
*/
'localize' => [
/**
* Short keys. This is the default for Laravel.
* They are stored in PHP files inside folders name by their locale code.
* Laravel comes with default: auth.php, pagination.php, passwords.php and validation.php
*/
'default' => true,
/**
* Translations strings as key.
* They are stored in JSON file for each locale.
*/
'json' => true,
],
/**
* Search criteria for files.
*/
'search' => [
/**
* Directories which should be looked inside.
*/
'dirs' => ['app','resources/views'],
/**
* Subdirectories which will be excluded.
* The values must be relative to the included directory paths.
*/
'exclude' => [
//
],
/**
* Patterns by which files should be queried.
* The values can be a regular expression, glob, or just a string.
*/
'patterns' => ['*.php'],
/**
* Functions that the strings will be extracted from.
* Add here any custom defined functions.
* NOTE: The translation string should always be the first argument.
*/
'functions' => ['__', 'trans', '@lang']
],
/**
* Should the localize command sort extracted strings alphabetically?
*/
'sort' => true,
];

View File

@ -68,7 +68,7 @@ return [
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'info'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => env('LOG_DAILY_DAYS', 14),
'replace_placeholders' => true,
],

View File

@ -43,17 +43,6 @@ return [
'allow_guest' => env('LDAP_ALLOW_GUEST',FALSE),
/*
|--------------------------------------------------------------------------
| Base DNs
|--------------------------------------------------------------------------
|
| Normally PLA will get the base DNs from the rootDSE's namingcontexts
| entry. Instead of using that, you can define your own base DNs to use.
|
*/
'base_dns' => ($x=env('LDAP_BASE_DN', NULL)) ? explode(':',$x) : NULL,
/*
|--------------------------------------------------------------------------
| Custom Date Format
@ -84,20 +73,7 @@ return [
* setup.
*/
'login' => [
// Attribute used to find user for login
'attr' => [strtolower(env('LDAP_LOGIN_ATTR','uid')) => env('LDAP_LOGIN_ATTR_DESC','User ID')],
// Objectclass that users must contain to login
'objectclass' => explode(',',env('LDAP_LOGIN_OBJECTCLASS', 'posixAccount')),
// Alert if DN is being used, and the login fails, and the the DN doesnt exist
'alert_rootdn' => env('LDAP_ALERT_ROOTDN',TRUE) && strtolower(env('LDAP_LOGIN_ATTR','uid')) === 'dn',
],
'template' => [
'dir' => env('LDAP_TEMPLATE_DRIVER','templates'),
'exclude_system' => env('LDAP_TEMPLATE_EXCLUDE_SYSTEM',FALSE),
'getnextnumber' => [
'gidnumber' => env('LDAP_TEMPLATE_GIDNUMBER_START', 1000),
'uidnumber' => env('LDAP_TEMPLATE_UIDNUMBER_START', 1000),
],
'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
],
];

View File

@ -7,6 +7,7 @@ php=${PHP_DIR:-/app}
composer=${COMPOSER_HOME:-/var/cache/composer}
SITE_USER=${SITE_USER:-www-data}
MEMCACHED_START=${MEMCACHED_START:-FALSE}
RUN_USER=$(id -u)
[ "${RUN_USER}" = "0" ] && USE_SU=1
@ -39,6 +40,12 @@ echo "* Started with [$@]"
# Run any container setup
[ -x /sbin/init-container ] && /sbin/init-container
# General Setup
if [ -x /usr/bin/memcached -a "${MEMCACHED_START}" == "TRUE" ]; then
echo "* Starting MEMCACHED..."
/usr/bin/memcached -d -P /run/memcached/memcached.pid -u memcached
fi
# Laravel Specific
if [ -r artisan -a -e ${php}/.env ]; then
echo "* Laravel Setup..."

1686
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,6 @@
"animate-sass": "^0.8.2",
"axios": "^1.3.4",
"bootstrap": "^5.2.3",
"bootstrap-icons": "^1.11.3",
"jquery": "^3.6.3",
"jquery-ui": "^1.13.2",
"jquery.fancytree": "^2.38.3",

View File

@ -1 +1 @@
v2.2.1-rel
v2.0.0-rel

100
public/css/custom.css vendored
View File

@ -1,100 +1,18 @@
img.jpegphoto {
display:block;
max-width:100px;
height:100px;
}
/** ensure our userpassword has select is next to the password input */
attribute#userpassword .select2-container--bootstrap-5 .select2-selection {
div#userPassword .select2-container--bootstrap-5 .select2-selection {
font-size: inherit;
width: 9em;
border: var(--bs-gray-500) 1px solid;
border: #444054 1px solid;
background-color: #f0f0f0;
}
/* render the structural inside the input box */
attribute#objectclass .input-group-end:not(input.form-control) {
position: absolute;
right: 1em;
top: 0.5em;
z-index: 5;
}
/* select forms that have nothing next to them */
.select-group:first-child .select2-container--bootstrap-5 .select2-selection {
border-radius: 4px !important;
}
.input-group:first-child:not(.select-group) .select2-container--bootstrap-5 .select2-selection {
.input-group:first-child .select2-container--bootstrap-5 .select2-selection {
border-bottom-right-radius: unset;
border-top-right-radius: unset;
}
.select2-container .select2-selection--single .select2-selection__rendered {
font-size: 0.88em;
}
input.form-control.input-group-end {
border-bottom-right-radius: 4px !important;
border-top-right-radius: 4px !important;
}
.custom-tooltip-success {
--bs-tooltip-bg: var(--bs-success);
}
.custom-tooltip-warning {
--bs-tooltip-bg: var(--bs-warning);
--bs-tooltip-color: black;
}
.custom-tooltip-danger {
--bs-tooltip-bg: var(--bs-danger);
}
.custom-tooltip {
--bs-tooltip-bg: var(--bs-gray-900);
}
.tooltip {
font-size: 85%;
}
/*
.custom-tooltip-warning .tooltip-inner {
--bs-tooltip-bg: #ffffff;
--bs-tooltip-color: var(--bs-warning);
--bs-font-size: 85%;
border: 2px solid var(--bs-warning);
border-radius: 6px;
}
.custom-tooltip-warning .tooltip-arrow::before {
--bs-tooltip-bg: var(--bs-warning);
}
.custom-tooltip-danger .tooltip-inner {
--bs-tooltip-bg: #ffffff;
--bs-tooltip-color: var(--bs-danger);
--bs-font-size: 85%;
border: 2px solid var(--bs-danger);
border-radius: 6px;
}
.custom-tooltip-danger .tooltip-arrow::before {
--bs-tooltip-bg: var(--bs-danger);
}
*/
/* hide the site icons when the search is opened */
.search-wrapper.active + .header-menu.nav {
display: none;
}
.page-title-wrapper .page-title-items {
margin-left: auto;
max-width: 50%;
}
.page-title-wrapper .page-title-items .page-title-status .alert {
font-size: 0.80em;
}
/* Square UL items */
ul.square {
list-style-type: square;
}

87
public/css/fixes.css vendored
View File

@ -69,6 +69,10 @@ table.dataTable thead .sorting {
*/
/** Fancy Tree Fixes **/
/*
@todo The unopened lazy branches off the tree are off by 5px. see *-cdl. below
@todo The last node is missing some dots, connecting to the previous node
*/
/* So our tree can be longer than the frame */
.scrollbar-sidebar {
overflow: auto;
@ -113,7 +117,7 @@ ul.fancytree-container ul {
}
.fancytree-node.fancytree-exp-n span.fancytree-expander,
.fancytree-node.fancytree-exp-n span.fancytree-expander:hover { /* node */
margin-top: 3px;
margin-top: 4px;
background-position: 0 -63px;
}
.fancytree-node.fancytree-exp-nl span.fancytree-expander { /* node last */
@ -139,6 +143,11 @@ ul.fancytree-container ul {
opacity: 0;
}
/* Fix ellipsis icon (top right) on small display with the light background */
.closed-sidebar .app-header.header-text-light .app-header__menu .mobile-toggle-header-nav {
background: #343a40;
}
/* Hide tree when collapsed and show it when open */
.sidebar-mobile-open:hover #tree, /* small */
.fixed-sidebar #tree, /* wide */
@ -151,6 +160,12 @@ ul.fancytree-container ul {
}
/** Server icons **/
.closed-sidebar .server-icon {
display: none;
}
.closed-sidebar .app-sidebar:hover .server-icon, .sidebar-mobile-open .server-icon {
display: flex;
}
.font-icon-wrapper {
text-align: center;
border: #e9ecef solid 1px;
@ -166,6 +181,47 @@ ul.fancytree-container ul {
font-size: 1.2rem;
}
/*
.font-icon-wrapper {
text-align: center;
border: $gray-200 solid 1px;
@include border-radius($border-radius);
margin: 0 0 10px;
padding: 5px;
&.font-icon-lg {
float: left;
padding: 10px;
text-align: center;
margin-right: 15px;
min-width: 64px;
i {
font-size: $h1-font-size;
}
}
&:hover {
background: $gray-100;
color: $primary;
p {
color: $gray-600;
}
}
i {
font-size: ($font-size-base * 1.5);
}
p {
color: $gray-500;
font-size: calc($font-size-sm / 1.2);
margin: 5px 0 0;
}
}
*/
/** Ensure our DN menu is at the top **/
.app-page-title .page-title-wrapper {
align-items: start;
@ -241,31 +297,14 @@ select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__
font-weight: 800;
}
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice {
padding: 0.25em 0.45em;
}
/* Remove the shadow outline on an opened box */
.select2-container--bootstrap-5.select2-container--focus .select2-selection, .select2-container--bootstrap-5.select2-container--open .select2-selection {
box-shadow: none;
div#objectClass .input-group-delete {
position: relative;
float: inline-end;
bottom: 30px;
right: 10px;
height: 5px;
}
.input-group-text {
background-color: #fafafa;
}
/* Stop showing a border on our user's drop down menu when open */
.btn-check:checked+.btn, .btn.active, .btn.show, .btn:first-child:active, :not(.btn-check)+.btn:active {
border-color: var(--bs-btn-bg);
}
/* limit selection to inside the modal */
body.modal-open {
user-select: none;
}
/* Fix our search results, implementing a scroll bar */
#search_results ul.typeahead.dropdown-menu {
overflow-y: scroll;
max-height: 300px;
}

33
public/js/custom.js vendored
View File

@ -15,7 +15,7 @@ function getNode(item) {
$.ajax({
url: '/frame',
method: 'POST',
data: { _key: item },
data: { key: item },
dataType: 'html',
beforeSend: function() {
content = $('.main-content')
@ -36,18 +36,16 @@ function getNode(item) {
case 404:
$('.main-content').empty().append(e.responseText);
break;
case 409: // Not in root
case 419: // Session Expired
case 409:
location.replace('/#'+item);
// When the session expires, and we are in the tree, we need to force a reload
if (location.pathname === '/')
location.reload();
break;
case 419:
alert('Session has expired, reloading the page and try again...');
location.reload();
break;
case 500:
case 555: // Missing Method
$('.main-content').empty().append(e.responseText);
break;
default:
alert('Well that didnt work? Code ['+e.status+']');
}
@ -59,21 +57,25 @@ $(document).ready(function() {
if (typeof basedn !== 'undefined') {
sources = basedn;
} else {
sources = { method: 'POST', url: '/ajax/bases' };
sources = { url: 'api/bases' };
}
// Attach the fancytree widget to an existing <div id="tree"> element
// and pass the tree options as an argument to the fancytree() function:
$('#tree').fancytree({
clickFolderMode: 3,
extensions: ['persist'],
extensions: ['glyph','persist'],
autoCollapse: true, // Automatically collapse all siblings, when a node is expanded.
autoScroll: true, // Automatically scroll nodes into visible area.
focusOnSelect: true, // Set focus when node is checked by a mouse click
glyph: {
preset: 'bootstrap3', // @todo look at changing this to awesome5
map: {}
},
persist: {
// Available options with their default:
cookieDelimiter: '~', // character used to join key strings
cookiePrefix: 'pla-<treeId>-', // 'fancytree-<treeId>-' by default
cookiePrefix: undefined, // 'fancytree-<treeId>-' by default
cookie: { // settings passed to jquery.cookie plugin
raw: false,
expires: '',
@ -83,21 +85,20 @@ $(document).ready(function() {
},
expandLazy: true, // true: recursively expand and load lazy nodes
expandOpts: undefined, // optional `opts` argument passed to setExpanded()
fireActivate: false, //
overrideSource: true, // true: cookie takes precedence over `source` data attributes.
store: 'auto', // 'cookie': use cookie, 'local': use localStore, 'session': use sessionStore
types: 'active expanded focus selected' // which status types to store
},
click: function(event,data) {
if (data.targetType === 'title')
if (data.targetType == 'title') {
getNode(data.node.data.item);
}
},
source: sources,
lazyLoad: function(event,data) {
data.result = {
method: 'POST',
url: '/ajax/children',
data: {_key: data.node.data.item,create: true}
url: '/api/children',
data: {key: data.node.data.item,depth: 1}
};
expandChildren(data.tree.rootNode);

23
public/js/template.js vendored
View File

@ -1,23 +0,0 @@
/* JavaScript template engine abstraction layer */
/* Currently implemented for jquery */
// Get a value from an attribute
function get_attribute(attribute,start,end) {
var val = $('#'+attribute).find('input').val();
return ((start !== undefined) && (end !== undefined))
? val.substring(start,end)
: val;
}
// Put a value to an attribute
function put_attribute(attribute,result) {
// Get the value, if the value hasnt changed, then we dont need to do anything
if (get_attribute(attribute) === result)
return;
$('#'+attribute)
.find('input')
.val(result)
.trigger('change');
}

81
public/js/toAscii.js vendored
View File

@ -1,81 +0,0 @@
//
// Purpose of this file is to remap characters as ASCII characters
//
//
var to_ascii_array = new Array();
to_ascii_array['à'] = 'a';
to_ascii_array['á'] = 'a';
to_ascii_array['â'] = 'a';
to_ascii_array['À'] = 'a';
to_ascii_array['ã'] = 'a';
to_ascii_array['Ã¥'] = 'a';
to_ascii_array['À'] = 'A';
to_ascii_array['Á'] = 'A';
to_ascii_array['Ä'] = 'A';
to_ascii_array['Â'] = 'A';
to_ascii_array['Ã'] = 'A';
to_ascii_array['Å'] = 'A';
to_ascii_array['é'] = 'e';
to_ascii_array['Ú'] = 'e';
to_ascii_array['ë'] = 'e';
to_ascii_array['ê'] = 'e';
to_ascii_array['€'] = 'E';
to_ascii_array['ï'] = 'i';
to_ascii_array['î'] = 'i';
to_ascii_array['ì'] = 'i';
to_ascii_array['í'] = 'i';
to_ascii_array['Ï'] = 'I';
to_ascii_array['Î'] = 'I';
to_ascii_array['Ì'] = 'I';
to_ascii_array['Í'] = 'I';
to_ascii_array['ò'] = 'o';
to_ascii_array['ó'] = 'o';
to_ascii_array['ÃŽ'] = 'o';
to_ascii_array['õ'] = 'o';
to_ascii_array['ö'] = 'o';
to_ascii_array['Þ'] = 'o';
to_ascii_array['Ò'] = 'O';
to_ascii_array['Ó'] = 'O';
to_ascii_array['Ô'] = 'O';
to_ascii_array['Õ'] = 'O';
to_ascii_array['Ö'] = 'O';
to_ascii_array['Ø'] = 'O';
to_ascii_array['ù'] = 'u';
to_ascii_array['ú'] = 'u';
to_ascii_array['Ì'] = 'u';
to_ascii_array['û'] = 'u';
to_ascii_array['Ù'] = 'U';
to_ascii_array['Ú'] = 'U';
to_ascii_array['Ü'] = 'U';
to_ascii_array['Û'] = 'U';
to_ascii_array['Ê'] = 'ae';
to_ascii_array['Æ'] = 'AE';
to_ascii_array['Ü'] = 'y';
to_ascii_array['ÿ'] = 'y';
to_ascii_array['ß'] = 'SS';
to_ascii_array['Ç'] = 'C';
to_ascii_array['ç'] = 'c';
to_ascii_array['Ñ'] = 'N';
to_ascii_array['ñ'] = 'n';
to_ascii_array['¢'] = 'c';
to_ascii_array['©'] = '(C)';
to_ascii_array['®'] = '(R)';
to_ascii_array['«'] = '<<';
to_ascii_array['»'] = '>>';
function toAscii(text) {
//var text = field.value;
var output = '';
for (position=0; position < text.length; position++) {
var tmp = text.substring(position,position+1);
if (to_ascii_array[tmp] !== undefined)
tmp = to_ascii_array[tmp];
output += tmp;
}
return output;
}

View File

@ -1,45 +1,6 @@
This directory contains language translation files for PLA. PLA should automatically detect your language based on your
browser configuration, and if the language is not available it will fall back to the language used internally (English).
This directory contains language translation files for PLA.
Language files are named by 2 letter iso language name (suffixed with .json) represent the translations for that
language.
Where a language is spoken in multiple countries, but has local country differences (eg: `en-US` vs `en-GB`,
or `zh-CN` vs `zh-TW`), then the language filename is suffixed with `-` and a two letter country, eg:
Language files named by 2 letter iso language name (suffixed with .json)
represent the translations for that language.
* `en.json` for English (General),
* `en-GB.json` for English (Great Britain),
* `zh-CN.json` for Chinese (China),
* `zh-TW.json` for Chinese (Taiwan), etc
The language file `zz.json` is an example language file, with each translated string prefixed with the letter "Z". Its
used to identify any default language text (in english) that is not in a translated configuration. Text strings enclosed
in `@lang()`, or `__()` functions are translatable to other languages.
If you want to update the language text for your language, then:
* If your language file **exists** (eg: `fr.json` for French), then:
* Identify the missing tags (compare it to `zz.json`),
* Insert the missing tags into the language file (eg: `fr.json` for French) - ensure you keep the file in English
Alphabetical order.
* If your language file **doesnt** exist (eg; `fr.json` for French), then
* Copy the default language file `zz.json` to `fr.json`
* Translate the strings
The structure of the json files is:
```json
{
"Untranslated string1": "Translated string1",
"Untranslated string2": "Translated string2"
}
```
Some important notes:
* `Untranslated string` is the string as it appears in PLA, wrapped in either a `__()` or `@lang()` function, normally and english phrase
* `Translated string` is the translation for your language
* Each translated string must be comma terminated *EXCEPT* the last string
Please submit a pull request with your translations, so that others users can benefit from the translation.
If you find any strings that you are not translatable, or translated incorrectly, please submit a bug report.
eg: en.json

10
resources/lang/dev.json Normal file
View File

@ -0,0 +1,10 @@
{
"Email": "DEV:Email",
"Home": "DEV:Home",
"Password": "DEV:Password",
"Please enter your email": "DEV:Please enter your email",
"Please enter your password": "DEV:Please enter your password",
"Server Info": "DEV:Server Info",
"Server Name": "DEV:Server Name",
"Sign in to <strong>:server</strong>": "DEV:Sign in to <strong>:server</strong>"
}

View File

@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'These credentials do not match our records.',
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
];

View File

@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; Previous',
'next' => 'Next &raquo;',
];

View File

@ -0,0 +1,22 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reset Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
'reset' => 'Your password has been reset!',
'sent' => 'We have emailed your password reset link!',
'throttled' => 'Please wait before retrying.',
'token' => 'This password reset token is invalid.',
'user' => "We can't find a user with that email address.",
];

View File

@ -0,0 +1,151 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'accepted' => 'The :attribute must be accepted.',
'active_url' => 'The :attribute is not a valid URL.',
'after' => 'The :attribute must be a date after :date.',
'after_or_equal' => 'The :attribute must be a date after or equal to :date.',
'alpha' => 'The :attribute may only contain letters.',
'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.',
'alpha_num' => 'The :attribute may only contain letters and numbers.',
'array' => 'The :attribute must be an array.',
'before' => 'The :attribute must be a date before :date.',
'before_or_equal' => 'The :attribute must be a date before or equal to :date.',
'between' => [
'numeric' => 'The :attribute must be between :min and :max.',
'file' => 'The :attribute must be between :min and :max kilobytes.',
'string' => 'The :attribute must be between :min and :max characters.',
'array' => 'The :attribute must have between :min and :max items.',
],
'boolean' => 'The :attribute field must be true or false.',
'confirmed' => 'The :attribute confirmation does not match.',
'date' => 'The :attribute is not a valid date.',
'date_equals' => 'The :attribute must be a date equal to :date.',
'date_format' => 'The :attribute does not match the format :format.',
'different' => 'The :attribute and :other must be different.',
'digits' => 'The :attribute must be :digits digits.',
'digits_between' => 'The :attribute must be between :min and :max digits.',
'dimensions' => 'The :attribute has invalid image dimensions.',
'distinct' => 'The :attribute field has a duplicate value.',
'email' => 'The :attribute must be a valid email address.',
'ends_with' => 'The :attribute must end with one of the following: :values.',
'exists' => 'The selected :attribute is invalid.',
'file' => 'The :attribute must be a file.',
'filled' => 'The :attribute field must have a value.',
'gt' => [
'numeric' => 'The :attribute must be greater than :value.',
'file' => 'The :attribute must be greater than :value kilobytes.',
'string' => 'The :attribute must be greater than :value characters.',
'array' => 'The :attribute must have more than :value items.',
],
'gte' => [
'numeric' => 'The :attribute must be greater than or equal :value.',
'file' => 'The :attribute must be greater than or equal :value kilobytes.',
'string' => 'The :attribute must be greater than or equal :value characters.',
'array' => 'The :attribute must have :value items or more.',
],
'image' => 'The :attribute must be an image.',
'in' => 'The selected :attribute is invalid.',
'in_array' => 'The :attribute field does not exist in :other.',
'integer' => 'The :attribute must be an integer.',
'ip' => 'The :attribute must be a valid IP address.',
'ipv4' => 'The :attribute must be a valid IPv4 address.',
'ipv6' => 'The :attribute must be a valid IPv6 address.',
'json' => 'The :attribute must be a valid JSON string.',
'lt' => [
'numeric' => 'The :attribute must be less than :value.',
'file' => 'The :attribute must be less than :value kilobytes.',
'string' => 'The :attribute must be less than :value characters.',
'array' => 'The :attribute must have less than :value items.',
],
'lte' => [
'numeric' => 'The :attribute must be less than or equal :value.',
'file' => 'The :attribute must be less than or equal :value kilobytes.',
'string' => 'The :attribute must be less than or equal :value characters.',
'array' => 'The :attribute must not have more than :value items.',
],
'max' => [
'numeric' => 'The :attribute may not be greater than :max.',
'file' => 'The :attribute may not be greater than :max kilobytes.',
'string' => 'The :attribute may not be greater than :max characters.',
'array' => 'The :attribute may not have more than :max items.',
],
'mimes' => 'The :attribute must be a file of type: :values.',
'mimetypes' => 'The :attribute must be a file of type: :values.',
'min' => [
'numeric' => 'The :attribute must be at least :min.',
'file' => 'The :attribute must be at least :min kilobytes.',
'string' => 'The :attribute must be at least :min characters.',
'array' => 'The :attribute must have at least :min items.',
],
'not_in' => 'The selected :attribute is invalid.',
'not_regex' => 'The :attribute format is invalid.',
'numeric' => 'The :attribute must be a number.',
'password' => 'The password is incorrect.',
'present' => 'The :attribute field must be present.',
'regex' => 'The :attribute format is invalid.',
'required' => 'The :attribute field is required.',
'required_if' => 'The :attribute field is required when :other is :value.',
'required_unless' => 'The :attribute field is required unless :other is in :values.',
'required_with' => 'The :attribute field is required when :values is present.',
'required_with_all' => 'The :attribute field is required when :values are present.',
'required_without' => 'The :attribute field is required when :values is not present.',
'required_without_all' => 'The :attribute field is required when none of :values are present.',
'same' => 'The :attribute and :other must match.',
'size' => [
'numeric' => 'The :attribute must be :size.',
'file' => 'The :attribute must be :size kilobytes.',
'string' => 'The :attribute must be :size characters.',
'array' => 'The :attribute must contain :size items.',
],
'starts_with' => 'The :attribute must start with one of the following: :values.',
'string' => 'The :attribute must be a string.',
'timezone' => 'The :attribute must be a valid zone.',
'unique' => 'The :attribute has already been taken.',
'uploaded' => 'The :attribute failed to upload.',
'url' => 'The :attribute format is invalid.',
'uuid' => 'The :attribute must be a valid UUID.',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap our attribute placeholder
| with something more reader friendly such as "E-Mail Address" instead
| of "email". This simply helps us make our message more expressive.
|
*/
'attributes' => [],
];

View File

@ -1,143 +0,0 @@
{
"(no description)": "Z(no description)",
"(none)": "Z(none)",
"(not applicable)": "Z(not applicable)",
"(not specified)": "Z(not specified)",
"(unknown syntax)": "Z(unknown syntax)",
"Add New Attribute": "ZAdd New Attribute",
"Add Objectclass": "ZAdd Objectclass",
"Add Value": "ZAdd Value",
"Aliases": "ZAliases",
"Attributes": "ZAttributes",
"attributes(s)": "Zattributes(s)",
"Attribute Types": "ZAttribute Types",
"Authority Key Identifier": "ZAuthority Key Identifier",
"Certificate Subject": "ZCertificate Subject",
"Check": "ZCheck",
"Check Password": "ZCheck Password",
"Close": "ZClose",
"Collective": "ZCollective",
"Copy\/Move": "ZCopy\/Move",
"Create Child Entry": "ZCreate Child Entry",
"Created": "ZCreated",
"Create Entry": "ZCreate Entry",
"Create New Entry": "ZCreate New Entry",
"Create new LDAP item here": "ZCreate new LDAP item here",
"Delete": "ZDelete",
"Deleted": "ZDeleted",
"Delete Entry": "ZDelete Entry",
"Deleting this DN will permanently delete it from your LDAP server.": "ZDeleting this DN will permanently delete it from your LDAP server.",
"Description": "ZDescription",
"DN": "ZDN",
"Download": "ZDownload",
"Do you want to make the following changes?": "ZDo you want to make the following changes?",
"dynamic": "Zdynamic",
"Edit Entry": "ZEdit Entry",
"Entry": "ZEntry",
"Entry updated": "ZEntry updated",
"Equality": "ZEquality",
"Error": "ZError",
"Expired": "ZExpired",
"Expires": "ZExpires",
"Export": "ZExport",
"Exported by": "ZExported by",
"Force as MAY by config": "ZForce as MAY by config",
"Generated by": "ZGenerated by",
"Home": "ZHome",
"Ignoring blank value": "ZIgnoring blank value",
"Import Result": "ZImport Result",
"Inherits from": "ZInherits from",
"Internal": "ZInternal",
"Invalid Password": "ZInvalid Password",
"KRB_DISALLOW_ALL_TIX": "ZKRB_DISALLOW_ALL_TIX",
"KRB_DISALLOW_DUP_SKEY": "ZKRB_DISALLOW_DUP_SKEY",
"KRB_DISALLOW_FORWARDABLE": "ZKRB_DISALLOW_FORWARDABLE",
"KRB_DISALLOW_POSTDATED": "ZKRB_DISALLOW_POSTDATED",
"KRB_DISALLOW_PROXIABLE": "ZKRB_DISALLOW_PROXIABLE",
"KRB_DISALLOW_RENEWABLE": "ZKRB_DISALLOW_RENEWABLE",
"KRB_DISALLOW_SVR": "ZKRB_DISALLOW_SVR",
"KRB_DISALLOW_TGT_BASED": "ZKRB_DISALLOW_TGT_BASED",
"KRB_PWCHANGE_SERVICE": "ZKRB_PWCHANGE_SERVICE",
"KRB_REQUIRES_HW_AUTH": "ZKRB_REQUIRES_HW_AUTH",
"KRB_REQUIRES_PRE_AUTH": "ZKRB_REQUIRES_PRE_AUTH",
"KRB_REQUIRES_PWCHANGE": "ZKRB_REQUIRES_PWCHANGE",
"LDAP Authentication Error": "ZLDAP Authentication Error",
"LDAP Entry": "ZLDAP Entry",
"LDAP Server Error Code": "ZLDAP Server Error Code",
"LDAP Server Unavailable": "ZLDAP Server Unavailable",
"LDIF": "ZLDIF",
"LDIF Import": "ZLDIF Import",
"LDIF Import Result": "ZLDIF Import Result",
"Line": "ZLine",
"locale": "ZZ",
"Matching Rules": "ZMatching Rules",
"Maximum file size": "ZMaximum file size",
"Maximum Length": "ZMaximum Length",
"NEW": "ZNEW",
"New Value": "ZNew Value",
"Next": "ZNext",
"No attributes changed": "ZNo attributes changed",
"No description available, can you help with one?": "ZNo description available, can you help with one?",
"No Server Name Yet": "ZNo Server Name Yet",
"NOT DEFINED": "ZNOT DEFINED",
"Not Implemented": "ZNot Implemented",
"Object Classes": "ZObject Classes",
"Object Identifier": "ZObject Identifier",
"Obsolete": "ZObsolete",
"Optional Attributes": "ZOptional Attributes",
"Ordering": "ZOrdering",
"Or upload LDIF file": "ZOr upload LDIF file",
"Parent to": "ZParent to",
"Paste in your LDIF here": "ZPaste in your LDIF here",
"Possible Causes": "ZPossible Causes",
"Process": "ZProcess",
"rdn": "Zrdn",
"RDN is required": "ZRDN is required",
"RDN is required.": "ZRDN is required.",
"RDN value is required.": "ZRDN value is required.",
"Rename": "ZRename",
"Replace": "ZReplace",
"required": "Zrequired",
"Required Attribute by ObjectClass(es)": "ZRequired Attribute by ObjectClass(es)",
"Required Attributes": "ZRequired Attributes",
"Required by ObjectClasses": "ZRequired by ObjectClasses",
"Reset": "ZReset",
"Result": "ZResult",
"Schema Information": "ZSchema Information",
"Search Filter": "ZSearch Filter",
"Search Scope": "ZSearch Scope",
"Select a Structural ObjectClass...": "ZSelect a Structural ObjectClass...",
"Select attribute...": "ZSelect attribute...",
"Select from": "ZSelect from",
"Serial Number": "ZSerial Number",
"Server": "ZServer",
"Server Info": "ZServer Info",
"Single Valued": "ZSingle Valued",
"Step": "ZStep",
"structural": "Zstructural",
"Subject Key Identifier": "ZSubject Key Identifier",
"Substring Rule": "ZSubstring Rule",
"Syntax": "ZSyntax",
"Syntaxes": "ZSyntaxes",
"These are dynamic values present as a result of another attribute": "ZThese are dynamic values present as a result of another attribute",
"This attribute is required for the RDN": "ZThis attribute is required for the RDN",
"To Server": "ZTo Server",
"Total Entries": "ZTotal Entries",
"Type": "ZType",
"Unknown": "ZUnknown",
"Untrapped Error": "ZUntrapped Error",
"Update": "ZUpdate",
"Updated": "ZUpdated",
"Upload JpegPhoto": "ZUpload JpegPhoto",
"Usage": "ZUsage",
"Used by Attributes": "ZUsed by Attributes",
"Used by ObjectClasses": "ZUsed by ObjectClasses",
"User Modification": "ZUser Modification",
"Validation Errors": "ZValidation Errors",
"Version": "ZVersion",
"WARNING": "ZWARNING",
"Your DNS server cannot resolve that hostname": "ZYour DNS server cannot resolve that hostname",
"Your LDAP server hostname is incorrect": "ZYour LDAP server hostname is incorrect",
"Your LDAP server is not connectable": "ZYour LDAP server is not connectable",
"Your Resolver is not pointing to your DNS server": "ZYour Resolver is not pointing to your DNS server"
}

View File

@ -7,6 +7,3 @@
// Select2
@import "select2/dist/css/select2";
@import "select2-bootstrap-5-theme/dist/select2-bootstrap-5-theme";
// Bootstrap icons
@import "bootstrap-icons"

View File

@ -7,6 +7,7 @@ import 'metismenu';
// Stylesheets
// import './assets/base.scss';
// import '../../themes/architect/src/base.scss';
$(document).ready(() => {
@ -72,14 +73,11 @@ $(document).ready(() => {
var resizeClass = function () {
var win = document.body.clientWidth;
if (win < 768) {
$('.app-container').addClass("closed-sidebar closed-sidebar-mobile");
$('.app-header').addClass("header-text-light bg-light").removeClass("bg-dark header-text-dark");
} else if (win < 1250) {
if (win < 1250) {
$('.app-container').addClass('closed-sidebar-mobile closed-sidebar');
$('.app-header').removeClass("header-text-light bg-dark").addClass("bg-light header-text-dark");
$('.app-header').removeClass("heard-text-light bg-dark").addClass("bg-light header-text-dark");
} else {
$('.app-header').addClass("header-text-light bg-dark").removeClass("bg-light header-text-dark");
$('.app-header').addClass("heard-text-light bg-dark").removeClass("bg-light header-text-dark");
$('.app-container').removeClass('closed-sidebar-mobile closed-sidebar');
}
};

View File

@ -1,9 +1,9 @@
/*!
=========================================================
* ArchitectUI HTML Theme Dashboard - v4.1.0
* ArchitectUI HTML Theme Dashboard - v2.0.0
=========================================================
* Product Page: https://dashboardpack.com
* Copyright 2025 DashboardPack (https://dashboardpack.com)
* Copyright 2021 DashboardPack (https://dashboardpack.com)
* Licensed under MIT (https://github.com/DashboardPack/architectui-html-theme-free/blob/master/LICENSE)
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
@ -21,8 +21,6 @@
// 3. Include remainder of required Bootstrap stylesheets
@import "components/bootstrap5/variables";
@import "components/bootstrap5/variables-dark";
@import "components/bootstrap5/maps";
@import "components/bootstrap5/mixins";
@import "components/bootstrap5/utilities";
@ -58,10 +56,9 @@
// @import "components/bootstrap5/carousel";
// @import "components/bootstrap5/spinners";
// @import "components/bootstrap5/offcanvas";
// @import "components/bootstrap5/placeholders";
// @import "components/bootstrap5/jumbotron";
@import "components/bootstrap5/helpers";
@import "components/bootstrap5/utilities/api";
// LAYOUT
@import "layout/layout";

View File

@ -3,25 +3,11 @@
//
.alert {
// scss-docs-start alert-css-vars
--#{$prefix}alert-bg: transparent;
--#{$prefix}alert-padding-x: #{$alert-padding-x};
--#{$prefix}alert-padding-y: #{$alert-padding-y};
--#{$prefix}alert-margin-bottom: #{$alert-margin-bottom};
--#{$prefix}alert-color: inherit;
--#{$prefix}alert-border-color: transparent;
--#{$prefix}alert-border: #{$alert-border-width} solid var(--#{$prefix}alert-border-color);
--#{$prefix}alert-border-radius: #{$alert-border-radius};
--#{$prefix}alert-link-color: inherit;
// scss-docs-end alert-css-vars
position: relative;
padding: var(--#{$prefix}alert-padding-y) var(--#{$prefix}alert-padding-x);
margin-bottom: var(--#{$prefix}alert-margin-bottom);
color: var(--#{$prefix}alert-color);
background-color: var(--#{$prefix}alert-bg);
border: var(--#{$prefix}alert-border);
@include border-radius(var(--#{$prefix}alert-border-radius));
padding: $alert-padding-y $alert-padding-x;
margin-bottom: $alert-margin-bottom;
border: $alert-border-width solid transparent;
@include border-radius($alert-border-radius);
}
// Headings for larger alerts
@ -33,7 +19,6 @@
// Provide class for links that match alerts
.alert-link {
font-weight: $alert-link-font-weight;
color: var(--#{$prefix}alert-link-color);
}
@ -56,13 +41,17 @@
// scss-docs-start alert-modifiers
// Generate contextual modifier classes for colorizing the alert
@each $state in map-keys($theme-colors) {
// Generate contextual modifier classes for colorizing the alert.
@each $state, $value in $theme-colors {
$alert-background: shift-color($value, $alert-bg-scale);
$alert-border: shift-color($value, $alert-border-scale);
$alert-color: shift-color($value, $alert-color-scale);
@if (contrast-ratio($alert-background, $alert-color) < $min-contrast-ratio) {
$alert-color: mix($value, color-contrast($alert-background), abs($alert-color-scale));
}
.alert-#{$state} {
--#{$prefix}alert-color: var(--#{$prefix}#{$state}-text-emphasis);
--#{$prefix}alert-bg: var(--#{$prefix}#{$state}-bg-subtle);
--#{$prefix}alert-border-color: var(--#{$prefix}#{$state}-border-subtle);
--#{$prefix}alert-link-color: var(--#{$prefix}#{$state}-text-emphasis);
@include alert-variant($alert-background, $alert-border, $alert-color);
}
}
// scss-docs-end alert-modifiers

View File

@ -4,25 +4,16 @@
// `background-color`.
.badge {
// scss-docs-start badge-css-vars
--#{$prefix}badge-padding-x: #{$badge-padding-x};
--#{$prefix}badge-padding-y: #{$badge-padding-y};
@include rfs($badge-font-size, --#{$prefix}badge-font-size);
--#{$prefix}badge-font-weight: #{$badge-font-weight};
--#{$prefix}badge-color: #{$badge-color};
--#{$prefix}badge-border-radius: #{$badge-border-radius};
// scss-docs-end badge-css-vars
display: inline-block;
padding: var(--#{$prefix}badge-padding-y) var(--#{$prefix}badge-padding-x);
@include font-size(var(--#{$prefix}badge-font-size));
font-weight: var(--#{$prefix}badge-font-weight);
padding: $badge-padding-y $badge-padding-x;
@include font-size($badge-font-size);
font-weight: $badge-font-weight;
line-height: 1;
color: var(--#{$prefix}badge-color);
color: $badge-color;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
@include border-radius(var(--#{$prefix}badge-border-radius));
@include border-radius($badge-border-radius);
@include gradient-bg();
// Empty badges collapse automatically

View File

@ -1,40 +1,28 @@
.breadcrumb {
// scss-docs-start breadcrumb-css-vars
--#{$prefix}breadcrumb-padding-x: #{$breadcrumb-padding-x};
--#{$prefix}breadcrumb-padding-y: #{$breadcrumb-padding-y};
--#{$prefix}breadcrumb-margin-bottom: #{$breadcrumb-margin-bottom};
@include rfs($breadcrumb-font-size, --#{$prefix}breadcrumb-font-size);
--#{$prefix}breadcrumb-bg: #{$breadcrumb-bg};
--#{$prefix}breadcrumb-border-radius: #{$breadcrumb-border-radius};
--#{$prefix}breadcrumb-divider-color: #{$breadcrumb-divider-color};
--#{$prefix}breadcrumb-item-padding-x: #{$breadcrumb-item-padding-x};
--#{$prefix}breadcrumb-item-active-color: #{$breadcrumb-active-color};
// scss-docs-end breadcrumb-css-vars
display: flex;
flex-wrap: wrap;
padding: var(--#{$prefix}breadcrumb-padding-y) var(--#{$prefix}breadcrumb-padding-x);
margin-bottom: var(--#{$prefix}breadcrumb-margin-bottom);
@include font-size(var(--#{$prefix}breadcrumb-font-size));
padding: $breadcrumb-padding-y $breadcrumb-padding-x;
margin-bottom: $breadcrumb-margin-bottom;
@include font-size($breadcrumb-font-size);
list-style: none;
background-color: var(--#{$prefix}breadcrumb-bg);
@include border-radius(var(--#{$prefix}breadcrumb-border-radius));
background-color: $breadcrumb-bg;
@include border-radius($breadcrumb-border-radius);
}
.breadcrumb-item {
// The separator between breadcrumbs (by default, a forward-slash: "/")
+ .breadcrumb-item {
padding-left: var(--#{$prefix}breadcrumb-item-padding-x);
padding-left: $breadcrumb-item-padding-x;
&::before {
float: left; // Suppress inline spacings and underlining of the separator
padding-right: var(--#{$prefix}breadcrumb-item-padding-x);
color: var(--#{$prefix}breadcrumb-divider-color);
content: var(--#{$prefix}breadcrumb-divider, escape-svg($breadcrumb-divider)) #{"/* rtl:"} var(--#{$prefix}breadcrumb-divider, escape-svg($breadcrumb-divider-flipped)) #{"*/"};
padding-right: $breadcrumb-item-padding-x;
color: $breadcrumb-divider-color;
content: var(--#{$variable-prefix}breadcrumb-divider, escape-svg($breadcrumb-divider)) #{"/* rtl:"} var(--#{$variable-prefix}breadcrumb-divider, escape-svg($breadcrumb-divider-flipped)) #{"*/"};
}
}
&.active {
color: var(--#{$prefix}breadcrumb-item-active-color);
color: $breadcrumb-active-color;
}
}

View File

@ -34,17 +34,14 @@
}
.btn-group {
@include border-radius($btn-border-radius);
// Prevent double borders when buttons are next to each other
> :not(.btn-check:first-child) + .btn,
> .btn:not(:first-child),
> .btn-group:not(:first-child) {
margin-left: calc(#{$btn-border-width} * -1); // stylelint-disable-line function-disallowed-list
margin-left: -$btn-border-width;
}
// Reset rounded corners
> .btn:not(:last-child):not(.dropdown-toggle),
> .btn.dropdown-toggle-split:first-child,
> .btn-group:not(:last-child) > .btn {
@include border-end-radius(0);
}
@ -126,7 +123,7 @@
> .btn:not(:first-child),
> .btn-group:not(:first-child) {
margin-top: calc(#{$btn-border-width} * -1); // stylelint-disable-line function-disallowed-list
margin-top: -$btn-border-width;
}
// Reset rounded corners

Some files were not shown because too many files have changed in this diff Show More