Compare commits
179 Commits
Author | SHA1 | Date | |
---|---|---|---|
7346a3daf5 | |||
305ef0f5a3 | |||
f1316d698d | |||
339ba7258a | |||
883ac5d90f | |||
46277146c5 | |||
06747064d4 | |||
2c91298b41 | |||
9798863e34 | |||
4494154879 | |||
b22c9505bc | |||
29a659ff69 | |||
2348da36c4 | |||
6f58f5db36 | |||
553368c7b9 | |||
c8d1122ff6 | |||
2320445dfb | |||
6d2c9d1354 | |||
6f20d426ad | |||
7b1b4f4e50 | |||
543250e1fb | |||
3bf97fc0d1 | |||
3ad4c446ea | |||
ee3cb395c2 | |||
29c39e618f | |||
647cee9858 | |||
54c0df2597 | |||
67d65b3a98 | |||
9547b5fc5a | |||
f6b7bff605 | |||
e8aaa17122 | |||
ee7762d69b | |||
fac560750e | |||
d3aa73e468 | |||
2ddeff8ed3 | |||
b6bce380dd | |||
8fd2a43ee2 | |||
96afbd8316 | |||
5ce3a63878 | |||
ac8e79ab99 | |||
d0c02b91c0 | |||
2a691c147e | |||
781c87cb83 | |||
98a0b87afe | |||
88db4ccc99 | |||
6059bc1e45 | |||
acf19cdc5b | |||
56fcd729e7 | |||
d61f6168a4 | |||
f2eaed247a | |||
31e3c75bc9 | |||
9f0290bd40 | |||
820f398c2c | |||
8602c2b17f | |||
33d96940e6 | |||
06b7c204b0 | |||
7854cbdabd | |||
32514c9ab1 | |||
db600a28d3 | |||
b08de519d4 | |||
6599bb7f4f | |||
d623f3c26d | |||
bd40ab0e84 | |||
3fcb8707d9 | |||
c6e1640752 | |||
917a3c1a0d | |||
148d19bbce | |||
6c501cc29d | |||
2ce0ed8974 | |||
9a6d80986a | |||
fa989b8f10 | |||
4e991db8b1 | |||
181971acc4 | |||
3493504720 | |||
54f27d3d16 | |||
3c0eb876e4 | |||
6d55b52cd4 | |||
16a1f85a79 | |||
f8d7819153 | |||
75dbb37d8b | |||
309fe83c98 | |||
ffb98631a6 | |||
be69e22867 | |||
21c88048e3 | |||
471ccfd88e | |||
5d23cbf0cc | |||
b9ae269895 | |||
ae782577e7 | |||
84f82aaf59 | |||
10eca55026 | |||
64cc21d819 | |||
3d511f3fae | |||
bab5a2626d | |||
6954b09089 | |||
a336e58b7a | |||
53880121b6 | |||
ea46cf36d0 | |||
36f8f57b77 | |||
3604f1498c | |||
808934ebfe | |||
21a690c6dd | |||
0083e9158b | |||
f4cc559931 | |||
3de46ac28e | |||
8d4dccd9e9 | |||
ccff36361f | |||
b7ca768cc6 | |||
a61f5e9b97 | |||
d845d87a6e | |||
b501dfe824 | |||
3fad9770a3 | |||
b1d153aa9f | |||
8b0af505a1 | |||
f0eaff7d42 | |||
352bbe2b75 | |||
0fe4894192 | |||
a7be4e00b4 | |||
2abc321eca | |||
6b2fb8dee4 | |||
66537dcec8 | |||
1bf8830887 | |||
c4d28c8a23 | |||
29c460fd4b | |||
3196b10aed | |||
f41b484dc4 | |||
855d7ae75c | |||
ffa8cdc826 | |||
8f39603f9f | |||
bcea6de791 | |||
28f4869628 | |||
cf535286c5 | |||
633513d3e9 | |||
705bfb2d64 | |||
3a3bf2addb | |||
5bb573100b | |||
a57ee78492 | |||
eab4f0427c | |||
fd2c5d1286 | |||
b35b44b2b8 | |||
ce66dcb2b5 | |||
56a91f853c | |||
81e0e58650 | |||
1470170928 | |||
85c7132b30 | |||
7e050954c3 | |||
16880cd0e2 | |||
696d87d190 | |||
87bae89ea3 | |||
1abc2cc6e1 | |||
1abab9db94 | |||
410daf649e | |||
9666841c3c | |||
9b33a20cc4 | |||
649749f9c1 | |||
5d3b8609bb | |||
93640959db | |||
f667250b2c | |||
4a84c25ac7 | |||
8ab5b4f35c | |||
de2d139288 | |||
d326d3c308 | |||
d3fc9c135f | |||
eb6e0b8d43 | |||
b01f7d5baf | |||
1ddb58ebbb | |||
b260912e01 | |||
7debd9ff2b | |||
49fd9b419a | |||
3161fe4fcb | |||
add3f85812 | |||
853bd92340 | |||
a56b2d8002 | |||
af7ca851d5 | |||
b34dad8836 | |||
ef2ea5e266 | |||
91b5b53137 | |||
d4c916923d | |||
e94a7d58e1 | |||
15d5bf605a |
@ -2,7 +2,6 @@ APP_NAME=Laravel
|
||||
APP_ENV=production
|
||||
APP_KEY=
|
||||
APP_DEBUG=false
|
||||
APP_URL=http://localhost
|
||||
|
||||
LOG_CHANNEL=daily
|
||||
|
||||
@ -12,7 +11,7 @@ SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
LDAP_HOST=
|
||||
LDAP_BASE_DN=
|
||||
LDAP_USERNAME=
|
||||
LDAP_PASSWORD=
|
||||
LDAP_CACHE=true
|
||||
LDAP_CACHE=false
|
||||
LDAP_ALERT_ROOTDN=true
|
||||
|
45
.env.testing
45
.env.testing
@ -1,51 +1,16 @@
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=dev
|
||||
APP_KEY=base64:KvIecx8zoy6RjcbJM8s98ZKs9IDGUHFVqBRn3Awfmso=
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_CHANNEL=stderr
|
||||
|
||||
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
|
||||
CACHE_DRIVER=array
|
||||
QUEUE_CONNECTION=sync
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
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_HOST=openldap
|
||||
LDAP_USERNAME="cn=admin,dc=Test"
|
||||
LDAP_PASSWORD="test"
|
||||
LDAP_CACHE=false
|
||||
|
@ -3,7 +3,7 @@ run-name: ${{ gitea.actor }} Building Docker Image 🐳
|
||||
on: [push]
|
||||
env:
|
||||
DOCKER_HOST: tcp://127.0.0.1:2375
|
||||
ASSETS: 509b1a1
|
||||
ASSETS: 2d732e5
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -10,6 +10,9 @@ 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 '...'
|
||||
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
55
README.md
55
README.md
@ -1,4 +1,10 @@
|
||||
# phpLDAPadmin
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
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.
|
||||
@ -27,38 +33,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.
|
||||
|
||||
## Version 2 Progress
|
||||
## 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.
|
||||
|
||||
The update to v2 is progressing well - here is a list of work to do and done:
|
||||
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.
|
||||
|
||||
- [X] Creating new LDAP entries
|
||||
- [X] Delete existing LDAP entries
|
||||
- [X] Updating existing LDAP Entries
|
||||
- [X] Password attributes
|
||||
- [X] Support different password hash options
|
||||
- [X] Validate password is correct
|
||||
## Outstanding items
|
||||
Compare to v1.x, there are a couple of outstanding items to address
|
||||
|
||||
Entry Editing:
|
||||
- [ ] JpegPhoto Create/Delete
|
||||
- [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
|
||||
- [ ] Searching
|
||||
- [ ] Is there something missing?
|
||||
- [ ] Binary attribute upload
|
||||
- [ ] If removing an objectClass, remove all attributes that only that objectclass provided
|
||||
- [ ] Move an entry
|
||||
- [ ] Group membership selection
|
||||
- [ ] Attribute tag creation
|
||||
|
||||
Support is known for these LDAP servers:
|
||||
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:
|
||||
- [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
|
||||
|
@ -7,41 +7,37 @@ 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, \Iterator
|
||||
class Attribute implements \Countable, \ArrayAccess
|
||||
{
|
||||
// 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 = FALSE;
|
||||
|
||||
// Is this attribute the RDN?
|
||||
protected bool $is_rdn = FALSE;
|
||||
protected ?bool $_is_internal = NULL;
|
||||
protected(set) bool $no_attr_tags = FALSE;
|
||||
|
||||
// MIN/MAX number of values
|
||||
protected int $min_values_count = 0;
|
||||
protected int $max_values_count = 0;
|
||||
protected(set) int $min_values_count = 0;
|
||||
protected(set) int $max_values_count = 0;
|
||||
|
||||
// RFC3866 Language Tags
|
||||
protected Collection $lang_tags;
|
||||
// The schema's representation of this attribute
|
||||
protected(set) ?AttributeType $schema;
|
||||
|
||||
// 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
|
||||
protected Collection $oldValues;
|
||||
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';
|
||||
|
||||
/*
|
||||
# Has the attribute been modified
|
||||
@ -94,16 +90,36 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
|
||||
protected $postvalue = array();
|
||||
*/
|
||||
|
||||
public function __construct(string $name,array $values)
|
||||
/**
|
||||
* 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=[])
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->values = collect($values);
|
||||
$this->lang_tags = collect();
|
||||
$this->oldValues = collect($values);
|
||||
$this->dn = $dn;
|
||||
$this->_values = collect($values);
|
||||
$this->_values_old = collect($values);
|
||||
|
||||
$this->schema = (new Server)
|
||||
$this->schema = config('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))
|
||||
@ -119,35 +135,49 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
|
||||
*/
|
||||
}
|
||||
|
||||
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' => isset($this->{$key}) && $this->{$key},
|
||||
// Is this attribute the RDN
|
||||
'is_rdn' => $this->is_rdn,
|
||||
'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(),
|
||||
// We prefer the name as per the schema if it exists
|
||||
'name' => $this->schema ? $this->schema->{$key} : $this->{$key},
|
||||
'name' => $this->schema->{$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),
|
||||
};
|
||||
@ -156,11 +186,16 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
|
||||
public function __set(string $key,mixed $values): void
|
||||
{
|
||||
switch ($key) {
|
||||
case 'value':
|
||||
$this->values = collect($values);
|
||||
case 'values':
|
||||
$this->_values = $values;
|
||||
break;
|
||||
|
||||
case 'values_old':
|
||||
$this->_values_old = $values;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \Exception('Unknown key:'.$key);
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,49 +204,27 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
/* INTERFACE */
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return $this->values->count();
|
||||
return $this->_values
|
||||
->dot()
|
||||
->count();
|
||||
}
|
||||
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
return ! is_null($this->values->has($offset));
|
||||
return $this->_values
|
||||
->dot()
|
||||
->has($offset);
|
||||
}
|
||||
|
||||
public function offsetGet(mixed $offset): mixed
|
||||
{
|
||||
return $this->values->get($offset);
|
||||
return $this->_values
|
||||
->dot()
|
||||
->get($offset);
|
||||
}
|
||||
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
@ -224,33 +237,48 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
|
||||
// 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 array
|
||||
* @return Collection
|
||||
*/
|
||||
public function hints(): array
|
||||
public function hints(): Collection
|
||||
{
|
||||
$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 attribute name is an alias for the schema attribute name
|
||||
// @todo
|
||||
if ($this->required()->count())
|
||||
$result->put(__('required'),sprintf('%s: %s',__('Required Attribute by ObjectClass(es)'),$this->required()->join(', ')));
|
||||
|
||||
// objectClasses requiring this attribute
|
||||
// @todo limit this to this DNs objectclasses
|
||||
// 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(',')));
|
||||
// If this attribute is a dynamic attribute
|
||||
if ($this->isDynamic())
|
||||
$result->put(__('dynamic'),__('These are dynamic values present as a result of another attribute'));
|
||||
|
||||
// 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();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -260,13 +288,38 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
|
||||
*/
|
||||
public function isDirty(): bool
|
||||
{
|
||||
return ($this->oldValues->count() !== $this->values->count())
|
||||
|| ($this->values->diff($this->oldValues)->count() !== 0);
|
||||
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);
|
||||
}
|
||||
|
||||
public function oldValues(array $array): void
|
||||
/**
|
||||
* Are these values as a result of a dynamic attribute
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isDynamic(): bool
|
||||
{
|
||||
$this->oldValues = collect($array);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -275,41 +328,98 @@ class Attribute implements \Countable, \ArrayAccess, \Iterator
|
||||
* @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): View
|
||||
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
|
||||
{
|
||||
return view('components.attribute')
|
||||
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
|
||||
->with('o',$this)
|
||||
->with('edit',$edit)
|
||||
->with('old',$old)
|
||||
->with('new',$new);
|
||||
}
|
||||
|
||||
public function render_item_old(int $key): ?string
|
||||
{
|
||||
return Arr::get($this->old_values,$key);
|
||||
}
|
||||
|
||||
public function render_item_new(int $key): ?string
|
||||
{
|
||||
return Arr::get($this->values,$key);
|
||||
->with('new',$new)
|
||||
->with('template',$template)
|
||||
->with('updated',$updated);
|
||||
}
|
||||
|
||||
/**
|
||||
* If this attribute has RFC3866 Language Tags, this will enable those values to be captured
|
||||
* Return the value of the original old values
|
||||
*
|
||||
* @param string $tag
|
||||
* @param array $value
|
||||
* @return void
|
||||
* @param string $dotkey
|
||||
* @return string|null
|
||||
*/
|
||||
public function setLangTag(string $tag,array $value): void
|
||||
public function render_item_old(string $dotkey): ?string
|
||||
{
|
||||
$this->lang_tags->put($tag,$value);
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
public function setRDN(): void
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
$this->is_rdn = TRUE;
|
||||
return Arr::get($this->values->dot(),$dotkey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @param string $tag
|
||||
* @return Collection
|
||||
*/
|
||||
public function tagValues(string $tag=Entry::TAG_NOTAG): Collection
|
||||
{
|
||||
return collect($this->_values
|
||||
->filter(fn($item,$key)=>($key===$tag))
|
||||
->get($tag,[]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
return collect($this->_values_old
|
||||
->filter(fn($item,$key)=>($key===$tag))
|
||||
->get($tag,[]));
|
||||
}
|
||||
}
|
@ -7,6 +7,6 @@ use App\Classes\LDAP\Attribute;
|
||||
/**
|
||||
* Represents an attribute whose values are binary
|
||||
*/
|
||||
class Binary extends Attribute
|
||||
abstract class Binary extends Attribute
|
||||
{
|
||||
}
|
@ -5,6 +5,7 @@ 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;
|
||||
|
||||
/**
|
||||
@ -14,13 +15,14 @@ final class JpegPhoto extends Binary
|
||||
{
|
||||
use MD5Updates;
|
||||
|
||||
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
|
||||
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): 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);
|
||||
}
|
||||
}
|
61
app/Classes/LDAP/Attribute/Certificate.php
Normal file
61
app/Classes/LDAP/Attribute/Certificate.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
14
app/Classes/LDAP/Attribute/CertificateList.php
Normal file
14
app/Classes/LDAP/Attribute/CertificateList.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?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;
|
||||
}
|
@ -3,7 +3,6 @@
|
||||
namespace App\Classes\LDAP\Attribute;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
use App\Classes\LDAP\Attribute;
|
||||
|
||||
@ -20,39 +19,45 @@ class Factory
|
||||
* Map of attributes to appropriate class
|
||||
*/
|
||||
public const map = [
|
||||
'authorityrevocationlist' => CertificateList::class,
|
||||
'cacertificate' => Certificate::class,
|
||||
'certificaterevocationlist' => CertificateList::class,
|
||||
'createtimestamp' => Internal\Timestamp::class,
|
||||
'creatorsname' => Internal\DN::class,
|
||||
'contextcsn' => Internal\CSN::class,
|
||||
'entrycsn' => Internal\CSN::class,
|
||||
'entrydn' => Internal\DN::class,
|
||||
'entryuuid' => Internal\UUID::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,
|
||||
'gidnumber' => GidNumber::class,
|
||||
'hassubordinates' => Internal\HasSubordinates::class,
|
||||
'jpegphoto' => Binary\JpegPhoto::class,
|
||||
'modifytimestamp' => Internal\Timestamp::class,
|
||||
'modifiersname' => Internal\DN::class,
|
||||
'monitorcontext' => Schema\Generic::class,
|
||||
'namingcontexts' => Schema\Generic::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 $attribute,array $values): Attribute
|
||||
public static function create(string $dn,string $attribute,array $values,array $oc=[]): Attribute
|
||||
{
|
||||
$class = Arr::get(self::map,strtolower($attribute),Attribute::class);
|
||||
Log::debug(sprintf('%s:Creating LDAP Attribute [%s] as [%s]',static::LOGKEY,$attribute,$class));
|
||||
|
||||
return new $class($attribute,$values);
|
||||
return new $class($dn,$attribute,$values,$oc);
|
||||
}
|
||||
}
|
@ -2,8 +2,6 @@
|
||||
|
||||
namespace App\Classes\LDAP\Attribute;
|
||||
|
||||
use Illuminate\Contracts\View\View;
|
||||
|
||||
use App\Classes\LDAP\Attribute;
|
||||
|
||||
/**
|
||||
@ -11,12 +9,6 @@ use App\Classes\LDAP\Attribute;
|
||||
*/
|
||||
abstract class Internal extends Attribute
|
||||
{
|
||||
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);
|
||||
}
|
||||
protected ?bool $_is_internal = TRUE;
|
||||
protected(set) bool $no_attr_tags = TRUE;
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\LDAP\Attribute\Internal;
|
||||
|
||||
use App\Classes\LDAP\Attribute\Internal;
|
||||
|
||||
/**
|
||||
* Represents an CSN Attribute
|
||||
*/
|
||||
final class CSN extends Internal
|
||||
{
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\LDAP\Attribute\Internal;
|
||||
|
||||
use App\Classes\LDAP\Attribute\Internal;
|
||||
|
||||
/**
|
||||
* Represents an DN Attribute
|
||||
*/
|
||||
final class DN extends Internal
|
||||
{
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\LDAP\Attribute\Internal;
|
||||
|
||||
use App\Classes\LDAP\Attribute\Internal;
|
||||
|
||||
/**
|
||||
* Represents an HasSubordinates Attribute
|
||||
*/
|
||||
final class HasSubordinates extends Internal
|
||||
{
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\LDAP\Attribute\Internal;
|
||||
|
||||
use App\Classes\LDAP\Attribute\Internal;
|
||||
|
||||
/**
|
||||
* Represents an StructuralObjectClass Attribute
|
||||
*/
|
||||
final class StructuralObjectClass extends Internal
|
||||
{
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\LDAP\Attribute\Internal;
|
||||
|
||||
use App\Classes\LDAP\Attribute\Internal;
|
||||
|
||||
/**
|
||||
* Represents an SubschemaSubentry Attribute
|
||||
*/
|
||||
final class SubschemaSubentry extends Internal
|
||||
{
|
||||
}
|
@ -5,13 +5,14 @@ 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): View
|
||||
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
|
||||
{
|
||||
// @note Internal attributes cannot be edited
|
||||
return view('components.attribute.internal.timestamp')
|
||||
|
@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\LDAP\Attribute\Internal;
|
||||
|
||||
use App\Classes\LDAP\Attribute\Internal;
|
||||
|
||||
/**
|
||||
* Represents an UUID Attribute
|
||||
*/
|
||||
final class UUID extends Internal
|
||||
{
|
||||
}
|
43
app/Classes/LDAP/Attribute/KrbPrincipalKey.php
Normal file
43
app/Classes/LDAP/Attribute/KrbPrincipalKey.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
63
app/Classes/LDAP/Attribute/KrbTicketFlags.php
Normal file
63
app/Classes/LDAP/Attribute/KrbTicketFlags.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?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());
|
||||
}
|
||||
}
|
13
app/Classes/LDAP/Attribute/NoAttrTags/Generic.php
Normal file
13
app/Classes/LDAP/Attribute/NoAttrTags/Generic.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?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;
|
||||
}
|
@ -6,22 +6,31 @@ 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;
|
||||
|
||||
public function __construct(string $name,array $values)
|
||||
/**
|
||||
* 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=[])
|
||||
{
|
||||
parent::__construct($name,$values);
|
||||
parent::__construct($dn,$name,$values,['top']);
|
||||
|
||||
$this->oc_schema = config('server')
|
||||
->schema('objectclasses')
|
||||
->filter(fn($item)=>$this->values->contains($item->name));
|
||||
$this->set_oc_schema($this->tagValuesOld());
|
||||
}
|
||||
|
||||
public function __get(string $key): mixed
|
||||
@ -32,6 +41,22 @@ final class ObjectClass extends Attribute
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
*
|
||||
@ -45,12 +70,20 @@ final class ObjectClass extends Attribute
|
||||
->contains($value);
|
||||
}
|
||||
|
||||
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
|
||||
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
|
||||
{
|
||||
return view('components.attribute.objectclass')
|
||||
->with('o',$this)
|
||||
->with('edit',$edit)
|
||||
->with('old',$old)
|
||||
->with('new',$new);
|
||||
->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));
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
use App\Classes\LDAP\Attribute;
|
||||
use App\Classes\Template;
|
||||
use App\Traits\MD5Updates;
|
||||
|
||||
/**
|
||||
@ -15,6 +16,10 @@ 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\\';
|
||||
|
||||
@ -75,29 +80,35 @@ 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): View
|
||||
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): 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(int $key): ?string
|
||||
public function render_item_old(string $dotkey): ?string
|
||||
{
|
||||
$pw = Arr::get($this->oldValues,$key);
|
||||
$pw = parent::render_item_old($dotkey);
|
||||
|
||||
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(int $key): ?string
|
||||
public function render_item_new(string $dotkey): ?string
|
||||
{
|
||||
$pw = Arr::get($this->values,$key);
|
||||
$pw = parent::render_item_new($dotkey);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -10,16 +10,16 @@ final class Argon2i extends Base
|
||||
|
||||
public static function subid(string $password): bool
|
||||
{
|
||||
return str_starts_with(base64_decode(self::password($password)),self::identifier.'$');
|
||||
return str_starts_with(self::password($password),self::identifier.'$');
|
||||
}
|
||||
|
||||
public function compare(string $source,string $compare): bool
|
||||
{
|
||||
return password_verify($compare,base64_decode($this->password($source)));
|
||||
return password_verify($compare,$this->password($source));
|
||||
}
|
||||
|
||||
public function encode(string $password): string
|
||||
{
|
||||
return sprintf('{%s}%s',self::key,base64_encode(password_hash($password,PASSWORD_ARGON2I)));
|
||||
return sprintf('{%s}%s',self::key,password_hash($password,PASSWORD_ARGON2I));
|
||||
}
|
||||
}
|
@ -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));
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
use App\Classes\LDAP\Attribute;
|
||||
use App\Classes\Template;
|
||||
|
||||
/**
|
||||
* Represents the RDN for an Entry
|
||||
@ -13,6 +14,9 @@ use App\Classes\LDAP\Attribute;
|
||||
final class RDN extends Attribute
|
||||
{
|
||||
private string $base;
|
||||
|
||||
protected(set) bool $no_attr_tags = TRUE;
|
||||
|
||||
private Collection $attrs;
|
||||
|
||||
public function __get(string $key): mixed
|
||||
@ -24,14 +28,14 @@ final class RDN extends Attribute
|
||||
};
|
||||
}
|
||||
|
||||
public function hints(): array
|
||||
public function hints(): Collection
|
||||
{
|
||||
return [
|
||||
return collect([
|
||||
'required' => __('RDN is required')
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE): View
|
||||
public function render(bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL): View
|
||||
{
|
||||
return view('components.attribute.rdn')
|
||||
->with('o',$this);
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Classes\LDAP\Attribute;
|
||||
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
@ -14,6 +13,7 @@ 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 OR preg_match('/^#/',$line))
|
||||
if ((! $line) || preg_match('/^#/',$line))
|
||||
continue;
|
||||
|
||||
$fields = explode(':',$line);
|
||||
@ -41,18 +41,14 @@ 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);
|
||||
}
|
||||
|
||||
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);
|
||||
return Arr::get(($array ? $array->get($string) : []),
|
||||
$key,
|
||||
__('No description available, can you help with one?'));
|
||||
}
|
||||
}
|
21
app/Classes/LDAP/Attribute/Schema/Generic.php
Normal file
21
app/Classes/LDAP/Attribute/Schema/Generic.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ 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
|
||||
@ -33,7 +34,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): View
|
||||
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.mechanisms')
|
||||
|
@ -5,6 +5,7 @@ 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
|
||||
@ -34,7 +35,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): View
|
||||
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.oid')
|
||||
|
@ -29,7 +29,7 @@ abstract class Export
|
||||
|
||||
abstract public function __toString(): string;
|
||||
|
||||
protected function header()
|
||||
protected function header(): string
|
||||
{
|
||||
$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'),config('app.url'),date('F j, Y g:i a')).$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',__('Exported by'),Auth::user() ?: 'Anonymous').$this->br;
|
||||
$output .= sprintf('# %s: %s',__('Version'),config('app.version')).$this->br;
|
||||
|
||||
|
@ -5,6 +5,7 @@ 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
|
||||
@ -41,6 +42,7 @@ 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)
|
||||
@ -48,6 +50,17 @@ class LDIF extends Export
|
||||
: 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,9 +3,10 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
@ -16,6 +17,8 @@ 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;
|
||||
@ -48,7 +51,6 @@ abstract class Import
|
||||
* @param int $action
|
||||
* @return Collection
|
||||
* @throws GeneralException
|
||||
* @throws ObjectExistsException
|
||||
*/
|
||||
final protected function commit(Entry $o,int $action): Collection
|
||||
{
|
||||
@ -57,7 +59,10 @@ abstract class Import
|
||||
try {
|
||||
$o->save();
|
||||
|
||||
} catch (\Exception $e) {
|
||||
} 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)',
|
||||
@ -66,8 +71,18 @@ abstract class Import
|
||||
$x->getDiagnosticMessage(),
|
||||
)
|
||||
]);
|
||||
else
|
||||
return collect([
|
||||
'dn'=>$o->getDN(),
|
||||
'result'=>sprintf('%d: %s',
|
||||
$e->getCode(),
|
||||
$e->getMessage(),
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
Log::debug(sprintf('%s:Import Commited',self::LOGKEY));
|
||||
|
||||
return collect(['dn'=>$o->getDN(),'result'=>__('Created')]);
|
||||
|
||||
default:
|
||||
|
@ -46,9 +46,9 @@ class LDIF extends Import
|
||||
if (! $line) {
|
||||
if (! is_null($o)) {
|
||||
// Add the last attribute;
|
||||
$o->addAttribute($attribute,$base64encoded ? base64_decode($value) : $value);
|
||||
$o->addAttributeItem($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,8 +59,6 @@ class LDIF extends Import
|
||||
$base64encoded = FALSE;
|
||||
$attribute = NULL;
|
||||
$value = '';
|
||||
|
||||
// Else its a blank line
|
||||
}
|
||||
|
||||
continue;
|
||||
@ -69,7 +67,7 @@ class LDIF extends Import
|
||||
$m = [];
|
||||
preg_match('/^([a-zA-Z0-9;-]+)(:+)\s+(.*)$/',$line,$m);
|
||||
|
||||
switch ($x=Arr::get($m,1)) {
|
||||
switch (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));
|
||||
@ -97,7 +95,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;
|
||||
@ -125,7 +123,7 @@ class LDIF extends Import
|
||||
Log::debug(sprintf('%s:Adding Attribute [%s] value [%s] (%d)',self::LOGKEY,$attribute,$value,$c));
|
||||
|
||||
if ($value)
|
||||
$o->addAttribute($attribute,$base64encoded ? base64_decode($value) : $value);
|
||||
$o->addAttributeItem($attribute,$base64encoded ? base64_decode($value) : $value);
|
||||
else
|
||||
throw new GeneralException(sprintf('Attribute has no value [%s] (line %d)',$attribute,$c));
|
||||
}
|
||||
@ -133,11 +131,10 @@ 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)
|
||||
@ -147,9 +144,9 @@ class LDIF extends Import
|
||||
// We may still have a pending action
|
||||
if ($action) {
|
||||
// Add the last attribute;
|
||||
$o->addAttribute($attribute,$base64encoded ? base64_decode($value) : $value);
|
||||
$o->addAttributeItem($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));
|
||||
@ -159,8 +156,8 @@ class LDIF extends Import
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function readEntry() {
|
||||
static $haveVersion = false;
|
||||
public function xreadEntry() {
|
||||
static $haveVersion = FALSE;
|
||||
|
||||
if ($lines = $this->nextLines()) {
|
||||
|
||||
@ -179,7 +176,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':
|
||||
@ -201,7 +198,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);
|
||||
|
||||
@ -221,13 +218,13 @@ class LDIF extends Import
|
||||
|
||||
default:
|
||||
if (! $server->dnExists($dn))
|
||||
return $this->error(_('Unkown change type'),$lines);
|
||||
return $this->error(_('Unknown change type'),$lines);
|
||||
}
|
||||
|
||||
} else
|
||||
return $this->error(_('A valid dn line is required'),$lines);
|
||||
|
||||
} else
|
||||
return false;
|
||||
return FALSE;
|
||||
}
|
||||
}
|
@ -6,302 +6,75 @@ 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 {
|
||||
// The attribute from which this attribute inherits (if any)
|
||||
private ?string $sup_attribute = NULL;
|
||||
final class AttributeType extends Base
|
||||
{
|
||||
private const LOGKEY = 'SAT';
|
||||
|
||||
// Array of AttributeTypes which inherit from this one
|
||||
private Collection $children;
|
||||
// An array of AttributeTypes which inherit from this one
|
||||
private(set) Collection $children;
|
||||
|
||||
// The equality rule used
|
||||
private ?string $equality = NULL;
|
||||
|
||||
// The ordering of the attributeType
|
||||
private ?string $ordering = NULL;
|
||||
|
||||
// Supports substring matching?
|
||||
private ?string $sub_str_rule = NULL;
|
||||
|
||||
// The full syntax string, ie 1.2.3.4{16}
|
||||
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 ?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 Collection $used_in_object_classes;
|
||||
|
||||
// A list of object class names that require this attribute type.
|
||||
private Collection $required_by_object_classes;
|
||||
private(set) ?string $equality = NULL;
|
||||
|
||||
// This attribute has been forced a MAY attribute by the configuration.
|
||||
private bool $forced_as_may = FALSE;
|
||||
private(set) 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));
|
||||
// boolean: is collective?
|
||||
private(set) bool $is_collective = FALSE;
|
||||
|
||||
parent::__construct($line);
|
||||
// Is this a must attribute
|
||||
private(set) bool $is_must = FALSE;
|
||||
|
||||
$strings = preg_split('/[\s,]+/',$line,-1,PREG_SPLIT_DELIM_CAPTURE);
|
||||
// boolean: can use modify?
|
||||
private(set) bool $is_no_user_modification = FALSE;
|
||||
|
||||
// Init
|
||||
$this->children = collect();
|
||||
$this->aliases = collect();
|
||||
$this->used_in_object_classes = collect();
|
||||
$this->required_by_object_classes = collect();
|
||||
// boolean: is single valued only?
|
||||
private(set) bool $is_single_value = FALSE;
|
||||
|
||||
for ($i=0; $i < count($strings); $i++) {
|
||||
switch ($strings[$i]) {
|
||||
case '(':
|
||||
case ')':
|
||||
break;
|
||||
// The max number of characters this attribute can be
|
||||
private(set) ?int $max_length = NULL;
|
||||
|
||||
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];
|
||||
// An array of names (including aliases) that this attribute is known by
|
||||
private(set) Collection $names;
|
||||
|
||||
} while (! preg_match("/\'$/s",$strings[$i]));
|
||||
// The ordering of the attributeType
|
||||
private(set) ?string $ordering = NULL;
|
||||
|
||||
// This attribute has no aliases
|
||||
//$this->aliases = collect();
|
||||
// A list of object class names that require this attribute type.
|
||||
private(set) Collection $required_by_object_classes;
|
||||
|
||||
} else {
|
||||
$i++;
|
||||
// Which objectclass is defining this attribute for an Entry
|
||||
public ?string $source = NULL;
|
||||
|
||||
do {
|
||||
// In case we came here becaues of a ('
|
||||
if (preg_match('/^\(/',$strings[$i]))
|
||||
$strings[$i] = preg_replace('/^\(/','',$strings[$i]);
|
||||
else
|
||||
$i++;
|
||||
// Supports substring matching?
|
||||
private(set) ?string $sub_str_rule = NULL;
|
||||
|
||||
$this->name .= ($this->name ? ' ' : '').$strings[++$i];
|
||||
// The attribute from which this attribute inherits (if any)
|
||||
private(set) ?string $sup_attribute = NULL;
|
||||
|
||||
} while (! preg_match("/\'$/s",$strings[$i]));
|
||||
// The full syntax string, ie 1.2.3.4{16}
|
||||
private(set) ?string $syntax = NULL;
|
||||
private(set) ?string $syntax_oid = NULL;
|
||||
|
||||
// Add alias names for this attribute
|
||||
while ($strings[++$i] != ')') {
|
||||
$alias = $strings[$i];
|
||||
$alias = preg_replace("/^\'(.*)\'$/",'$1',$alias);
|
||||
$this->addAlias($alias);
|
||||
}
|
||||
}
|
||||
// The usage string set by the LDAP schema
|
||||
private(set) ?string $usage = NULL;
|
||||
|
||||
$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;
|
||||
}
|
||||
// An array of objectClasses which use this attributeType (must be set by caller)
|
||||
private(set) Collection $used_in_object_classes;
|
||||
|
||||
public function __get(string $key): mixed
|
||||
{
|
||||
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);
|
||||
return match ($key) {
|
||||
'names_lc' => $this->names->map('strtolower'),
|
||||
default => parent::__get($key)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -312,7 +85,8 @@ final class AttributeType extends Base {
|
||||
*/
|
||||
public function addChild(string $child): void
|
||||
{
|
||||
$this->children->push($child);
|
||||
$this->children
|
||||
->push($child);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -320,11 +94,12 @@ 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): void
|
||||
public function addRequiredByObjectClass(string $name,bool $structural): void
|
||||
{
|
||||
if (! $this->required_by_object_classes->contains($name))
|
||||
$this->required_by_object_classes->push($name);
|
||||
if (! $this->required_by_object_classes->has($name))
|
||||
$this->required_by_object_classes->put($name,$structural);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -332,6 +107,7 @@ 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
|
||||
{
|
||||
@ -339,179 +115,176 @@ final class AttributeType extends Base {
|
||||
$this->used_in_object_classes->put($name,$structural);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
private function factory(): Attribute
|
||||
{
|
||||
return $this->aliases;
|
||||
return Attribute\Factory::create(
|
||||
dn:'',
|
||||
attribute:$this->name,
|
||||
values:[]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets this attribute's equality string
|
||||
* For a list of objectclasses return all parent objectclasses as well
|
||||
*
|
||||
* @return string
|
||||
* @deprecated use $this->equality
|
||||
* @param Collection $ocs
|
||||
* @return Collection
|
||||
*/
|
||||
public function getEquality()
|
||||
private function heirachy(Collection $ocs): Collection
|
||||
{
|
||||
return $this->equality;
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this attribute is collective.
|
||||
* Creates a new AttributeType object from a raw LDAP AttributeType string.
|
||||
*
|
||||
* @return boolean Returns TRUE if this attribute is collective and FALSE otherwise.
|
||||
* @deprecated use $this->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 )
|
||||
*/
|
||||
public function getIsCollective(): bool
|
||||
protected function parse(string $line): void
|
||||
{
|
||||
return $this->is_collective;
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
protected function parse_chunk(array $strings,int &$i): void
|
||||
{
|
||||
return $this->is_no_user_modification;
|
||||
switch ($strings[$i]) {
|
||||
case 'NAME':
|
||||
$name = '';
|
||||
|
||||
// @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];
|
||||
|
||||
} while (! preg_match("/\'$/s",$strings[$i]));
|
||||
|
||||
} else {
|
||||
$i++;
|
||||
|
||||
do {
|
||||
// In case we came here because of a ('
|
||||
if (preg_match('/^\(/',$strings[$i]))
|
||||
$strings[$i] = preg_replace('/^\(/','',$strings[$i]);
|
||||
else
|
||||
$i++;
|
||||
|
||||
$name .= ($name ? ' ' : '').$strings[++$i];
|
||||
|
||||
} while (! preg_match("/\'$/s",$strings[$i]));
|
||||
|
||||
// Add alias names for this attribute
|
||||
while ($strings[++$i] !== ')') {
|
||||
$alias = preg_replace("/^\'(.*)\'$/",'$1',$strings[$i]);
|
||||
$this->names->push($alias);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
$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 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;
|
||||
}
|
||||
if (static::DEBUG_VERBOSE)
|
||||
Log::debug(sprintf('%s:- Case NAME returned (%s)',self::LOGKEY,$this->name),['names'=>$this->names]);
|
||||
break;
|
||||
|
||||
/**
|
||||
* Gets this attribute's ordering specification.
|
||||
*
|
||||
* @return string
|
||||
* @deprecated use $this->ordering
|
||||
*/
|
||||
public function getOrdering(): string
|
||||
{
|
||||
return $this->ordering;
|
||||
}
|
||||
case 'SUP':
|
||||
$this->sup_attribute = preg_replace("/^\'(.*)\'$/",'$1',$strings[++$i]);
|
||||
|
||||
/**
|
||||
* Gets this attribute's substring matching specification
|
||||
*
|
||||
* @return string
|
||||
* @deprecated use $this->sub_str_rule;
|
||||
*/
|
||||
public function getSubstr() {
|
||||
return $this->sub_str_rule;
|
||||
}
|
||||
if (static::DEBUG_VERBOSE)
|
||||
Log::debug(sprintf('%s:- Case SUP returned (%s)',self::LOGKEY,$this->sup_attribute));
|
||||
break;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
case 'EQUALITY':
|
||||
$this->equality = $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;
|
||||
}
|
||||
if (static::DEBUG_VERBOSE)
|
||||
Log::debug(sprintf('%s:- Case EQUALITY returned (%s)',self::LOGKEY,$this->equality));
|
||||
break;
|
||||
|
||||
/**
|
||||
* Gets this attribute's usage string as defined by the LDAP server
|
||||
*
|
||||
* @return string
|
||||
* @deprecated use $this->usage
|
||||
*/
|
||||
public function getUsage()
|
||||
{
|
||||
return $this->usage;
|
||||
}
|
||||
case 'ORDERING':
|
||||
$this->ordering = $strings[++$i];
|
||||
|
||||
/**
|
||||
* 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 ORDERING returned (%s)',self::LOGKEY,$this->ordering));
|
||||
break;
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* @deprecated use $this->forced_as_may
|
||||
*/
|
||||
public function isForceMay(): bool
|
||||
{
|
||||
return $this->forced_as_may;
|
||||
}
|
||||
case 'SUBSTR':
|
||||
$this->sub_str_rule = $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 SUBSTR returned (%s)',self::LOGKEY,$this->sub_str_rule));
|
||||
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 'SYNTAX':
|
||||
$this->syntax = preg_replace("/^\'(.*)\'$/",'$1',$strings[++$i]);
|
||||
$this->syntax_oid = preg_replace("/^\'?(.*){\d+}\'?$/",'$1',$this->syntax);
|
||||
|
||||
/**
|
||||
* This function will mark this attribute as a forced MAY attribute
|
||||
*/
|
||||
public function setForceMay() {
|
||||
$this->forced_as_may = TRUE;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -525,13 +298,28 @@ final class AttributeType extends Base {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets this attribute's SUP attribute (ie, the attribute from which this attribute inherits).
|
||||
* If this is a MUST attribute to the objectclass that defines it
|
||||
*
|
||||
* @param string $attr The name of the new parent (SUP) attribute
|
||||
* @return void
|
||||
*/
|
||||
public function setSupAttribute(string $attr): void
|
||||
public function setMust(): void
|
||||
{
|
||||
$this->sup_attribute = trim($attr);
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -544,21 +332,22 @@ final class AttributeType extends Base {
|
||||
*/
|
||||
public function validation(array $array): ?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))
|
||||
// For each item in array, we need to get the OC hierarchy
|
||||
$heirachy = $this->heirachy(collect($array)
|
||||
->flatten()
|
||||
->unique();
|
||||
->filter());
|
||||
|
||||
// Get any config validation
|
||||
$validation = collect(Arr::get(config('ldap.validation'),$this->name_lc,[]));
|
||||
if (($heirachy->intersect($this->required_by_object_classes)->count() > 0)
|
||||
|
||||
$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)
|
||||
&& (! collect($validation->get($this->name_lc))->contains('required'))) {
|
||||
$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.'.*',[])));
|
||||
$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);
|
||||
}
|
||||
|
||||
return $validation->toArray();
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Classes\LDAP\Schema;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
use App\Exceptions\InvalidUsage;
|
||||
|
||||
/**
|
||||
@ -10,38 +12,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 {
|
||||
abstract class Base
|
||||
{
|
||||
private const LOGKEY = 'Sb-';
|
||||
|
||||
protected const DEBUG_VERBOSE = FALSE;
|
||||
|
||||
// Record the LDAP String
|
||||
private string $line;
|
||||
private(set) string $line;
|
||||
|
||||
// The schema item's name.
|
||||
protected string $name = '';
|
||||
protected(set) string $name = '';
|
||||
|
||||
// The OID of this schema item.
|
||||
protected string $oid;
|
||||
protected(set) string $oid = '';
|
||||
|
||||
# The description of this schema item.
|
||||
protected string $description = '';
|
||||
protected(set) string $description = '';
|
||||
|
||||
// Boolean value indicating whether this objectClass is obsolete
|
||||
private bool $is_obsolete = FALSE;
|
||||
private(set) 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);
|
||||
@ -54,69 +56,95 @@ 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;
|
||||
}
|
||||
|
||||
public function setDescription(string $desc): void
|
||||
protected function parse(string $line): void
|
||||
{
|
||||
$this->description = $desc;
|
||||
$strings = preg_split('/[\s,]+/',$line,-1,PREG_SPLIT_DELIM_CAPTURE);
|
||||
|
||||
for ($i=0; $i < count($strings); $i++) {
|
||||
$this->parse_chunk($strings,$i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets this attribute's name.
|
||||
*
|
||||
* @param string $name The new name to give this attribute.
|
||||
*/
|
||||
public function setName($name): void
|
||||
protected function parse_chunk(array $strings,int &$i): void
|
||||
{
|
||||
$this->name = $name;
|
||||
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]));
|
||||
}
|
||||
|
||||
public function setOID(string $oid): void
|
||||
{
|
||||
$this->oid = $oid;
|
||||
$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]));
|
||||
}
|
||||
}
|
||||
}
|
@ -6,74 +6,49 @@ use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Represents an LDAP Syntax
|
||||
*
|
||||
* @package phpLDAPadmin
|
||||
* @subpackage Schema
|
||||
*/
|
||||
final class LDAPSyntax extends Base {
|
||||
final class LDAPSyntax extends Base
|
||||
{
|
||||
private const LOGKEY = 'SLS';
|
||||
|
||||
// Is human readable?
|
||||
private ?bool $is_not_human_readable = NULL;
|
||||
private(set) ?bool $is_not_human_readable = NULL;
|
||||
|
||||
// Binary transfer required?
|
||||
private ?bool $binary_transfer_required = NULL;
|
||||
private(set) ?bool $binary_transfer_required = NULL;
|
||||
|
||||
/**
|
||||
* Creates a new Syntax object from a raw LDAP syntax string.
|
||||
*/
|
||||
public function __construct(string $line) {
|
||||
Log::debug(sprintf('Parsing LDAPSyntax [%s]',$line));
|
||||
protected function parse(string $line): void
|
||||
{
|
||||
if (static::DEBUG_VERBOSE)
|
||||
Log::debug(sprintf('%s:Parsing LDAPSyntax [%s]',self::LOGKEY,$line));
|
||||
|
||||
parent::__construct($line);
|
||||
|
||||
$strings = preg_split('/[\s,]+/',$line,-1,PREG_SPLIT_DELIM_CAPTURE);
|
||||
parent::parse($line);
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
Log::debug(sprintf('- Case X-BINARY-TRANSFER-REQUIRED returned (%s)',$this->binary_transfer_required));
|
||||
if (static::DEBUG_VERBOSE)
|
||||
Log::debug(sprintf('%s:- Case X-BINARY-TRANSFER-REQUIRED returned (%s)',self::LOGKEY,$this->binary_transfer_required));
|
||||
break;
|
||||
|
||||
case 'X-NOT-HUMAN-READABLE':
|
||||
$this->is_not_human_readable = (str_replace("'",'',$strings[++$i]) === 'TRUE');
|
||||
|
||||
Log::debug(sprintf('- Case X-NOT-HUMAN-READABLE returned (%s)',$this->is_not_human_readable));
|
||||
if (static::DEBUG_VERBOSE)
|
||||
Log::debug(sprintf('%s:- Case X-NOT-HUMAN-READABLE returned (%s)',self::LOGKEY,$this->is_not_human_readable));
|
||||
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]);
|
||||
parent::parse_chunk($strings,$i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -7,106 +7,16 @@ use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Represents an LDAP MatchingRule
|
||||
*
|
||||
* @package phpLDAPadmin
|
||||
* @subpackage Schema
|
||||
*/
|
||||
final class MatchingRule extends Base {
|
||||
final class MatchingRule extends Base
|
||||
{
|
||||
private const LOGKEY = 'SMR';
|
||||
|
||||
// This rule's syntax OID
|
||||
private ?string $syntax = NULL;
|
||||
private(set) ?string $syntax = NULL;
|
||||
|
||||
// An array of attribute names who use this MatchingRule
|
||||
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);
|
||||
}
|
||||
}
|
||||
private(set) Collection $used_by_attrs;
|
||||
|
||||
/**
|
||||
* Adds an attribute name to the list of attributes who use this MatchingRule
|
||||
@ -120,23 +30,33 @@ final class MatchingRule extends Base {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an array of attribute names (strings) which use this MatchingRule
|
||||
* Creates a new MatchingRule object from a raw LDAP MatchingRule string.
|
||||
*
|
||||
* @return array The array of attribute names (strings).
|
||||
* @deprecated use $this->used_by_attrs
|
||||
* @param string $line
|
||||
* @return void
|
||||
*/
|
||||
public function getUsedByAttrs()
|
||||
protected function parse(string $line): void
|
||||
{
|
||||
return $this->used_by_attrs;
|
||||
if (static::DEBUG_VERBOSE)
|
||||
Log::debug(sprintf('%s:Parsing MatchingRule [%s]',self::LOGKEY,$line));
|
||||
|
||||
// Init
|
||||
$this->used_by_attrs = collect();
|
||||
|
||||
parent::parse($line);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
protected function parse_chunk(array $strings,int &$i): void
|
||||
{
|
||||
$this->used_by_attrs = $attrs;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,99 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
@ -10,206 +10,28 @@ 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 Collection $sup_classes;
|
||||
private(set) Collection $sup_classes;
|
||||
|
||||
// One of STRUCTURAL, ABSTRACT, or AUXILIARY
|
||||
private int $type;
|
||||
|
||||
// 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Attributes that this objectclass defines
|
||||
private(set) Collection $attributes;
|
||||
|
||||
public function __get(string $key): mixed
|
||||
{
|
||||
return match ($key) {
|
||||
'attributes' => $this->getAllAttrs(TRUE),
|
||||
'sup' => $this->sup_classes,
|
||||
'all_attributes' => $this->getMustAttrs(TRUE)
|
||||
->merge($this->getMayAttrs(TRUE)),
|
||||
'type_name' => match ($this->type) {
|
||||
Server::OC_STRUCTURAL => 'Structural',
|
||||
Server::OC_ABSTRACT => 'Abstract',
|
||||
@ -220,23 +42,6 @@ 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.
|
||||
@ -245,57 +50,8 @@ final class ObjectClass extends Base
|
||||
*/
|
||||
public function addChildObjectClass(string $name): void
|
||||
{
|
||||
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;
|
||||
if (! $this->child_classes->contains($name))
|
||||
$this->child_classes->push($name);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -313,42 +69,26 @@ final class ObjectClass extends Base
|
||||
*/
|
||||
public function getMayAttrs(bool $parents=FALSE): Collection
|
||||
{
|
||||
// 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));
|
||||
|
||||
$attrs = $this->may_attrs;
|
||||
$attrs = $this->attributes
|
||||
->filter(fn($item)=>! $item->is_must)
|
||||
->transform(function($item) {
|
||||
$item->source = $this->name;
|
||||
return $item;
|
||||
});
|
||||
|
||||
if ($parents)
|
||||
foreach ($this->getParents() as $object_class)
|
||||
$attrs = $attrs->merge($object_class->getMayAttrs($parents));
|
||||
|
||||
// Remove any duplicates
|
||||
$attrs = $attrs->unique(function($item) { return $item->name; });
|
||||
$attrs = $attrs->merge($object_class
|
||||
->getMayAttrs($parents)
|
||||
->transform(function($item) use ($object_class) {
|
||||
$item->source = $item->source ?: $object_class->name;
|
||||
return $item;
|
||||
}));
|
||||
|
||||
// Return a sorted list
|
||||
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');
|
||||
return $attrs
|
||||
->unique(fn($item)=>$item->name)
|
||||
->sortBy(fn($item)=>$item->name);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -365,41 +105,26 @@ final class ObjectClass extends Base
|
||||
*/
|
||||
public function getMustAttrs(bool $parents=FALSE): Collection
|
||||
{
|
||||
// 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); });
|
||||
|
||||
$attrs = $this->must_attrs;
|
||||
$attrs = $this->attributes
|
||||
->filter(fn($item)=>$item->is_must)
|
||||
->transform(function($item) {
|
||||
$item->source = $this->name;
|
||||
return $item;
|
||||
});
|
||||
|
||||
if ($parents)
|
||||
foreach ($this->getParents() as $object_class)
|
||||
$attrs = $attrs->merge($object_class->getMustAttrs($parents));
|
||||
|
||||
// Remove any duplicates
|
||||
$attrs = $attrs->unique(function($item) { return $item->name; });
|
||||
$attrs = $attrs->merge($object_class
|
||||
->getMustAttrs($parents)
|
||||
->transform(function($item) use ($object_class) {
|
||||
$item->source = $item->source ?: $object_class->name;
|
||||
return $item;
|
||||
}));
|
||||
|
||||
// Return a sorted list
|
||||
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');
|
||||
return $attrs
|
||||
->unique(fn($item)=>$item->name)
|
||||
->sortBy(fn($item)=>$item->name);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -426,27 +151,6 @@ 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
|
||||
*
|
||||
@ -457,39 +161,109 @@ final class ObjectClass extends Base
|
||||
return $this->type === Server::OC_AUXILIARY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an array is listed in the may_force attrs
|
||||
*/
|
||||
public function isForceMay(string $attr): bool
|
||||
{
|
||||
return $this->may_force->ppluck('name')->contains($attr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if this objectClass is related to $oclass
|
||||
*
|
||||
* @param array $oclass ObjectClasses that this attribute may be related to
|
||||
* @return bool
|
||||
* @throws InvalidUsage
|
||||
*/
|
||||
public function isRelated(array $oclass): bool
|
||||
{
|
||||
// If I am in the array, we'll just return false
|
||||
if (in_array_ignore_case($this->name,$oclass))
|
||||
return FALSE;
|
||||
|
||||
foreach ($oclass as $object_class)
|
||||
if ($object_class->isStructural() && in_array_ignore_case($this->name,$object_class->getParents()->pluck('name')))
|
||||
return TRUE;
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
public function isStructural(): bool
|
||||
{
|
||||
return $this->type === Server::OC_STRUCTURAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
protected function parse(string $line): void
|
||||
{
|
||||
Log::debug(sprintf('%s:Parsing ObjectClass [%s]',self::LOGKEY,$line));
|
||||
|
||||
// Init
|
||||
$this->attributes = collect();
|
||||
$this->sup_classes = collect();
|
||||
$this->child_classes = collect();
|
||||
|
||||
parent::parse($line);
|
||||
}
|
||||
|
||||
protected function parse_chunk(array $strings,int &$i): void
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an LDAP schema list
|
||||
*
|
||||
|
@ -1,40 +0,0 @@
|
||||
<?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),
|
||||
};
|
||||
}
|
||||
}
|
@ -8,49 +8,49 @@ 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,MatchingRuleUse,ObjectClass};
|
||||
use App\Classes\LDAP\Schema\{AttributeType,Base,LDAPSyntax,MatchingRule,ObjectClass};
|
||||
use App\Exceptions\InvalidUsage;
|
||||
use App\Ldap\Entry;
|
||||
|
||||
final class Server
|
||||
{
|
||||
// Connection information used for these object and children
|
||||
private ?string $connection;
|
||||
private const LOGKEY = 'SVR';
|
||||
|
||||
// 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(?string $connection=NULL)
|
||||
public function __construct()
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->rootDSE = self::rootDSE();
|
||||
|
||||
$this->attributetypes = collect();
|
||||
$this->ldapsyntaxes = collect();
|
||||
$this->matchingrules = collect();
|
||||
$this->objectclasses = collect();
|
||||
}
|
||||
|
||||
public function __get(string $key): mixed
|
||||
{
|
||||
return match($key) {
|
||||
'attributetypes' => $this->attributetypes,
|
||||
'connection' => $this->connection,
|
||||
'ldapsyntaxes' => $this->ldapsyntaxes,
|
||||
'matchingrules' => $this->matchingrules,
|
||||
'objectclasses' => $this->objectclasses,
|
||||
'config' => config('ldap.connections.'.config('ldap.default')),
|
||||
'config' => config(sprintf('ldap.connections.%s',config('ldap.default'))),
|
||||
'name' => Arr::get($this->config,'name',__('No Server Name Yet')),
|
||||
default => throw new Exception('Unknown key:'.$key),
|
||||
};
|
||||
@ -62,20 +62,14 @@ 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(?string $connection=NULL,bool $objects=TRUE): Collection
|
||||
public static function baseDNs(bool $objects=TRUE): Collection
|
||||
{
|
||||
$cachetime = Carbon::now()
|
||||
->addSeconds(Config::get('ldap.cache.time'));
|
||||
|
||||
try {
|
||||
$base = self::rootDSE($connection,$cachetime);
|
||||
$namingcontexts = collect(config('pla.base_dns') ?: self::rootDSE()?->namingcontexts);
|
||||
|
||||
/**
|
||||
* LDAP Error Codes:
|
||||
@ -173,16 +167,6 @@ 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:
|
||||
@ -191,71 +175,107 @@ final class Server
|
||||
}
|
||||
|
||||
if (! $objects)
|
||||
return collect($base->namingcontexts);
|
||||
return $namingcontexts;
|
||||
|
||||
return Cache::remember('basedns'.Session::id(),config('ldap.cache.time'),function() use ($namingcontexts) {
|
||||
$result = collect();
|
||||
|
||||
// @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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
* 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
|
||||
*/
|
||||
$result = collect();
|
||||
foreach ($base->namingcontexts as $dn)
|
||||
$result->push((new Entry)->cache($cachetime)->findOrFail($dn));
|
||||
private static function cacheflush(string $dn): bool
|
||||
{
|
||||
$cache = (! config('ldap.cache.enabled'))
|
||||
|| match (strtolower($dn)) {
|
||||
'','cn=schema','cn=subschema' => FALSE,
|
||||
default => TRUE,
|
||||
};
|
||||
|
||||
return $result;
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the rootDSE for the server, that gives us server information
|
||||
*
|
||||
* @param null $connection
|
||||
* @return Entry|null
|
||||
* @return Model
|
||||
* @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(?string $connection=NULL,?Carbon $cachetime=NULL): ?Model
|
||||
public static function rootDSE(): Model
|
||||
{
|
||||
$e = new Entry;
|
||||
static $rootdse = NULL;
|
||||
|
||||
return Entry::on($connection ?? $e->getConnectionName())
|
||||
->cache($cachetime)
|
||||
->in(NULL)
|
||||
if (is_null($rootdse))
|
||||
$rootdse = self::get('',['+','*'])
|
||||
->read()
|
||||
->select(['+'])
|
||||
->whereHas('objectclass')
|
||||
->firstOrFail();
|
||||
|
||||
return $rootdse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
/* METHODS */
|
||||
|
||||
/**
|
||||
* 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): ?LDAPCollection
|
||||
public function children(string $dn,array $attrs=['dn']): ?LDAPCollection
|
||||
{
|
||||
return ($x=(new Entry)
|
||||
->on($this->connection)
|
||||
->cache(Carbon::now()->addSeconds(Config::get('ldap.cache.time')))
|
||||
->select(['*','hassubordinates'])
|
||||
->setDn($dn)
|
||||
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
|
||||
]))
|
||||
->list()
|
||||
->get()) ? $x : NULL;
|
||||
->get() ?: NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -263,26 +283,47 @@ final class Server
|
||||
*
|
||||
* @param string $dn
|
||||
* @param array $attrs
|
||||
* @return Entry|null
|
||||
* @return Model|null
|
||||
*/
|
||||
public function fetch(string $dn,array $attrs=['*','+']): ?Entry
|
||||
public function fetch(string $dn,array $attrs=['*','+']): ?Model
|
||||
{
|
||||
return ($x=(new Entry)
|
||||
->on($this->connection)
|
||||
->cache(Carbon::now()->addSeconds(Config::get('ldap.cache.time')))
|
||||
->select($attrs)
|
||||
->find($dn)) ? $x : NULL;
|
||||
return $this->get($dn,$attrs)
|
||||
->read()
|
||||
->first() ?: NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function determines if the specified attribute is contained in the force_may list
|
||||
* as configured in config.php.
|
||||
* Get an attribute key for an attributetype name
|
||||
*
|
||||
* @return boolean True if the specified attribute is configured to be force as a may attribute
|
||||
* @param string $key
|
||||
* @return int|bool
|
||||
* @throws InvalidUsage
|
||||
*/
|
||||
public function isForceMay($attr_name): bool
|
||||
public function get_attr_id(string $key): int|bool
|
||||
{
|
||||
return in_array($attr_name,config('pla.force_may',[]));
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -303,57 +344,19 @@ 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);
|
||||
|
||||
$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();
|
||||
|
||||
break;
|
||||
|
||||
case 'ldapsyntaxes':
|
||||
if (isset($this->ldapsyntaxes))
|
||||
return $this->ldapsyntaxes;
|
||||
else
|
||||
$this->ldapsyntaxes = collect();
|
||||
|
||||
break;
|
||||
|
||||
case 'matchingrules':
|
||||
if (isset($this->matchingrules))
|
||||
return $this->matchingrules;
|
||||
else
|
||||
$this->matchingrules = collect();
|
||||
|
||||
break;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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($this->connection);
|
||||
$schema = $this->fetch($schema_dn);
|
||||
$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)
|
||||
@ -361,7 +364,7 @@ final class Server
|
||||
|
||||
switch ($item) {
|
||||
case 'attributetypes':
|
||||
Log::debug('Attribute Types');
|
||||
Log::debug(sprintf('%s:Attribute Types',self::LOGKEY));
|
||||
// build the array of attribueTypes
|
||||
//$syntaxes = $this->SchemaSyntaxes($dn);
|
||||
|
||||
@ -370,148 +373,100 @@ final class Server
|
||||
continue;
|
||||
|
||||
$o = new AttributeType($line);
|
||||
$this->attributetypes->put($o->name_lc,$o);
|
||||
$this->attributetypes->push($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);
|
||||
$attrid = $this->get_attr_id($o->sup_attribute);
|
||||
|
||||
if ($this->attributetypes->has($parent) !== FALSE)
|
||||
$this->attributetypes[$parent]->addChild($o->name);
|
||||
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)
|
||||
// @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);
|
||||
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($parent->is_single_value) && is_null($this->attributetypes[$child]->is_single_value))
|
||||
$this->attributetypes[$child]->setIsSingleValue($parent->is_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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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');
|
||||
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->put(strtolower($o->oid),$o);
|
||||
$this->ldapsyntaxes->push($o);
|
||||
}
|
||||
|
||||
return $this->ldapsyntaxes;
|
||||
|
||||
case 'matchingrules':
|
||||
Log::debug('Matching Rules');
|
||||
$this->matchingruleuse = collect();
|
||||
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->put($o->name_lc,$o);
|
||||
$this->matchingrules->push($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 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());
|
||||
}
|
||||
|
||||
} 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());
|
||||
$rule_id = $this->matchingrules->search(fn($item)=>$item->oid === $attr->equality);
|
||||
|
||||
if ($this->matchingrules->has($rule_key) !== FALSE)
|
||||
$this->matchingrules[$rule_key]->addUsedByAttr($attr->name);
|
||||
}
|
||||
if ($rule_id !== FALSE)
|
||||
$this->matchingrules[$rule_id]->addUsedByAttr($attr->name);
|
||||
}
|
||||
|
||||
return $this->matchingrules;
|
||||
|
||||
case 'objectclasses':
|
||||
Log::debug('Object Classes');
|
||||
Log::debug(sprintf('%s:Object Classes',self::LOGKEY));
|
||||
|
||||
foreach ($schema->{$item} as $line) {
|
||||
if (is_null($line) || ! strlen($line))
|
||||
continue;
|
||||
|
||||
$o = new ObjectClass($line,$this);
|
||||
$this->objectclasses->put($o->name_lc,$o);
|
||||
$o = new ObjectClass($line);
|
||||
$this->objectclasses->push($o);
|
||||
}
|
||||
|
||||
foreach ($this->objectclasses as $o) {
|
||||
// Now go through and reference the parent/child relationships
|
||||
foreach ($this->objectclasses as $o)
|
||||
foreach ($o->getSupClasses() as $parent) {
|
||||
$parent = strtolower($parent);
|
||||
foreach ($o->sup_classes as $sup) {
|
||||
$oc_id = $this->objectclasses->search(fn($item)=>$item->name === $sup);
|
||||
|
||||
if (! $this->objectclasses->contains($parent))
|
||||
$this->objectclasses[$parent]->addChildObjectClass($o->name);
|
||||
if (($oc_id !== FALSE) && (! $this->objectclasses[$oc_id]->child_classes->contains($o->name)))
|
||||
$this->objectclasses[$oc_id]->addChildObjectClass($o->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());
|
||||
|
||||
// Add Required By.
|
||||
if ($attribute->is_must)
|
||||
$this->attributetypes[$attrid]->addRequiredByObjectClass($o->oid,$o->isStructural());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Put the updated attributetypes back in the cache
|
||||
Cache::put('schema.attributetypes',$this->attributetypes,config('ldap.cache.time'));
|
||||
|
||||
return $this->objectclasses;
|
||||
|
||||
// Shouldnt get here
|
||||
@ -519,19 +474,37 @@ final class Server
|
||||
throw new InvalidUsage('Invalid request to fetch schema: '.$item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return is_null($key) ? $result : $result->get($key);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an OID, return the ldapsyntax for the OID
|
||||
* Get the Schema DN
|
||||
*
|
||||
* @param string $oid
|
||||
* @return LDAPSyntax|null
|
||||
* @throws InvalidUsage
|
||||
* @return string
|
||||
* @throws ObjectNotFoundException
|
||||
*/
|
||||
public function schemaSyntaxName(string $oid): ?LDAPSyntax
|
||||
public function schemaDN(): string
|
||||
{
|
||||
return $this->schema('ldapsyntaxes',$oid);
|
||||
return Arr::get($this->rootDSE->subschemasubentry,0);
|
||||
}
|
||||
}
|
450
app/Classes/Template.php
Normal file
450
app/Classes/Template.php
Normal file
@ -0,0 +1,450 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
@ -10,8 +10,10 @@ use Illuminate\Support\Collection;
|
||||
|
||||
use App\Classes\LDAP\Server;
|
||||
|
||||
class APIController extends Controller
|
||||
class AjaxController extends Controller
|
||||
{
|
||||
private const LOGKEY = 'CAc';
|
||||
|
||||
/**
|
||||
* Get the LDAP server BASE DNs
|
||||
*
|
||||
@ -20,17 +22,14 @@ class APIController extends Controller
|
||||
*/
|
||||
public function bases(): Collection
|
||||
{
|
||||
$base = Server::baseDNs() ?: collect();
|
||||
|
||||
return $base
|
||||
->transform(fn($item)=>
|
||||
[
|
||||
return Server::baseDNs()
|
||||
->map(fn($item)=> [
|
||||
'title'=>$item->getRdn(),
|
||||
'item'=>$item->getDNSecure(),
|
||||
'lazy'=>TRUE,
|
||||
'icon'=>'fa-fw fas fa-sitemap',
|
||||
'tooltip'=>$item->getDn(),
|
||||
]);
|
||||
])->values();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -39,14 +38,13 @@ class APIController extends Controller
|
||||
*/
|
||||
public function children(Request $request): Collection
|
||||
{
|
||||
$levels = $request->query('depth',1);
|
||||
$dn = Crypt::decryptString($request->query('key'));
|
||||
$dn = Crypt::decryptString($request->post('_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] - Levels [%d]',__METHOD__,$dn,$levels));
|
||||
Log::debug(sprintf('%s:Query [%s]',self::LOGKEY,$dn));
|
||||
|
||||
return (config('server'))
|
||||
->children($dn)
|
||||
@ -59,13 +57,18 @@ class APIController 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();
|
||||
}
|
||||
|
||||
public function schema_view(Request $request)
|
||||
@ -98,11 +101,10 @@ class APIController 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(Request $request,string $objectclass): array
|
||||
public function schema_objectclass_attrs(string $objectclass): array
|
||||
{
|
||||
$oc = config('server')->schema('objectclasses',$objectclass);
|
||||
|
@ -5,9 +5,13 @@ namespace App\Http\Controllers\Auth;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cookie;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use LdapRecord\Auth\BindException;
|
||||
use LdapRecord\Container;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Ldap\Entry;
|
||||
|
||||
class LoginController extends Controller
|
||||
{
|
||||
@ -38,7 +42,8 @@ class LoginController extends Controller
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest')->except('logout');
|
||||
$this->middleware('guest')
|
||||
->except('logout');
|
||||
}
|
||||
|
||||
protected function credentials(Request $request): array
|
||||
@ -49,6 +54,45 @@ 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
|
||||
*
|
||||
@ -58,17 +102,14 @@ class LoginController extends Controller
|
||||
*/
|
||||
public function logout(Request $request)
|
||||
{
|
||||
// Delete our LDAP authentication cookies
|
||||
Cookie::queue(Cookie::forget('username_encrypt'));
|
||||
Cookie::queue(Cookie::forget('password_encrypt'));
|
||||
$user = Auth::user();
|
||||
|
||||
$this->guard()->logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
if ($response = $this->loggedOut($request)) {
|
||||
Log::info(sprintf('Logged out [%s]',$user->dn));
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
@ -2,88 +2,83 @@
|
||||
|
||||
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\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 function bases(): Collection
|
||||
{
|
||||
$base = Server::baseDNs() ?: collect();
|
||||
private const LOGKEY = 'CHc';
|
||||
|
||||
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');
|
||||
}
|
||||
private const INTERNAL_POST = ['_auto_value','_key','_rdn','_rdn_new','_rdn_value','_step','_template','_token','_userpassword_hash'];
|
||||
|
||||
/**
|
||||
* Create a new object in the LDAP server
|
||||
*
|
||||
* @param EntryAddRequest $request
|
||||
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
|
||||
* @return \Illuminate\View\View
|
||||
* @throws InvalidUsage
|
||||
*/
|
||||
public function entry_add(EntryAddRequest $request)
|
||||
public function entry_add(EntryAddRequest $request): \Illuminate\View\View
|
||||
{
|
||||
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;
|
||||
|
||||
if (count(array_filter($x=old('objectclass',$request->objectclass)))) {
|
||||
$o->objectclass = $x;
|
||||
|
||||
foreach($o->getAvailableAttributes()->filter(fn($item)=>$item->required) as $ao)
|
||||
$o->addAttribute($ao,'');
|
||||
|
||||
$o->setRDNBase($key['dn']);
|
||||
|
||||
foreach (collect(old())->except(self::INTERNAL_POST) as $old => $value)
|
||||
$o->{$old} = array_filter($value);
|
||||
|
||||
if (old('_template',$request->validated('template'))) {
|
||||
$template = $o->template(old('_template',$request->validated('template')));
|
||||
|
||||
$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=>''];
|
||||
}
|
||||
|
||||
$step = $request->step ? $request->step+1 : old('step');
|
||||
} 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=>''];
|
||||
}
|
||||
|
||||
$step = $request->get('_step') ? $request->get('_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']));
|
||||
}
|
||||
|
||||
@ -92,35 +87,50 @@ class HomeController extends Controller
|
||||
*
|
||||
* @param Request $request
|
||||
* @param string $id
|
||||
* @return \Closure|\Illuminate\Contracts\View\View|string
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function entry_attr_add(Request $request,string $id): string
|
||||
public function entry_attr_add(Request $request,string $id): \Illuminate\View\View
|
||||
{
|
||||
$xx = new \stdClass;
|
||||
$xx->index = 0;
|
||||
|
||||
$x = $request->noheader
|
||||
? (string)view(sprintf('components.attribute.widget.%s',$id))
|
||||
->with('o',Factory::create($id,[]))
|
||||
$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))
|
||||
->with('value',$request->value)
|
||||
->with('loop',$xx)
|
||||
: (new AttributeType(Factory::create($id,[]),TRUE,collect($request->oc ?: [])))->render();
|
||||
: view('components.attribute-type')
|
||||
->with('new',TRUE)
|
||||
->with('edit',TRUE);
|
||||
|
||||
return $x;
|
||||
return $view
|
||||
->with('o',$o)
|
||||
->with('langtag',Entry::TAG_NOTAG)
|
||||
->with('template',NULL)
|
||||
->with('updated',FALSE);
|
||||
}
|
||||
|
||||
public function entry_create(EntryAddRequest $request)
|
||||
public function entry_create(EntryAddRequest $request): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
$key = $this->request_key($request,collect(old()));
|
||||
|
||||
$dn = sprintf('%s=%s,%s',$request->rdn,$request->rdn_value,$key['dn']);
|
||||
$dn = sprintf('%s=%s,%s',$request->get('_rdn'),$request->get('_rdn_value'),$key['dn']);
|
||||
|
||||
$o = new Entry;
|
||||
$o->setDn($dn);
|
||||
|
||||
foreach ($request->except(['_token','key','step','rdn','rdn_value']) as $key => $value)
|
||||
foreach ($request->except(self::INTERNAL_POST) 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();
|
||||
|
||||
@ -131,7 +141,7 @@ class HomeController extends Controller
|
||||
case 50:
|
||||
return Redirect::to('/')
|
||||
->withInput()
|
||||
->withErrors(sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
|
||||
->with('failed',sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
|
||||
|
||||
default:
|
||||
abort(599,$e->getDetailedError()->getErrorMessage());
|
||||
@ -139,24 +149,27 @@ class HomeController extends Controller
|
||||
|
||||
// @todo when we create an entry, and it already exists, enable a redirect to it
|
||||
} catch (LdapRecordException $e) {
|
||||
$request->flash();
|
||||
|
||||
switch ($x=$e->getDetailedError()->getErrorCode()) {
|
||||
case 8:
|
||||
return Redirect::to('/')
|
||||
return Redirect::back()
|
||||
->withInput()
|
||||
->withErrors(sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
|
||||
|
||||
default:
|
||||
abort(599,$e->getDetailedError()->getErrorMessage());
|
||||
->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)
|
||||
public function entry_delete(Request $request): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
$dn = Crypt::decryptString($request->dn);
|
||||
|
||||
@ -172,7 +185,7 @@ class HomeController extends Controller
|
||||
case 50:
|
||||
return Redirect::to('/')
|
||||
->withInput()
|
||||
->withErrors(sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
|
||||
->with('failed',sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
|
||||
|
||||
default:
|
||||
abort(599,$e->getDetailedError()->getErrorMessage());
|
||||
@ -185,7 +198,7 @@ class HomeController extends Controller
|
||||
case 8:
|
||||
return Redirect::to('/')
|
||||
->withInput()
|
||||
->withErrors(sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
|
||||
->with('failed',sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
|
||||
|
||||
default:
|
||||
abort(599,$e->getDetailedError()->getErrorMessage());
|
||||
@ -193,17 +206,14 @@ class HomeController extends Controller
|
||||
}
|
||||
|
||||
return Redirect::to('/')
|
||||
->with('success',[sprintf('%s: %s',__('Deleted'),$dn)]);
|
||||
->with('success',sprintf('%s: %s',__('Deleted'),$dn));
|
||||
}
|
||||
|
||||
public function entry_export(Request $request,string $id)
|
||||
public function entry_export(Request $request,string $id): \Illuminate\View\View
|
||||
{
|
||||
$dn = Crypt::decryptString($id);
|
||||
|
||||
$result = (new Entry)
|
||||
->query()
|
||||
//->cache(Carbon::now()->addSeconds(Config::get('ldap.cache.time')))
|
||||
//->select(['*'])
|
||||
$result = Entry::query()
|
||||
->setDn($dn)
|
||||
->recursive()
|
||||
->get();
|
||||
@ -215,12 +225,13 @@ class HomeController extends Controller
|
||||
/**
|
||||
* Render an available list of objectclasses for an Entry
|
||||
*
|
||||
* @param string $id
|
||||
* @return mixed
|
||||
* @param Request $request
|
||||
* @return Collection
|
||||
*/
|
||||
public function entry_objectclass_add(Request $request)
|
||||
public function entry_objectclass_add(Request $request): Collection
|
||||
{
|
||||
$oc = Factory::create('objectclass',$request->oc);
|
||||
$dn = $request->get('_key') ? Crypt::decryptString($request->dn) : '';
|
||||
$oc = Factory::create($dn,'objectclass',$request->oc);
|
||||
|
||||
$ocs = $oc
|
||||
->structural
|
||||
@ -242,7 +253,7 @@ class HomeController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function entry_password_check(Request $request)
|
||||
public function entry_password_check(Request $request): Collection
|
||||
{
|
||||
$dn = Crypt::decryptString($request->dn);
|
||||
$o = config('server')->fetch($dn);
|
||||
@ -250,7 +261,7 @@ class HomeController extends Controller
|
||||
$password = $o->getObject('userpassword');
|
||||
|
||||
$result = collect();
|
||||
foreach ($password as $key => $value) {
|
||||
foreach ($password->values->dot() 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]);
|
||||
@ -265,55 +276,72 @@ class HomeController extends Controller
|
||||
* Show a confirmation to update a DN
|
||||
*
|
||||
* @param EntryRequest $request
|
||||
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Foundation\Application|\Illuminate\Http\RedirectResponse
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||
* @throws ObjectNotFoundException
|
||||
*/
|
||||
public function entry_pending_update(EntryRequest $request)
|
||||
public function entry_pending_update(EntryRequest $request): \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||
{
|
||||
$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));
|
||||
|
||||
// We need to process and encrypt the password
|
||||
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;
|
||||
}
|
||||
// @todo Need to handle incoming attributes that were modified by MD5Updates Trait (eg: jpegphoto)
|
||||
|
||||
if ($value) {
|
||||
$type = Arr::get($request->userpassword_hash,$key);
|
||||
array_push($passwords,Password::hash_id($type)->encode($value));
|
||||
}
|
||||
}
|
||||
$o->userpassword = $passwords;
|
||||
}
|
||||
// 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 (! $o->getDirty())
|
||||
return back()
|
||||
return Redirect::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)
|
||||
public function entry_update(EntryRequest $request): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
$dn = Crypt::decryptString($request->dn);
|
||||
|
||||
@ -323,7 +351,7 @@ class HomeController extends Controller
|
||||
$o->{$key} = array_filter($value);
|
||||
|
||||
if (! $dirty=$o->getDirty())
|
||||
return back()
|
||||
return Redirect::back()
|
||||
->withInput()
|
||||
->with('note',__('No attributes changed'));
|
||||
|
||||
@ -337,29 +365,29 @@ class HomeController extends Controller
|
||||
case 50:
|
||||
return Redirect::to('/')
|
||||
->withInput()
|
||||
->withErrors(sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
|
||||
->with('failed',sprintf('%s: %s (%s)',__('LDAP Server Error Code'),$x,__($e->getDetailedError()->getErrorMessage())));
|
||||
|
||||
default:
|
||||
abort(599,$e->getDetailedError()->getErrorMessage());
|
||||
}
|
||||
|
||||
} catch (LdapRecordException $e) {
|
||||
$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());
|
||||
}
|
||||
->with('failed',sprintf('%s: %s - %s: %s',
|
||||
__('LDAP Server Error Code'),
|
||||
$e->getDetailedError()->getErrorCode(),
|
||||
__($e->getDetailedError()->getErrorMessage()),
|
||||
$e->getDetailedError()->getDiagnosticMessage(),
|
||||
));
|
||||
}
|
||||
|
||||
return Redirect::to('/')
|
||||
->withInput()
|
||||
->with('updated',collect($dirty)->map(fn($key,$item)=>$o->getObject($item)));
|
||||
->with('updated',collect($dirty)
|
||||
->map(fn($item,$key)=>$o->getObject(collect(explode(';',$key))->first()))
|
||||
->values()
|
||||
->unique());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -368,9 +396,10 @@ class HomeController extends Controller
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Collection|null $old
|
||||
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|View
|
||||
* @return \Illuminate\View\View
|
||||
* @throws InvalidUsage
|
||||
*/
|
||||
public function frame(Request $request,?Collection $old=NULL): View
|
||||
public function frame(Request $request,?Collection $old=NULL): \Illuminate\View\View
|
||||
{
|
||||
// If our index was not render from a root url, then redirect to it
|
||||
if (($request->root().'/' !== url()->previous()) && $request->method() === 'POST')
|
||||
@ -378,19 +407,40 @@ class HomeController extends Controller
|
||||
|
||||
$key = $this->request_key($request,$old);
|
||||
|
||||
$view = ($old
|
||||
$view = $old
|
||||
? view('frame')->with('subframe',$key['cmd'])
|
||||
: view('frames.'.$key['cmd']))
|
||||
->with('bases',$this->bases());
|
||||
: 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;
|
||||
}
|
||||
|
||||
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('page_actions',collect(['edit'=>TRUE,'copy'=>TRUE])),
|
||||
->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,
|
||||
])),
|
||||
|
||||
'import' => $view,
|
||||
|
||||
@ -401,13 +451,12 @@ class HomeController extends Controller
|
||||
/**
|
||||
* This is the main page render function
|
||||
*/
|
||||
public function home(Request $request)
|
||||
public function home(Request $request): \Illuminate\View\View
|
||||
{
|
||||
// Did we come here as a result of a redirect
|
||||
return count(old())
|
||||
? $this->frame($request,collect(old()))
|
||||
: view('home')
|
||||
->with('bases',$this->bases());
|
||||
: view('home');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -415,15 +464,16 @@ class HomeController extends Controller
|
||||
*
|
||||
* @param ImportRequest $request
|
||||
* @param string $type
|
||||
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Foundation\Application
|
||||
* @return \Illuminate\View\View
|
||||
* @throws GeneralException
|
||||
* @throws VersionException
|
||||
*/
|
||||
public function import(ImportRequest $request,string $type)
|
||||
public function import(ImportRequest $request,string $type): \Illuminate\View\View
|
||||
{
|
||||
switch ($type) {
|
||||
case 'ldif':
|
||||
$import = new LDIFImport($x=($request->text ?: $request->file->get()));
|
||||
Log::debug('Processing LDIF import',['data'=>$x,'import'=>$import]);
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -442,25 +492,30 @@ class HomeController extends Controller
|
||||
|
||||
return view('frame')
|
||||
->with('subframe','import_result')
|
||||
->with('bases',$this->bases())
|
||||
->with('result',$result)
|
||||
->with('ldif',htmlspecialchars($x));
|
||||
}
|
||||
|
||||
public function import_frame()
|
||||
private function password(Password $po,array $values,array $hash): array
|
||||
{
|
||||
return view('frames.import');
|
||||
// We need to process and encrypt the password
|
||||
$passwords = [];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'));
|
||||
if ($value) {
|
||||
$type = Arr::get($hash,$dotkey);
|
||||
$passwords[$dotkey] = Password::hash_id($type)
|
||||
->encode($value);
|
||||
}
|
||||
}
|
||||
|
||||
return Arr::undot($passwords);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -475,8 +530,8 @@ class HomeController extends Controller
|
||||
// Setup
|
||||
$cmd = NULL;
|
||||
$dn = NULL;
|
||||
$key = $request->get('key',old('key'))
|
||||
? Crypt::decryptString($request->get('key',old('key')))
|
||||
$key = ($x=$request->get('_key',old('_key')))
|
||||
? Crypt::decryptString($x)
|
||||
: NULL;
|
||||
|
||||
// Determine if our key has a command
|
||||
@ -488,9 +543,9 @@ class HomeController extends Controller
|
||||
$dn = ($m[2] !== '_NOP') ? $m[2] : NULL;
|
||||
}
|
||||
|
||||
} elseif (old('dn',$request->get('key'))) {
|
||||
} elseif ($x=old('dn',$request->get('_key'))) {
|
||||
$cmd = 'dn';
|
||||
$dn = Crypt::decryptString(old('dn',$request->get('key')));
|
||||
$dn = Crypt::decryptString($x);
|
||||
}
|
||||
|
||||
return ['cmd'=>$cmd,'dn'=>$dn];
|
||||
@ -501,18 +556,18 @@ class HomeController extends Controller
|
||||
*
|
||||
* @note Our route will validate that types are valid.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
|
||||
* @return \Illuminate\View\View
|
||||
* @throws InvalidUsage
|
||||
*/
|
||||
public function schema_frame(Request $request)
|
||||
public function schema_frame(Request $request): \Illuminate\View\View
|
||||
{
|
||||
// If an invalid key, we'll 404
|
||||
if ($request->type && $request->key && (! config('server')->schema($request->type)->has($request->key)))
|
||||
if ($request->type && $request->get('_key') && (! config('server')->schema($request->type)->has($request->get('_key'))))
|
||||
abort(404);
|
||||
|
||||
return view('frames.schema')
|
||||
->with('type',$request->type)
|
||||
->with('key',$request->key);
|
||||
->with('key',$request->get('_key'));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -530,9 +585,9 @@ class HomeController extends Controller
|
||||
* Return the image for the logged in user or anonymous
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function user_image(Request $request)
|
||||
public function user_image(Request $request): \Illuminate\Http\Response
|
||||
{
|
||||
$image = NULL;
|
||||
$content = NULL;
|
||||
|
59
app/Http/Controllers/SearchController.php
Normal file
59
app/Http/Controllers/SearchController.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
use App\Ldap\Entry;
|
||||
|
||||
class SearchController extends Controller
|
||||
{
|
||||
public function search(Request $request): Collection
|
||||
{
|
||||
$so = config('server');
|
||||
|
||||
// We are searching for a value
|
||||
if (strpos($request->term,'=')) {
|
||||
list($attr,$value) = explode('=',$request->term,2);
|
||||
$value = trim($value);
|
||||
|
||||
$result = collect();
|
||||
|
||||
foreach ($so->baseDNs(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();
|
||||
}
|
||||
}
|
||||
}
|
46
app/Http/Middleware/AcceptLanguage.php
Normal file
46
app/Http/Middleware/AcceptLanguage.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cookie;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
class AllowAnonymous
|
||||
{
|
||||
@ -17,7 +17,9 @@ class AllowAnonymous
|
||||
*/
|
||||
public function handle(Request $request,Closure $next): mixed
|
||||
{
|
||||
if (((! Cookie::has('username_encrypt')) || (! Cookie::has('password_encrypt'))) && (! config('pla.allow_guest',FALSE)))
|
||||
if ((! config('pla.allow_guest',FALSE))
|
||||
&& ($request->path() !== 'login')
|
||||
&& ((! Session::has('username_encrypt')) || (! Session::has('password_encrypt'))))
|
||||
return redirect()
|
||||
->to('/login');
|
||||
|
||||
|
@ -2,14 +2,16 @@
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Classes\LDAP\Server;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Cookie;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use LdapRecord\Container;
|
||||
|
||||
use App\Ldap\Connection;
|
||||
use App\Ldap\Guard;
|
||||
|
||||
class SwapinAuthUser
|
||||
{
|
||||
@ -28,25 +30,21 @@ class SwapinAuthUser
|
||||
if (! array_key_exists($key,config('ldap.connections')))
|
||||
abort(599,sprintf('LDAP default server [%s] configuration doesnt exist?',$key));
|
||||
|
||||
/*
|
||||
// Rebuild our connection with the authenticated user.
|
||||
if (Session::has('username_encrypt') && Session::has('password_encrypt')) {
|
||||
if (($request->path() !== 'logout') && Session::has('username_encrypt') && Session::has('password_encrypt')) {
|
||||
Config::set('ldap.connections.'.$key.'.username',Crypt::decryptString(Session::get('username_encrypt')));
|
||||
Config::set('ldap.connections.'.$key.'.password',Crypt::decryptString(Session::get('password_encrypt')));
|
||||
|
||||
} 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')]);
|
||||
Log::debug('Swapping out configured LDAP credentials with the user\'s session.',['key'=>$key]);
|
||||
}
|
||||
|
||||
// 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.
|
||||
Container::getInstance()->addConnection(new Connection(config('ldap.connections.'.$key)),$key);
|
||||
$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);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
@ -6,13 +6,12 @@ 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 ApplicationSession
|
||||
class ViewVariables
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
@ -23,8 +22,7 @@ class ApplicationSession
|
||||
*/
|
||||
public function handle(Request $request,Closure $next): mixed
|
||||
{
|
||||
Config::set('server',new Server);
|
||||
|
||||
view()->share('server',Config::get('server'));
|
||||
view()->share('user',auth()->user() ?: new User);
|
||||
|
||||
return $next($request);
|
@ -3,12 +3,17 @@
|
||||
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.
|
||||
*
|
||||
@ -17,8 +22,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.'),
|
||||
];
|
||||
}
|
||||
|
||||
@ -34,14 +39,24 @@ class EntryAddRequest extends FormRequest
|
||||
if (request()->method() === 'GET')
|
||||
return [];
|
||||
|
||||
$r = request() ?: collect();
|
||||
$rk = array_keys($r->all());
|
||||
|
||||
return config('server')
|
||||
->schema('attributetypes')
|
||||
->intersectByKeys($this->request)
|
||||
->map(fn($item)=>$item->validation(request()->get('objectclass')))
|
||||
->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',[])))
|
||||
->filter()
|
||||
->flatMap(fn($item)=>$item)
|
||||
->merge([
|
||||
'key' => [
|
||||
'_key' => [
|
||||
'required',
|
||||
new DNExists,
|
||||
function (string $attribute,mixed $value,\Closure $fail) {
|
||||
@ -56,14 +71,68 @@ 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();
|
||||
|
@ -10,13 +10,25 @@ 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')
|
||||
->intersectByKeys($this->request)
|
||||
->map(fn($item)=>$item->validation(request()?->get('objectclass') ?: []))
|
||||
->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',[])))
|
||||
->filter()
|
||||
->flatMap(fn($item)=>$item)
|
||||
->toArray();
|
||||
|
@ -1,20 +0,0 @@
|
||||
<?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);
|
||||
};
|
||||
}
|
||||
}
|
@ -3,21 +3,40 @@
|
||||
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;
|
||||
private bool $noObjectAttributes = FALSE;
|
||||
|
||||
// Templates that apply to this entry
|
||||
private(set) Collection $templates;
|
||||
|
||||
// For new entries, this is the container that this entry will be stored in
|
||||
private string $rdnbase;
|
||||
|
||||
@ -25,9 +44,31 @@ class Entry extends Model
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
parent::__construct($attributes);
|
||||
|
||||
$this->objects = collect();
|
||||
|
||||
parent::__construct($attributes);
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
|
||||
public function discardChanges(): static
|
||||
@ -43,46 +84,39 @@ 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
|
||||
->map(fn($item)=>$item->values)
|
||||
->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]))
|
||||
->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);
|
||||
|
||||
// @todo Silently ignore keys of language tags - we should work with them
|
||||
if (str_contains($key,';'))
|
||||
return TRUE;
|
||||
list($attribute,$tag) = $this->keytag($key);
|
||||
|
||||
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();
|
||||
return ((! array_key_exists($key,$this->original)) && (! $this->objects->has($attribute)))
|
||||
|| (! $this->getObject($attribute)->isDirty());
|
||||
}
|
||||
|
||||
/**
|
||||
* As attribute values are updated, or new ones created, we need to mirror that
|
||||
* into our $objects
|
||||
* into our $objects. This is called when we $o->key = $value
|
||||
*
|
||||
* This function should update $this->attributes and correctly reflect changes in $this->objects
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
@ -90,16 +124,16 @@ class Entry extends Model
|
||||
*/
|
||||
public function setAttribute(string $key,mixed $value): static
|
||||
{
|
||||
parent::setAttribute($key,$value);
|
||||
foreach ($value as $k => $v)
|
||||
parent::setAttribute($key.($k !== self::TAG_NOTAG ? ';'.$k : ''),$v);
|
||||
|
||||
$key = $this->normalizeAttributeKey($key);
|
||||
list($attribute,$tags) = $this->keytag($key);
|
||||
|
||||
if ((! $this->objects->get($key)) && $value) {
|
||||
$this->objects->put($key,Factory::create($key,$value));
|
||||
$o = $this->objects->get($attribute) ?: Factory::create($this->dn ?: '',$attribute,[],Arr::get($this->attributes,'objectclass',[]));
|
||||
$o->values = collect($value);
|
||||
|
||||
} elseif ($this->objects->get($key)) {
|
||||
$this->objects->get($key)->value = $this->attributes[$key];
|
||||
}
|
||||
$this->objects->put($key,$o);
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -124,6 +158,17 @@ 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;
|
||||
}
|
||||
|
||||
@ -133,69 +178,106 @@ 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 $this->getDn();
|
||||
return collect(explode(',',$this->getDn()))->reverse()->join(',');
|
||||
}
|
||||
|
||||
/* METHODS */
|
||||
|
||||
public function addAttribute(string $key,mixed $value): void
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
// 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($key);
|
||||
$key = $this->normalizeAttributeKey(strtolower($key));
|
||||
|
||||
if (! config('server')->schema('attributetypes')->has($key))
|
||||
throw new AttributeException(sprintf('Schema doesnt have attribute [%s]',$key));
|
||||
// If the attribute name has tags
|
||||
list($attribute,$tag) = $this->keytag($key);
|
||||
|
||||
if ($x=$this->objects->get($key)) {
|
||||
$x->addValue($value);
|
||||
if (config('server')->get_attr_id($attribute) === FALSE)
|
||||
throw new AttributeException(sprintf('Schema doesnt have attribute [%s]',$attribute));
|
||||
|
||||
} else {
|
||||
$this->objects->put($key,Attribute\Factory::create($key,Arr::wrap($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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert all our attribute values into an array of Objects
|
||||
*
|
||||
* @param array $attributes
|
||||
* @return Collection
|
||||
*/
|
||||
public function getAttributesAsObjects(): Collection
|
||||
private function getAttributesAsObjects(): Collection
|
||||
{
|
||||
$result = collect();
|
||||
$entry_oc = Arr::get($this->attributes,'objectclass',[]);
|
||||
|
||||
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];
|
||||
foreach ($this->attributes as $attrtag => $values) {
|
||||
list($attribute,$tags) = $this->keytag($attrtag);
|
||||
|
||||
$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);
|
||||
$o = Arr::get(
|
||||
$result,
|
||||
$attribute,
|
||||
Factory::create(
|
||||
$this->dn,
|
||||
$attribute,
|
||||
[$tags=>$orig],
|
||||
$entry_oc,
|
||||
));
|
||||
|
||||
} else {
|
||||
$o = Factory::create($attribute,$value);
|
||||
}
|
||||
|
||||
if (! $result->has($attribute)) {
|
||||
// Set the rdn flag
|
||||
if (preg_match('/^'.$attribute.'=/i',$this->dn))
|
||||
$o->setRDN();
|
||||
|
||||
// Store our original value to know if this attribute has changed
|
||||
$o->oldValues(Arr::get($this->original,$attribute,[]));
|
||||
$o->addValue($tags,$values);
|
||||
$o->addValueOld($tags,Arr::get($this->original,$attrtag));
|
||||
|
||||
$result->put($attribute,$o);
|
||||
}
|
||||
}
|
||||
|
||||
$sort = collect(config('pla.attr_display_order',[]))->map(fn($item)=>strtolower($item));
|
||||
|
||||
@ -235,8 +317,8 @@ class Entry extends Model
|
||||
{
|
||||
$result = collect();
|
||||
|
||||
foreach ($this->objectclass as $oc)
|
||||
$result = $result->merge(config('server')->schema('objectclasses',$oc)->attributes);
|
||||
foreach (($this->getObject('objectclass')?->values ?: []) as $oc)
|
||||
$result = $result->merge(config('server')->schema('objectclasses',$oc)->all_attributes);
|
||||
|
||||
return $result;
|
||||
}
|
||||
@ -262,6 +344,31 @@ 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
|
||||
*
|
||||
@ -287,6 +394,29 @@ 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
|
||||
*
|
||||
@ -300,10 +430,10 @@ class Entry extends Model
|
||||
|
||||
private function getRDNObject(): Attribute\RDN
|
||||
{
|
||||
$o = new Attribute\RDN('dn',['']);
|
||||
// @todo for an existing object, return the base.
|
||||
$o = new Attribute\RDN('','dn',['']);
|
||||
// @todo for an existing object, rdnbase would be null, so dynamically get it from the DN.
|
||||
$o->setBase($this->rdnbase);
|
||||
$o->setAttributes($this->getAvailableAttributes()->filter(fn($item)=>$item->required));
|
||||
$o->setAttributes($this->getAvailableAttributes()->filter(fn($item)=>$item->is_must));
|
||||
|
||||
return $o;
|
||||
}
|
||||
@ -311,12 +441,22 @@ class Entry extends Model
|
||||
/**
|
||||
* Return this list of user attributes
|
||||
*
|
||||
* @param string $tag If null return all tags
|
||||
* @return Collection
|
||||
*/
|
||||
public function getVisibleAttributes(): Collection
|
||||
public function getVisibleAttributes(string $tag=''): Collection
|
||||
{
|
||||
return $this->objects
|
||||
->filter(fn($item)=>! $item->is_internal);
|
||||
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_'];
|
||||
}
|
||||
|
||||
public function hasAttribute(int|string $key): bool
|
||||
@ -326,33 +466,18 @@ class Entry extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Export this record
|
||||
* Did this query generate a size limit exception
|
||||
*
|
||||
* @param string $method
|
||||
* @param string $scope
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
* @return bool
|
||||
* @throws \LdapRecord\ContainerException
|
||||
*/
|
||||
public function export(string $method,string $scope): string
|
||||
public function hasMore(): bool
|
||||
{
|
||||
// @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);
|
||||
}
|
||||
return $this->getConnectionContainer()
|
||||
->getConnection()
|
||||
->getLdapConnection()
|
||||
->getDetailedError()
|
||||
?->getErrorCode() === 4;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -362,59 +487,65 @@ class Entry extends Model
|
||||
*/
|
||||
public function icon(): string
|
||||
{
|
||||
$objectclasses = array_map('strtolower',$this->objectclass);
|
||||
$objectclasses = ($x=$this->getObject('objectclass'))
|
||||
? $x->tagValues()
|
||||
->map(fn($item)=>strtolower($item))
|
||||
: collect();
|
||||
|
||||
// Return icon based upon objectClass value
|
||||
if (in_array('person',$objectclasses) ||
|
||||
in_array('organizationalperson',$objectclasses) ||
|
||||
in_array('inetorgperson',$objectclasses) ||
|
||||
in_array('account',$objectclasses) ||
|
||||
in_array('posixaccount',$objectclasses))
|
||||
|
||||
if ($objectclasses->intersect([
|
||||
'account',
|
||||
'inetorgperson',
|
||||
'organizationalperson',
|
||||
'person',
|
||||
'posixaccount',
|
||||
])->count())
|
||||
return 'fas fa-user';
|
||||
|
||||
elseif (in_array('organization',$objectclasses))
|
||||
elseif ($objectclasses->contains('organization'))
|
||||
return 'fas fa-university';
|
||||
|
||||
elseif (in_array('organizationalunit',$objectclasses))
|
||||
elseif ($objectclasses->contains('organizationalunit'))
|
||||
return 'fas fa-object-group';
|
||||
|
||||
elseif (in_array('posixgroup',$objectclasses) ||
|
||||
in_array('groupofnames',$objectclasses) ||
|
||||
in_array('groupofuniquenames',$objectclasses) ||
|
||||
in_array('group',$objectclasses))
|
||||
|
||||
elseif ($objectclasses->intersect([
|
||||
'posixgroup',
|
||||
'groupofnames',
|
||||
'groupofuniquenames',
|
||||
'group',
|
||||
])->count())
|
||||
return 'fas fa-users';
|
||||
|
||||
elseif (in_array('dcobject',$objectclasses) ||
|
||||
in_array('domainrelatedobject',$objectclasses) ||
|
||||
in_array('domain',$objectclasses) ||
|
||||
in_array('builtindomain',$objectclasses))
|
||||
|
||||
elseif ($objectclasses->intersect([
|
||||
'dcobject',
|
||||
'domainrelatedobject',
|
||||
'domain',
|
||||
'builtindomain',
|
||||
])->count())
|
||||
return 'fas fa-network-wired';
|
||||
|
||||
elseif (in_array('alias',$objectclasses))
|
||||
elseif ($objectclasses->contains('alias'))
|
||||
return 'fas fa-theater-masks';
|
||||
|
||||
elseif (in_array('country',$objectclasses))
|
||||
return sprintf('flag %s',strtolower(Arr::get($this->c,0)));
|
||||
elseif ($objectclasses->contains('country'))
|
||||
return sprintf('flag %s',strtolower(Arr::get($this->c ?: [],0)));
|
||||
|
||||
elseif (in_array('device',$objectclasses))
|
||||
elseif ($objectclasses->contains('device'))
|
||||
return 'fas fa-mobile-alt';
|
||||
|
||||
elseif (in_array('document',$objectclasses))
|
||||
elseif ($objectclasses->contains('document'))
|
||||
return 'fas fa-file-alt';
|
||||
|
||||
elseif (in_array('iphost',$objectclasses))
|
||||
elseif ($objectclasses->contains('iphost'))
|
||||
return 'fas fa-wifi';
|
||||
|
||||
elseif (in_array('room',$objectclasses))
|
||||
elseif ($objectclasses->contains('room'))
|
||||
return 'fas fa-door-open';
|
||||
|
||||
elseif (in_array('server',$objectclasses))
|
||||
elseif ($objectclasses->contains('server'))
|
||||
return 'fas fa-server';
|
||||
|
||||
elseif (in_array('openldaprootdse',$objectclasses))
|
||||
elseif ($objectclasses->contains('openldaprootdse'))
|
||||
return 'fas fa-info';
|
||||
|
||||
// Default
|
||||
@ -422,15 +553,25 @@ class Entry extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Dont convert our $this->attributes to $this->objects when creating a new Entry::class
|
||||
* Given an LDAP attribute, this will return the attribute name and the tag
|
||||
* eg: description;lang-cn will return [description,lang-cn]
|
||||
*
|
||||
* @return $this
|
||||
* @param string $key
|
||||
* @return array
|
||||
*/
|
||||
public function noObjectAttributes(): static
|
||||
private function keytag(string $key): array
|
||||
{
|
||||
$this->noObjectAttributes = TRUE;
|
||||
$matches = [];
|
||||
if (preg_match(sprintf('/^([%s]+);+([%s;]+)/',self::TAG_CHARS,self::TAG_CHARS),$key,$matches)) {
|
||||
$attribute = $matches[1];
|
||||
$tags = $matches[2];
|
||||
|
||||
return $this;
|
||||
} else {
|
||||
$attribute = $key;
|
||||
$tags = self::TAG_NOTAG;
|
||||
}
|
||||
|
||||
return [$attribute,$tags];
|
||||
}
|
||||
|
||||
public function setRDNBase(string $bdn): void
|
||||
@ -439,5 +580,13 @@ 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);
|
||||
}
|
||||
}
|
@ -2,26 +2,20 @@
|
||||
|
||||
namespace App\Ldap;
|
||||
|
||||
use Illuminate\Support\Facades\Cookie;
|
||||
// use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use LdapRecord\Auth\Guard as GuardBase;
|
||||
|
||||
class Guard extends GuardBase
|
||||
{
|
||||
public function attempt(string $username, string $password, bool $stayBound = false): bool
|
||||
{
|
||||
if ($result = parent::attempt($username,$password,$stayBound)) {
|
||||
/*
|
||||
* 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));
|
||||
*/
|
||||
Log::info(sprintf('Attempting login for [%s] with password [%s]',$username,($password ? str_repeat('*',16) : str_repeat('?',16))));
|
||||
|
||||
// For our API calls, we store the cookie - which our cookies are already encrypted
|
||||
Cookie::queue('username_encrypt',$username);
|
||||
Cookie::queue('password_encrypt',$password);
|
||||
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));
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
@ -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() as $base) {
|
||||
foreach (Server::baseDNs(FALSE) 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() as $base) {
|
||||
foreach (Server::baseDNs(FALSE) as $base) {
|
||||
$user = $this->query()->setBaseDn($base)->findByGuid($guid);
|
||||
|
||||
if ($user)
|
||||
|
@ -14,10 +14,13 @@ 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($user->objectclass,$x));
|
||||
return count(array_intersect(
|
||||
array_map('strtolower',$user?->objectclass ?: []),
|
||||
array_map('strtolower',$x)
|
||||
));
|
||||
|
||||
// Otherwise allow the user to login
|
||||
} else {
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use LdapRecord\Configuration\DomainConfiguration;
|
||||
use LdapRecord\Laravel\LdapRecord;
|
||||
@ -29,11 +28,5 @@ 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());
|
||||
}
|
||||
}
|
@ -20,10 +20,11 @@ class HasStructuralObjectClass implements ValidationRule
|
||||
*/
|
||||
public function validate(string $attribute,mixed $value,Closure $fail): void
|
||||
{
|
||||
foreach ($value as $item)
|
||||
foreach (collect($value)->dot() as $item)
|
||||
if ($item && config('server')->schema('objectclasses',$item)->isStructural())
|
||||
return;
|
||||
|
||||
$fail('There isnt a Structural Objectclass.');
|
||||
if (collect($value)->dot()->filter()->count())
|
||||
$fail(__('There isnt a Structural Objectclass.'));
|
||||
}
|
||||
}
|
||||
|
@ -11,8 +11,9 @@ trait MD5Updates
|
||||
{
|
||||
public function isDirty(): bool
|
||||
{
|
||||
foreach ($this->values->diff($this->oldValues) as $key => $value)
|
||||
if (md5(Arr::get($this->oldValues,$key)) !== $value)
|
||||
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)))
|
||||
return TRUE;
|
||||
|
||||
return FALSE;
|
||||
|
@ -2,9 +2,11 @@
|
||||
|
||||
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
|
||||
{
|
||||
@ -12,30 +14,37 @@ class Attribute extends Component
|
||||
public bool $edit;
|
||||
public bool $new;
|
||||
public bool $old;
|
||||
public ?string $na;
|
||||
public bool $updated;
|
||||
public ?Template $template;
|
||||
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct(?LDAPAttribute $o,bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,?string $na=NULL)
|
||||
public function __construct(?LDAPAttribute $o,bool $edit=FALSE,bool $old=FALSE,bool $new=FALSE,bool $updated=FALSE,?Template $template=NULL)
|
||||
{
|
||||
$this->o = $o;
|
||||
$this->edit = $edit;
|
||||
$this->old = $old;
|
||||
$this->new = $new;
|
||||
$this->na = $na;
|
||||
$this->updated = $updated;
|
||||
$this->template = $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\View|\Closure|string
|
||||
* @return View|string
|
||||
*/
|
||||
public function render()
|
||||
public function render(): View|string
|
||||
{
|
||||
return $this->o
|
||||
? $this->o
|
||||
->render($this->edit,$this->old,$this->new)
|
||||
: $this->na;
|
||||
->render(
|
||||
edit: $this->edit,
|
||||
old: $this->old,
|
||||
new: $this->new,
|
||||
updated: $this->updated,
|
||||
template: $this->template)
|
||||
: __('Unknown');
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
@ -1,35 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
|
||||
use App\Http\Middleware\{AllowAnonymous,ApplicationSession,CheckUpdate,SwapinAuthUser};
|
||||
use App\Http\Middleware\{AcceptLanguage,AllowAnonymous,CheckUpdate,SwapinAuthUser,ViewVariables};
|
||||
|
||||
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('web', [
|
||||
SwapinAuthUser::class,
|
||||
ApplicationSession::class,
|
||||
CheckUpdate::class,
|
||||
]);
|
||||
|
||||
$middleware->prependToGroup('api', [
|
||||
EncryptCookies::class,
|
||||
SwapinAuthUser::class,
|
||||
ApplicationSession::class,
|
||||
$middleware->appendToGroup(
|
||||
group: 'web',
|
||||
middleware: [
|
||||
AcceptLanguage::class,
|
||||
AllowAnonymous::class,
|
||||
SwapinAuthUser::class,
|
||||
ViewVariables::class,
|
||||
CheckUpdate::class,
|
||||
]);
|
||||
|
||||
$middleware->trustProxies(at: [
|
||||
'10.0.0.0/8',
|
||||
'127.0.0.0/8',
|
||||
'172.16.0.0/12',
|
||||
'192.168.0.0/12',
|
||||
]);
|
||||
|
@ -7,13 +7,15 @@
|
||||
"require": {
|
||||
"ext-fileinfo": "*",
|
||||
"ext-ldap": "*",
|
||||
"ext-openssl": "*",
|
||||
"php": "^8.4",
|
||||
"directorytree/ldaprecord-laravel": "^3.0",
|
||||
"laravel/framework": "^11.9",
|
||||
"laravel/framework": "^12.0",
|
||||
"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",
|
||||
|
974
composer.lock
generated
974
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -43,19 +43,6 @@ 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
|
||||
|
10
config/filesystems.php
Normal file
10
config/filesystems.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'disks' => [
|
||||
'templates' => [
|
||||
'driver' => 'local',
|
||||
'root' => base_path(env('LDAP_TEMPLATE_DIR','templates')),
|
||||
],
|
||||
],
|
||||
];
|
@ -35,7 +35,6 @@ return [
|
||||
'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),
|
||||
@ -47,7 +46,6 @@ return [
|
||||
'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),
|
||||
@ -59,7 +57,6 @@ return [
|
||||
'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),
|
||||
@ -72,7 +69,6 @@ return [
|
||||
'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),
|
||||
@ -122,54 +118,47 @@ return [
|
||||
*/
|
||||
'validation' => [
|
||||
'objectclass' => [
|
||||
'objectclass'=>[
|
||||
'required',
|
||||
'array',
|
||||
'min:1',
|
||||
'objectclass.*'=>[
|
||||
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'
|
||||
|
@ -54,6 +54,7 @@
|
||||
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
|
||||
|
59
config/localizator.php
Normal file
59
config/localizator.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?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,
|
||||
|
||||
];
|
@ -68,7 +68,7 @@ return [
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'level' => env('LOG_LEVEL', 'info'),
|
||||
'days' => env('LOG_DAILY_DAYS', 14),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
@ -43,6 +43,17 @@ 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
|
||||
@ -73,7 +84,20 @@ return [
|
||||
* setup.
|
||||
*/
|
||||
'login' => [
|
||||
'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
|
||||
// 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),
|
||||
],
|
||||
],
|
||||
];
|
@ -7,7 +7,6 @@ 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
|
||||
|
||||
@ -40,12 +39,6 @@ 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..."
|
||||
|
1541
package-lock.json
generated
1541
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -15,6 +15,7 @@
|
||||
"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",
|
||||
|
@ -1 +1 @@
|
||||
v2.0.1-rel
|
||||
v2.2.1-rel
|
||||
|
47
public/css/custom.css
vendored
47
public/css/custom.css
vendored
@ -1,28 +1,42 @@
|
||||
/** ensure our userpassword has select is next to the password input */
|
||||
div#userPassword .select2-container--bootstrap-5 .select2-selection {
|
||||
attribute#userpassword .select2-container--bootstrap-5 .select2-selection {
|
||||
font-size: inherit;
|
||||
width: 9em;
|
||||
border: #444054 1px solid;
|
||||
border: var(--bs-gray-500) 1px solid;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.input-group:first-child .select2-container--bootstrap-5 .select2-selection {
|
||||
border-bottom-right-radius: unset;
|
||||
border-top-right-radius: unset;
|
||||
}
|
||||
|
||||
div#objectClass .input-group-end:not(input.form-control) {
|
||||
/* 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 {
|
||||
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;
|
||||
@ -30,7 +44,10 @@ input.form-control.input-group-end {
|
||||
|
||||
.custom-tooltip-danger {
|
||||
--bs-tooltip-bg: var(--bs-danger);
|
||||
}
|
||||
|
||||
.custom-tooltip {
|
||||
--bs-tooltip-bg: var(--bs-gray-900);
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
@ -67,3 +84,17 @@ input.form-control.input-group-end {
|
||||
.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;
|
||||
}
|
16
public/css/fixes.css
vendored
16
public/css/fixes.css
vendored
@ -245,6 +245,11 @@ select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__
|
||||
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;
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
@ -253,3 +258,14 @@ select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__
|
||||
.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;
|
||||
}
|
15
public/js/custom.js
vendored
15
public/js/custom.js
vendored
@ -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')
|
||||
@ -37,15 +37,17 @@ function getNode(item) {
|
||||
$('.main-content').empty().append(e.responseText);
|
||||
break;
|
||||
case 409: // Not in root
|
||||
location.replace('/#'+item);
|
||||
break;
|
||||
case 419: // Session Expired
|
||||
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 500:
|
||||
case 555: // Missing Method
|
||||
$('.main-content').empty().append(e.responseText);
|
||||
break;
|
||||
|
||||
default:
|
||||
alert('Well that didnt work? Code ['+e.status+']');
|
||||
}
|
||||
@ -57,7 +59,7 @@ $(document).ready(function() {
|
||||
if (typeof basedn !== 'undefined') {
|
||||
sources = basedn;
|
||||
} else {
|
||||
sources = { url: 'api/bases' };
|
||||
sources = { method: 'POST', url: '/ajax/bases' };
|
||||
}
|
||||
|
||||
// Attach the fancytree widget to an existing <div id="tree"> element
|
||||
@ -93,8 +95,9 @@ $(document).ready(function() {
|
||||
source: sources,
|
||||
lazyLoad: function(event,data) {
|
||||
data.result = {
|
||||
url: '/api/children',
|
||||
data: {key: data.node.data.item,depth: 1}
|
||||
method: 'POST',
|
||||
url: '/ajax/children',
|
||||
data: {_key: data.node.data.item,create: true}
|
||||
};
|
||||
|
||||
expandChildren(data.tree.rootNode);
|
||||
|
23
public/js/template.js
vendored
Normal file
23
public/js/template.js
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
/* 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
Normal file
81
public/js/toAscii.js
vendored
Normal file
@ -0,0 +1,81 @@
|
||||
//
|
||||
// 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;
|
||||
}
|
@ -1,6 +1,45 @@
|
||||
This directory contains language translation files for PLA.
|
||||
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).
|
||||
|
||||
Language files named by 2 letter iso language name (suffixed with .json)
|
||||
represent the translations for that language.
|
||||
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:
|
||||
|
||||
eg: en.json
|
||||
* `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.
|
||||
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"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>"
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
<?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.',
|
||||
|
||||
];
|
@ -1,19 +0,0 @@
|
||||
<?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' => '« Previous',
|
||||
'next' => 'Next »',
|
||||
|
||||
];
|
@ -1,22 +0,0 @@
|
||||
<?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.",
|
||||
|
||||
];
|
@ -1,151 +0,0 @@
|
||||
<?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' => [],
|
||||
|
||||
];
|
143
resources/lang/zz.json
Normal file
143
resources/lang/zz.json
Normal file
@ -0,0 +1,143 @@
|
||||
{
|
||||
"(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"
|
||||
}
|
3
resources/sass/app.scss
vendored
3
resources/sass/app.scss
vendored
@ -7,3 +7,6 @@
|
||||
// Select2
|
||||
@import "select2/dist/css/select2";
|
||||
@import "select2-bootstrap-5-theme/dist/select2-bootstrap-5-theme";
|
||||
|
||||
// Bootstrap icons
|
||||
@import "bootstrap-icons"
|
||||
|
4
resources/themes/architect/src/base.scss
vendored
4
resources/themes/architect/src/base.scss
vendored
@ -1,9 +1,9 @@
|
||||
/*!
|
||||
=========================================================
|
||||
* ArchitectUI HTML Theme Dashboard - v4.0.0
|
||||
* ArchitectUI HTML Theme Dashboard - v4.1.0
|
||||
=========================================================
|
||||
* Product Page: https://dashboardpack.com
|
||||
* Copyright 2023 DashboardPack (https://dashboardpack.com)
|
||||
* Copyright 2025 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.
|
||||
|
22
resources/themes/architect/src/utils/_animate-override.scss
vendored
Normal file
22
resources/themes/architect/src/utils/_animate-override.scss
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
@use "sass:math";
|
||||
|
||||
@if $use-lightSpeedIn == true {
|
||||
@-webkit-keyframes lightSpeedIn {
|
||||
0% { -webkit-transform: translateX(100%) skewX(-$base-degrees); opacity: 0; }
|
||||
60% { -webkit-transform: translateX(-20%) skewX($base-degrees); opacity: 1; }
|
||||
80% { -webkit-transform: translateX(0%) skewX(calc(-1 * $base-degrees / 2)); opacity: 1; }
|
||||
100% { -webkit-transform: translateX(0%) skewX(0deg); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes lightSpeedIn {
|
||||
0% { transform: translateX(100%) skewX(-$base-degrees); opacity: 0; }
|
||||
60% { transform: translateX(-20%) skewX($base-degrees); opacity: 1; }
|
||||
80% { transform: translateX(0%) skewX(calc(-1 * $base-degrees / 2)); opacity: 1; }
|
||||
100% { transform: translateX(0%) skewX(0deg); opacity: 1; }
|
||||
}
|
||||
|
||||
.lightSpeedIn {
|
||||
@include animate-prefixer(animation-name, lightSpeedIn);
|
||||
@include animate-prefixer(animation-timing-function, $base-timing-function-out);
|
||||
}
|
||||
}
|
@ -58,8 +58,7 @@ $use-all: true;
|
||||
"~animate-sass/animations/flippers/flipOutY";
|
||||
|
||||
// LIGHTSPEED
|
||||
@import "~animate-sass/animations/lightspeed/lightSpeedIn",
|
||||
"~animate-sass/animations/lightspeed/lightSpeedOut";
|
||||
@import "./_animate-override";
|
||||
|
||||
// ROTATE
|
||||
@import "~animate-sass/animations/rotate-enter/rotateIn",
|
||||
|
@ -90,6 +90,14 @@
|
||||
font-size: 1.3rem !important;
|
||||
}
|
||||
|
||||
.font-size-xs {
|
||||
font-size: .6rem !important;
|
||||
}
|
||||
|
||||
.font-size-sm {
|
||||
font-size: .8rem !important;
|
||||
}
|
||||
|
||||
.font-size-md {
|
||||
font-size: .9rem !important;
|
||||
}
|
||||
|
@ -22,7 +22,7 @@
|
||||
<div class="h5 modal-title text-center">
|
||||
<h4 class="mt-2">
|
||||
<div class="app-logo mx-auto mb-3"><img class="w-75" src="{{ url('images/logo-h-lg.png') }}"></div>
|
||||
<small>@lang('Sign in to') <strong>{{ config('server')->name }}</strong></small>
|
||||
<small>@lang('Sign in to') <strong>{{ $server->name }}</strong></small>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" translate="no">
|
||||
@section('htmlheader')
|
||||
@include('architect::layouts.partials.htmlheader')
|
||||
@show
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div class="app-page-title">
|
||||
<div class="page-title-wrapper">
|
||||
<div class="page-title-wrapper bg-white">
|
||||
<div class="page-title-heading">
|
||||
@if(trim($__env->yieldContent('page_icon')))
|
||||
<div class="page-title-icon f32">
|
||||
@ -13,55 +13,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-title-items p-2">
|
||||
<div class="page-title-actions">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="action-buttons float-end">
|
||||
<ul class="nav">
|
||||
@if(isset($page_actions) && $page_actions->contains('export'))
|
||||
<li>
|
||||
<span data-bs-toggle="modal" data-bs-target="#entry_export-modal">
|
||||
<button class="btn btn-outline-dark p-1 m-1" data-bs-toggle="tooltip" data-bs-placement="bottom" title="@lang('Export')"><i class="fas fa-fw fa-download fs-5"></i></button>
|
||||
</span>
|
||||
</li>
|
||||
@endif
|
||||
@if(isset($page_actions) && $page_actions->contains('copy'))
|
||||
<li>
|
||||
<button class="btn btn-outline-dark p-1 m-1" id="entry-copy-move" data-bs-toggle="tooltip" data-bs-placement="bottom" title="@lang('Copy/Move')"><i class="fas fa-fw fa-copy fs-5"></i></button>
|
||||
</li>
|
||||
@endif
|
||||
@if((isset($page_actions) && $page_actions->contains('edit')) || old())
|
||||
<li>
|
||||
<button class="btn btn-outline-dark p-1 m-1" id="entry-edit" data-bs-toggle="tooltip" data-bs-placement="bottom" title="@lang('Edit Entry')"><i class="fas fa-fw fa-edit fs-5"></i></button>
|
||||
</li>
|
||||
@endif
|
||||
<!-- @todo Dont offer the delete button for an entry with children -->
|
||||
@if(isset($page_actions) && $page_actions->contains('delete'))
|
||||
<li>
|
||||
<span id="entry-delete" data-bs-toggle="modal" data-bs-target="#page-modal">
|
||||
<button class="btn btn-outline-danger p-1 m-1" data-bs-custom-class="custom-tooltip-danger" data-bs-toggle="tooltip" data-bs-placement="bottom" title="@lang('Delete Entry')"><i class="fas fa-fw fa-trash-can fs-5"></i></button>
|
||||
</span>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@yield('page_actions')
|
||||
</div>
|
||||
|
||||
@section('page-scripts')
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$('button[id=entry-edit]').on('click',function(item) {
|
||||
item.preventDefault();
|
||||
|
||||
if ($(this).hasClass('btn-dark'))
|
||||
return;
|
||||
|
||||
editmode();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@append
|
||||
<div class="page-title-status pt-4">
|
||||
@yield('page_status')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user