Compare commits

...

179 Commits

Author SHA1 Message Date
7346a3daf5 Release v2.2.1
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m41s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 3m15s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-07-03 13:30:38 +08:00
305ef0f5a3 Minor code consistency changes, no functional changes 2025-07-03 13:17:25 +08:00
f1316d698d Implement DN Entry rename 2025-07-03 13:17:25 +08:00
339ba7258a Make the ajax calls POST methods, and make the 'Create Entry' in the tree configurable so calls to children() can just return child entries 2025-07-03 13:17:25 +08:00
883ac5d90f Dont render delete button on Entries that have subordinates 2025-07-03 13:17:13 +08:00
46277146c5 Fix rendering of Add Value attributes when the attribute is also rendered by a template, resulting in double javascript and blank values 2025-07-03 13:16:58 +08:00
06747064d4 Fix add new attributes where being marked as readonly 2025-07-03 13:16:58 +08:00
2c91298b41 Release v2.2.0
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 27s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m31s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 2m48s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-07-02 13:48:25 +08:00
9798863e34 Sample data fixes so that test completes on first run. This synchronises the import test with what is initially loaded
Some checks failed
Create Docker Image / Build Docker Image (arm64) (push) Has been cancelled
Create Docker Image / Build Docker Image (x86_64) (push) Has been cancelled
Create Docker Image / Final Docker Image Manifest (push) Has been cancelled
Create Docker Image / Test Application (x86_64) (push) Has been cancelled
2025-07-02 13:26:22 +08:00
4494154879 Fix regression introduced in 56fcd729. Server was added to the configuration before SwapinAuthUser::class resulting in the configured LDAP user being used for all queries and not the logged in user. Fixes #348
Some checks failed
Create Docker Image / Test Application (x86_64) (push) Has been cancelled
Create Docker Image / Build Docker Image (arm64) (push) Has been cancelled
Create Docker Image / Build Docker Image (x86_64) (push) Has been cancelled
Create Docker Image / Final Docker Image Manifest (push) Has been cancelled
2025-06-30 20:35:33 +08:00
b22c9505bc Fix rendering of objectclass in server info, consistent use of true/false/null in view blades
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 31s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m33s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 2m48s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-06-27 18:44:25 +10:00
29a659ff69 Fix typo in 553368c that stopped configuration defaults from loading
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m28s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 2m45s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2025-06-27 16:53:04 +10:00
2348da36c4 Fix hasing password on entry create. Fixes #353
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 2m46s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 15m7s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-06-27 14:18:52 +10:00
6f58f5db36 Fix bug introduced with 553368c, when clearing session _auto_number when need to allow for edits that doesnt have this set
Some checks failed
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 2m41s
Create Docker Image / Final Docker Image Manifest (push) Has been cancelled
Create Docker Image / Build Docker Image (x86_64) (push) Has been cancelled
2025-06-27 14:03:35 +10:00
553368c7b9 Implement getNextNumber() to populate template->values for attributes, where the attribute is determined after evaulating whats in the directory
Some checks failed
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 3m1s
Create Docker Image / Final Docker Image Manifest (push) Has been cancelled
Create Docker Image / Build Docker Image (x86_64) (push) Has been cancelled
2025-06-27 13:50:01 +10:00
c8d1122ff6 Fix validation on existing entries, missed in 88db4cc 2025-06-26 23:13:46 +10:00
2320445dfb Fix regression introduce in 31e3c7, x-form.select wasnt rendering the current value of the select list. Also fix validation redirect where the password encryption method was changed, but the new encryption method was not set. 2025-06-26 22:49:06 +10:00
6d2c9d1354 Specifying a comma delimited list for LDAP_BASE_DN was never going to work. Use a colon instead. Fixes #351 2025-06-26 22:04:37 +10:00
6f20d426ad Dont sort by DN, problematic when sssvlv overlay is used in openldap. Seems DN's are sorted anyway. Fixes #350 2025-06-26 21:55:10 +10:00
7b1b4f4e50 Rename and group schema modification files to better identify global and specific database changes 2025-06-26 21:53:16 +10:00
543250e1fb Fix entry-userpassword-check when entry is rendered with a template
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 30s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m27s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 2m51s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2025-06-22 22:10:21 +10:00
3bf97fc0d1 Add the ability to use a select list for template attributes 2025-06-22 22:08:38 +10:00
3ad4c446ea Change our template attribute processing, to be collections, so we can find attributes using anycase keys 2025-06-22 17:27:56 +10:00
ee3cb395c2 Enhancement to 8fd2a43, validating authentication before rendering the DN doesnt exist error (otherwise it is an authentication issue) 2025-06-22 14:07:33 +10:00
29c39e618f Ensure form validation is displayed on template input entries, especially those marked as read-only
Some checks failed
Create Docker Image / Test Application (x86_64) (push) Has been cancelled
Create Docker Image / Build Docker Image (arm64) (push) Has been cancelled
Create Docker Image / Build Docker Image (x86_64) (push) Has been cancelled
Create Docker Image / Final Docker Image Manifest (push) Has been cancelled
2025-06-22 10:18:23 +10:00
647cee9858 Fix regression introduce in 31e3c7 when adding a new objectclass to a new entry, newoc shouldnt be passed as a form value 2025-06-22 10:18:23 +10:00
54c0df2597 Fix rendering updated attributes on entries that trigger a template 2025-06-22 10:18:23 +10:00
67d65b3a98 Framework and javascript dependancies update 2025-06-22 10:18:23 +10:00
9547b5fc5a Update README with v2.2 updates, as well as updating the home page 2025-06-22 10:18:23 +10:00
f6b7bff605 Enable disabling internal templates, as well as having custom templates 2025-06-22 09:11:47 +10:00
e8aaa17122 Change our internal template keys to be prefixed with an underscore for easier identification 2025-06-22 09:11:47 +10:00
ee7762d69b Working JS Template Engine with basic functionality 2025-06-22 09:11:43 +10:00
fac560750e Update npm assets to make dependabot happier 2025-06-20 17:13:33 +10:00
d3aa73e468 Remove our highlighted item from the tree, when we click on the top-menu buttons 2025-06-20 17:13:33 +10:00
2ddeff8ed3 Fix page expired 419 started showing a page expired message, instead of refreshing the session and loading the clicked item on the tree 2025-06-20 17:13:33 +10:00
b6bce380dd Fix for when specifying multiple base DNs with LDAP_BASE_DN, and the user doesnt have access to the first one. 2025-06-20 17:13:33 +10:00
8fd2a43ee2 Add alert for DN logins that dont exist. Might be attempts to use the rootdn which is not supported.
Closes #345
2025-06-20 17:13:33 +10:00
96afbd8316 Pass the template object to the attributes, so we can leverage template rules when rendering attributes 2025-06-20 17:13:33 +10:00
5ce3a63878 Revert c56df8d3d and remove adding Objects directly - taking a different approach to add template actions 2025-06-19 16:15:22 +10:00
ac8e79ab99 Minor logging message updates, no functional changes 2025-06-19 16:15:22 +10:00
d0c02b91c0 Re-implement LDAP_BASE_DN to limit what is shown in the tree, and what PLA uses internally to search the server. Fixes #342 2025-06-19 16:15:22 +10:00
2a691c147e Remove references to APP_URL and LDAP_BASE_DN, they are not actually used 2025-06-19 16:15:22 +10:00
781c87cb83 Fix positioning of Check Password box, and dont render it when creating a new entry 2025-06-19 16:15:22 +10:00
98a0b87afe Add objects directly to Entry::class when rendering a template. Fix objectclasses and attributes processing for templates 2025-06-19 16:15:22 +10:00
88db4ccc99 Update AttributeTypes/LDAPSyntaxes/MatchingRules for performance and process improvements 2025-06-18 22:39:23 +10:00
6059bc1e45 Pass template to our component rendering to avoid duplicate javascript object id's 2025-06-18 22:39:23 +10:00
acf19cdc5b Optimize schema objectclass processing, changing debugging output, remove redundant functions 2025-06-13 23:03:27 +10:00
56fcd729e7 Load the rootDSE in Server::__construct(), remove basedn from views, and rely on the javascript to get the basedns 2025-06-12 12:06:44 +09:30
d61f6168a4 Remove MatchRuleUse::class, it wasnt used 2025-06-12 12:06:44 +09:30
f2eaed247a Cache loading templates 2025-06-12 12:06:44 +09:30
31e3c75bc9 Enhancements to logic that makes form.select component 2025-06-12 12:06:44 +09:30
9f0290bd40 Enable creation of new entries via templates 2025-06-12 12:06:44 +09:30
820f398c2c Start of work on templates - identify templates that apply to existing entries 2025-06-10 16:02:07 +10:00
8602c2b17f Only swap in user's credentials if the requested page is not the logout page. This avoids an issue if the user's credentials are changed during their session, they couldnt log out 2025-06-09 10:31:25 +10:00
33d96940e6 Consistent rendering of certificatelist attributes with certificate attributes
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m26s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 2m42s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-06-03 23:20:32 +10:00
06b7c204b0 Add more Certificate Serial Number, Subject and Authority Key IDs
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m39s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 2m55s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2025-06-03 22:49:04 +10:00
7854cbdabd Cosmetic fixes for search results - fixing overflow affecting the input box 2025-06-03 16:16:33 +10:00
32514c9ab1 Remove the warning about multi-language tags, PLA handles them fine now 2025-06-02 10:39:18 +10:00
db600a28d3 Install amiranagram/localizator into dev setup to identify translatable strings,
Show locale on the debug frame,
Detect the browsers language,
Documentation on translating PLA, and
Some missed translatable strings
2025-06-02 10:39:02 +10:00
b08de519d4 Blade syntax consistency updates - no functional changes
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 4m7s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 2m35s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m3s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2025-06-01 19:28:08 +10:00
6599bb7f4f Fix deprecation message introduced by 3d511f3
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 27s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m24s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 2m48s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2025-06-01 16:08:11 +10:00
d623f3c26d Move langtag rendering from dn/Entry into Attribute - more enhancements for #16,
Reduce use of style= tags,
Cosmetic layout changes,
Layout change to enable rendering template views,
<attribute> id tags are now lowecase
2025-06-01 16:08:11 +10:00
bd40ab0e84 Framework upgrade to Laravel 12 and javascript updates 2025-06-01 16:08:11 +10:00
3fcb8707d9 Revert version to 2.2.0-dev 2025-05-31 10:52:20 +10:00
c6e1640752 Fix for when the logged in user's details doesnt include an objectclass (because the query didnt have the ACLs to return them).
This should help #330 but doesnt allow the user to login even if they have the right objectclasses, but the query didnt return them.
2025-05-24 22:02:58 +10:00
917a3c1a0d Testing for handling attribute values 2025-05-24 22:02:58 +10:00
148d19bbce Some extra debug logging to help with fixes testing 2025-05-24 22:02:58 +10:00
6c501cc29d Fixes to ensure testing works again
Added LDIF Import testing
2025-05-24 22:02:58 +10:00
2ce0ed8974 Framework update to make dependabot happy #319 2025-05-24 00:32:39 +10:00
9a6d80986a We should start resume the session with ApplicationSession before checking for Session keys in AllowAnonymous 2025-05-24 00:26:55 +10:00
fa989b8f10 Remove MEMCACHED startup, since it is no longer included in the container 2025-05-24 00:26:55 +10:00
4e991db8b1 Fix showing required attributes when required by a parent class of the entries objectclasses 2025-05-24 00:26:55 +10:00
181971acc4 Improvements to userPassword handling when redirecting back. No need to use old() when rendering attributes, since they have already been processed by display with any updates 2025-05-24 00:26:55 +10:00
3493504720 Fix exception 500 being raised 'Argument #1 () must be of type array' when creating new entries with a userPassword. Fixes #320 2025-05-24 00:26:55 +10:00
54f27d3d16 Fix grammar errors on home page :( 2025-05-24 00:26:55 +10:00
3c0eb876e4 In the 'Add New Attribute' select list, use the attributes LDAP name instead of its lowercased name 2025-05-24 00:26:55 +10:00
6d55b52cd4 Attribute search should be lowercase, since we are comparing against $o->name_lc
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m21s
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m32s
Create Docker Image / Final Docker Image Manifest (push) Successful in 8s
2025-05-04 20:23:42 +10:00
16a1f85a79 Release v2.1.3
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m21s
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m33s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-05-04 19:06:02 +10:00
f8d7819153 Fix for ARGON2 passwords, they shouldnt be base64 encoded. Fixes #316 2025-05-04 00:01:40 +10:00
75dbb37d8b $langtag doesnt need to be passed to x-attribute.layout. Fix adding new userpassword. Only show Add Value when the Attribute has atleast 1 old value 2025-05-04 00:01:40 +10:00
309fe83c98 LDAP server errors were not being displayed when used with ->withErrors(), so created a new component x-failed 2025-05-04 00:01:40 +10:00
ffb98631a6 Implemented search via the top menu 2025-05-03 20:27:16 +10:00
be69e22867 Update home page 2025-05-03 20:27:16 +10:00
21c88048e3 Fix rendering of x-attribute-type missing defaults from edit/new/langtag/update as a result of bab5a262
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m26s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m36s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-05-02 11:39:59 +10:00
471ccfd88e Release v2.1.2
Some checks failed
Create Docker Image / Build Docker Image (arm64) (push) Has been cancelled
Create Docker Image / Build Docker Image (x86_64) (push) Has been cancelled
Create Docker Image / Final Docker Image Manifest (push) Has been cancelled
Create Docker Image / Test Application (x86_64) (push) Has been cancelled
2025-05-01 12:12:20 +09:30
5d23cbf0cc If a user doesnt have permission to see an entries attributes - but can see the entry, disable edit and dont attempt to render. Further, if they cant see the objectclasses, dont make additional attributes available 2025-05-01 12:12:20 +09:30
b9ae269895 Fix html syntax error that stopped rendering uuid for a DN 2025-05-01 11:02:58 +09:30
ae782577e7 Automatically work out if attributes are internal (because they are not used in objectclasses)
Some checks failed
Create Docker Image / Test Application (x86_64) (push) Has been cancelled
Create Docker Image / Build Docker Image (arm64) (push) Has been cancelled
Create Docker Image / Build Docker Image (x86_64) (push) Has been cancelled
Create Docker Image / Final Docker Image Manifest (push) Has been cancelled
2025-04-30 21:46:57 +09:30
84f82aaf59 Cosmetic layout change for DNs (extending the background to the page width). 2025-04-30 21:46:57 +09:30
10eca55026 NPM and framework update 2025-04-30 09:13:58 +09:30
64cc21d819 Fix logic processing isDirty() for MD5Update apps by checking that the new & old values dont equal before checking their md5 value 2025-04-30 09:04:10 +09:30
3d511f3fae Change rendering of notes/errors/update and highlight attributes that are successfully updated 2025-04-30 09:04:10 +09:30
bab5a2626d Remove Components/AttributeType::class it wasnt providing any functionality 2025-04-30 09:04:10 +09:30
6954b09089 @todo udpates 2025-04-27 14:12:24 +10:00
a336e58b7a Fixes for 389 Directory Server - addresses recursion issue #314. The primary issue was that 389DS doesnt render the subschemaSubentry attribute unless it is specifically requested. 2025-04-27 14:12:24 +10:00
53880121b6 Server::class optimisations, minimal functional changes - basically caching/performance improvements 2025-04-27 14:12:24 +10:00
ea46cf36d0 Remove deprecteated Entry::query() override and associated noObjectAttributes() it wasnt used 2025-04-27 14:12:24 +10:00
36f8f57b77 When opening the export modal, limit selection to inside the modal. Generally when opening modals disable selection.
When selecting a DN on a DN fragment, autoselect the whole DN.
2025-04-27 14:12:24 +10:00
3604f1498c Update existing LDAP instance configuration instead of replacing it. Caching was not enabled as per the configuration, so this fixes this. 2025-04-27 14:12:24 +10:00
808934ebfe Change we now store logged in user details in session, instead of cookies.
This is so when the session expires, the logged in user details are expired as well, which wasnt happening with cookies.
2025-04-27 14:12:24 +10:00
21a690c6dd Move our /api routes into /ajax under web.php. The /api routes werent authenticated and may not have been using the logged in users details 2025-04-27 14:12:24 +10:00
0083e9158b Move out view variables until after our session has been setup. This was needed so that auth()->user() could be resolved correctly and needed to be done after we have started the session and swapped in the users cookies 2025-04-27 14:12:24 +10:00
f4cc559931 Dynamically work out objectclasses on the current entry, this fixes usage issues between adding objectclasses and adding attribute that are now available from new objectclasses, as well as determining that they are not dynamic 2025-04-27 14:12:24 +10:00
3de46ac28e Fix when rendering changes to 2 or more attributes, the update confirmation table had one too many rowspan values for the Attribute.
Fix updating an entry by adding an new objectclass
2025-04-27 14:12:24 +10:00
8d4dccd9e9 Release v2.1.1
Some checks failed
Create Docker Image / Build Docker Image (arm64) (push) Has been cancelled
Create Docker Image / Build Docker Image (x86_64) (push) Has been cancelled
Create Docker Image / Final Docker Image Manifest (push) Has been cancelled
Create Docker Image / Test Application (x86_64) (push) Has been cancelled
2025-04-21 18:07:37 +10:00
ccff36361f Fix catching InsufficientAccessException when creating new entries 2025-04-21 17:24:59 +10:00
b7ca768cc6 Enable creation of new branch. Closes #312 2025-04-21 17:24:59 +10:00
a61f5e9b97 Release v2.1.0
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 29s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m24s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m34s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2025-04-11 15:38:06 +10:00
d845d87a6e Laravel framework and javascript modules update 2025-04-11 15:38:06 +10:00
b501dfe824 During create we were passing the wrong objectlcasses to the ajax call when adding a new attribute. 2025-04-11 15:38:06 +10:00
3fad9770a3 When submitting an import form and validation fails, there is no DN returned, so dont update one. 2025-04-11 14:59:24 +10:00
b1d153aa9f Change Attribute/UserCertificate into Syntax/Certificate for any Certificate attributes. Add Syntax/CertificateList.
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 29s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m30s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m34s
Create Docker Image / Final Docker Image Manifest (push) Successful in 8s
2025-04-11 08:55:32 +10:00
8b0af505a1 When viewing the schema, highlight Structural and non-Structural classes
Some checks failed
Create Docker Image / Test Application (x86_64) (push) Has been cancelled
Create Docker Image / Build Docker Image (arm64) (push) Has been cancelled
Create Docker Image / Build Docker Image (x86_64) (push) Has been cancelled
Create Docker Image / Final Docker Image Manifest (push) Has been cancelled
2025-04-11 08:55:32 +10:00
f0eaff7d42 Removing debugging that made it into LDIF import 2025-04-11 08:55:32 +10:00
352bbe2b75 Capture PLA version when submitting a bug report 2025-04-11 08:50:31 +10:00
0fe4894192 Create config.yml to disable blank issues reporting 2025-04-11 08:43:37 +10:00
a7be4e00b4 Fix rendering new attributes, so that they dont render as dynamic. Fix adding new objectClasses to entries, need langtag to render the component
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m28s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m47s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-04-08 22:04:48 +10:00
2abc321eca Fix for showing no_lang_tag attrs (which are displayed without values) on a lang_tag attr pane when viewing a DN 2025-04-08 14:50:23 +10:00
6b2fb8dee4 Dont add hints for internal attributes. Our hints now also returns a collection. 2025-04-08 11:04:31 +10:00
66537dcec8 Revert version to 2.1.0-dev
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 29s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m29s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m36s
Create Docker Image / Final Docker Image Manifest (push) Successful in 8s
2025-04-07 22:35:08 +10:00
1bf8830887 When rendering dynamic attributes, dont make them editable. Closes #10 and #89.
Also some minor fixes when returning from a post for a DN with attribute tags.
2025-04-07 22:35:08 +10:00
c4d28c8a23 Add support for displaying user certificates, that are recorded in the directory with a ;binary tag. Closes #75
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m22s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m31s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-04-07 14:34:56 +10:00
29c460fd4b Ensure our validation message is shown when not selecting an objectclass when creating a new entry 2025-04-07 14:34:56 +10:00
3196b10aed Add OID description for searchguide attributes 2025-04-07 14:34:56 +10:00
f41b484dc4 More ldap configuration settings for demo ldap environment.
Should help when working on #10, #89, #287.
2025-04-06 22:50:46 +10:00
855d7ae75c Move entry-edit javascript out of architect theme 2025-04-06 22:50:46 +10:00
ffa8cdc826 Fix User Password Check now that we have attribute tags 2025-04-06 22:50:46 +10:00
8f39603f9f Improved determination of attribute object being dirty, improved detection of blank input and processing
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 30s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m36s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m34s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-04-06 19:33:04 +10:00
bcea6de791 Validation of inputs for a DN with language tags - work for #16
Some checks failed
Create Docker Image / Build Docker Image (arm64) (push) Has been cancelled
Create Docker Image / Build Docker Image (x86_64) (push) Has been cancelled
Create Docker Image / Final Docker Image Manifest (push) Has been cancelled
Create Docker Image / Test Application (x86_64) (push) Has been cancelled
2025-04-06 13:54:32 +10:00
28f4869628 Attribute is no longer iterable - cant be used now that we manage attribute tags 2025-04-06 13:54:32 +10:00
cf535286c5 Render HTML inputs for a DN with language tags - work for #16 2025-04-06 13:54:32 +10:00
633513d3e9 Display a DN entry with language tags - work for #16 2025-04-06 13:54:32 +10:00
705bfb2d64 Update page_actions to be consistent with what we can do so far 2025-04-05 23:24:45 +11:00
3a3bf2addb Make select automatically selecting one item when there is only one configurable 2025-04-05 23:24:45 +11:00
5bb573100b Further to eab4f04 we need some attributes to render tree icons 2025-04-04 20:48:42 +11:00
a57ee78492 Ensure that Attribute::required() doesnt work with NULL $this->schema. Avoids issue as reported by #306 2025-04-04 20:48:42 +11:00
eab4f0427c No need to retrieve all records by default when getting children. By default sort records by DN until we implemented configurable sorting.
Should help the timeout issues reported in #301
2025-03-20 21:17:28 +11:00
fd2c5d1286 Add some attribute tags messages when we cant handle some attributes. 2025-03-19 09:41:47 +11:00
b35b44b2b8 Import and Export work with attribute tags 2025-03-19 09:41:47 +11:00
ce66dcb2b5 Remove deprecated Attribute::lang_tags 2025-03-19 09:41:47 +11:00
56a91f853c Fix export to work with no_attr_tags 2025-03-19 09:41:47 +11:00
81e0e58650 Handle no attribute tags at an Attribute::class level, added form/disabled components 2025-03-19 09:41:47 +11:00
1470170928 Internal attributes are now handled by the new backend setup for attribute tags 2025-03-19 09:41:47 +11:00
85c7132b30 Start of work to handle attribute tags - should help with #75 and #16 2025-03-19 09:41:47 +11:00
7e050954c3 Release v2.0.3
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 30s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m32s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m31s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-03-19 09:04:22 +11:00
16880cd0e2 Revert "Dont run CI/CD on master"
This reverts commit 9b33a20cc4ce2a9aa36821dec9fc569326f8c2c4.
2025-03-19 09:04:11 +11:00
696d87d190 Improve entry validation to only require the first item of multi value attributes 2025-03-19 08:36:01 +11:00
87bae89ea3 Fix validation when creating a new entry and not identifying required attributes, broken by 4a84c25 2025-03-18 23:40:38 +11:00
1abc2cc6e1 Move userpassword check to its own modal, leveraging page-modal 2025-03-18 23:40:38 +11:00
1abab9db94 Move DN export to its own modal, leveraging page-modal 2025-03-18 23:40:38 +11:00
410daf649e Squash with Move our page-actions out of the theme... 2025-03-18 23:40:38 +11:00
9666841c3c Move our page-actions out of the theme into frame/dn. Add some attribute tags messages when we cant handle some attributes. 2025-03-18 23:40:38 +11:00
9b33a20cc4 Dont run CI/CD on master 2025-03-16 10:19:23 +11:00
649749f9c1 MD5Update attributes cannot handle validation failures with a redirect back to the form, so restore the old values for now 2025-03-16 10:13:03 +11:00
5d3b8609bb Added an entry with a binary certification to test environment, with example LDIF to implement #75 2025-03-16 10:13:03 +11:00
93640959db Add our request()->root() to our debug page, implement Entry::getSortKeyAttribute() 2025-03-16 10:13:03 +11:00
f667250b2c Some PHP 8.4 deprecration fixes regarding NULL assignment to cast values on class instantiation 2025-03-16 10:13:03 +11:00
4a84c25ac7 Add Attribute required by ObjectClasses in schema viewer,
Attribute is_rdn dynamically calculated,
Fix Required by Objectclasses when viewing a DN
2025-03-16 10:13:03 +11:00
8ab5b4f35c Move direct controller direct view calls to route/web, add global $server to use in views, negating the need to use config('server') 2025-03-16 10:13:03 +11:00
de2d139288 Some DN rendering fixes, so that our Server Info renders correctly (aligned values) 2025-03-16 10:13:03 +11:00
d326d3c308 Store our DN and objectclasses in Attribute::class entries, so that we can dynamically calculate is_rdn and required objects (to be implemented) 2025-03-16 10:13:03 +11:00
d3fc9c135f When creating a new entry, and an RDN attribute has more than 1 input, only take over the first input when selecting the RDN attribute 2025-03-16 10:13:03 +11:00
eb6e0b8d43 Include LDAP diagnostic error message when we have an LDAP error 2025-03-16 10:13:03 +11:00
b01f7d5baf Attribute cleanup and optimisation in preparation to support attribute tags, HomeController return casting 2025-03-16 10:13:03 +11:00
1ddb58ebbb Buttons that trigger ajax activity cant be buttons, change them back to span 2025-03-13 23:25:04 +11:00
b260912e01 Revert changing buttons in 49fd9b419a 2025-03-13 21:22:31 +11:00
7debd9ff2b Node updates to address vulnerabilities in babel/helpers and axios. Framework update too. 2025-03-13 09:43:47 +11:00
49fd9b419a Some jquery selector changes, change some button spans to buttons, set readonly on the form for attribute javascript, fix krbTicketFlags to only be changed when in edit mode
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 30s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m29s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m38s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-03-13 09:33:45 +11:00
3161fe4fcb Fix password hash select list, was not being editable when choosing edit mode 2025-03-13 09:33:44 +11:00
add3f85812 Improved handling for Kerberous attributes - closes #154 2025-03-13 09:33:44 +11:00
853bd92340 Fix detection of zero values when rendering update NEW/DELETED tags 2025-03-13 09:33:44 +11:00
a56b2d8002 Add some opendj internal attributes. Remove some unused variables in APIController 2025-03-13 09:33:44 +11:00
af7ca851d5 Release v2.0.2
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 29s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m40s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 4m33s
Create Docker Image / Final Docker Image Manifest (push) Successful in 9s
2025-03-11 20:11:36 +11:00
b34dad8836 Fix when adding a new objectclass with required attributes, validation errors are correctly display on the returned form 2025-03-10 13:25:43 +11:00
ef2ea5e266 Fix detection of new attributes added to an entry 2025-03-10 13:25:43 +11:00
91b5b53137 When making new attributes available, only render unique attributes 2025-03-10 13:25:43 +11:00
d4c916923d When adding new attributes as a result of adding a new objectclass, dont duplicate existing attributes already present 2025-03-10 12:35:38 +11:00
e94a7d58e1 Disable buttons that we havent implemented yet, update README with some more todos 2025-03-09 14:08:45 +11:00
15d5bf605a Include loopback in our trusted proxies configuration - fixes #294 2025-03-09 13:32:58 +11:00
183 changed files with 6680 additions and 4348 deletions

View File

@ -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

View File

@ -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

View File

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

View File

@ -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
View File

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

View File

@ -1,4 +1,10 @@
# phpLDAPadmin
![GitHub commit activity](https://img.shields.io/github/commit-activity/m/leenooks/phpldapadmin)
![Docker Pulls](https://img.shields.io/docker/pulls/phpldapadmin/phpldapadmin)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/leenooks/phpldapadmin/total)
![GitHub Release Date](https://img.shields.io/github/release-date/leenooks/phpldapadmin)
![GitHub commits since latest release](https://img.shields.io/github/commits-since/leenooks/phpldapadmin/latest)
phpLDAPadmin is a web based LDAP data management tool for system administrators. It is commonly known and referred by many as "PLA".
PLA is designed to be compliant with LDAP RFCs, enabling it to be used with any LDAP server.
@ -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

View File

@ -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,[]));
}
}

View File

@ -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
{
}

View File

@ -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);
}
}

View 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);
}
}

View 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;
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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
{
}

View File

@ -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
{
}

View File

@ -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
{
}

View File

@ -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
{
}

View File

@ -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
{
}

View File

@ -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')

View File

@ -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
{
}

View 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;
}
}

View 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());
}
}

View 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;
}

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

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

View File

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

View File

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

View File

@ -6,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);

View File

@ -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?'));
}
}

View 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);
}
}

View File

@ -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')

View File

@ -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')

View File

@ -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;

View File

@ -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);
}
}
}
}

View File

@ -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:

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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]));
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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
*

View File

@ -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),
};
}
}

View File

@ -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
View 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;
}
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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;

View File

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

View File

@ -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');
}
}

View File

@ -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');

View File

@ -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);
}

View File

@ -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);

View File

@ -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();

View File

@ -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();

View File

@ -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);
};
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -31,7 +31,7 @@ class LdapUserRepository extends LdapUserRepositoryBase
return $this->query()->find($credentials['dn']);
// Look for a user using all our baseDNs
foreach (Server::baseDNs() 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)

View File

@ -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 {

View File

@ -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());
}
}

View File

@ -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.'));
}
}

View File

@ -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;

View File

@ -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');
}
}

View File

@ -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);
}
}

View File

@ -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',
]);

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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
View File

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

View File

@ -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'

View File

@ -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
View 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,
];

View File

@ -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,
],

View File

@ -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),
],
],
];

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

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

47
public/css/custom.css vendored
View File

@ -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
View File

@ -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
View File

@ -15,7 +15,7 @@ function getNode(item) {
$.ajax({
url: '/frame',
method: 'POST',
data: { key: item },
data: { _key: item },
dataType: 'html',
beforeSend: function() {
content = $('.main-content')
@ -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
View 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
View 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;
}

View File

@ -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.

View File

@ -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>"
}

View File

@ -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.',
];

View File

@ -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' => '&laquo; Previous',
'next' => 'Next &raquo;',
];

View File

@ -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.",
];

View File

@ -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
View 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"
}

View File

@ -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"

View File

@ -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.

View 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);
}
}

View File

@ -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",

View File

@ -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;
}

View File

@ -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>

View File

@ -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

View File

@ -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