370 Commits

Author SHA1 Message Date
Austin Alvarado
a627e69e46 putting a pin in it 2024-01-19 01:37:54 +00:00
Valentin Tolmer
bd0a58b476 server: clean up the attributes, relax the substring filter conditions
This consolidates both user and group attributes in their map_{user,group}_attribute as the only point of parsing. It adds support for custom attribute filters for groups, and makes a SubString filter on an unknown attribute resolve to just false.
2024-01-17 23:44:25 +01:00
dependabot[bot]
4adb636d53 build(deps): bump actions/cache from 3 to 4
Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-17 22:11:09 +01:00
Valentin Tolmer
6f905b1ca9 server: update ldap3_proto dependency
This will fix the issue with some unhandled controls, this time for sure
2024-01-16 17:52:15 +01:00
Valentin Tolmer
2ea17c04ba server: Move the definition of UserId down to lldap_auth 2024-01-15 23:48:59 +01:00
Valentin Tolmer
10609b25e9 docs: Misc updates
Deprecate key_file in favor of key_seed, add a script to generate the secrets
2024-01-14 22:57:10 +01:00
Valentin Tolmer
9f8364ca1a server: Fix private key reset functionality 2024-01-14 22:54:13 +01:00
Valentin Tolmer
56078c0b47 docs: add lldap-cli references, improve README 2024-01-13 22:53:12 +01:00
Valentin Tolmer
8b7852bf1c chore: clippy warnings 2024-01-13 18:32:58 +01:00
Valentin Tolmer
c4be7f5b6f server: Serialize attribute values when searching
This should fix #763 and allow filtering by custom attribute values.
2024-01-13 13:37:46 +01:00
Valentin Tolmer
337101edea server: update ldap3_proto dependency
This will fix the issue with some unhandled controls
2024-01-08 16:10:11 +01:00
Valentin Tolmer
dc140f1675 server: exit with non-zero code when running into errors starting 2024-01-06 00:43:41 +01:00
Roman
f74f88f0c0 example_configs: Add grocy 2024-01-03 21:46:14 +01:00
Valentin Tolmer
708d927e90 server: add a unique index to the memberships 2024-01-03 12:40:24 +01:00
Valentin Tolmer
0d48b7f8c9 server: add support for entryDN 2023-12-31 08:27:25 +01:00
Valentin Tolmer
f2b1e73929 server: Add a check for a changing private key
This checks that the private key used to encode the passwords has not
changed since last successful startup, leading to a corruption of all
the passwords. Lots of common scenario are covered, with various
combinations of key in a file or from a seed, set in the config file or
in an env variable or through CLI, and so on.
2023-12-29 15:37:52 +01:00
Dedy Martadinata S
997119cdcf switch up build steps (#776)
* switch up build steps

* also swith the buildx
2023-12-29 00:23:57 +07:00
ddiawara
a147085a2f example_configs: add Dovecot configuration for docker-mailserver
---------

Co-authored-by: Dedy Martadinata S <dedyms@proton.me>
2023-12-28 11:26:37 +01:00
Dedy Martadinata S
f363ff9437 docker: Add a rootless container
New images with "-rootless" tags will automatically get released on the docker registry.
2023-12-28 11:22:20 +01:00
Haoyu Xu
b6e6269956 example_configs: make the zitadel doc more comprehensive
fixed `Userbase` attribute; added `Preferred username attribute`; added `Automatic creation`
2023-12-25 18:48:07 +01:00
Valentin Tolmer
ff0ea51121 server: Add an option to force reset the admin password 2023-12-22 08:27:35 +01:00
Haoyu Xu
9ac96e8c6e example_configs: add support for admins and local users in homeassistant 2023-12-19 22:36:00 +01:00
Haoyu Xu
63f802648f example_configs: Add zitadel 2023-12-19 22:11:21 +01:00
Valentin Tolmer
1aba962cd3 readme: Fix block quote 2023-12-19 13:42:07 +01:00
Dedy Martadinata S
06697a5305 readme: Add installation from package 2023-12-19 13:34:26 +01:00
Sematre
5a5d5b1d0e example_configs: Add GitLab 2023-12-17 22:46:02 +01:00
Cherryblue
2e0d65e665 example_configs: Update seafile.md for v11
Updating the guide for Seafile v11+, to mention the differences.
2023-12-16 09:08:30 +01:00
Valentin Tolmer
2c54ad895d chore: clippy 2023-12-15 23:37:25 +01:00
Valentin Tolmer
272c84c574 server: make attributes names, group names and emails case insensitive
In addition, group names and emails keep their casing
2023-12-15 23:21:22 +01:00
dependabot[bot]
71d37b9e5e build(deps): bump actions/download-artifact from 3 to 4
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-14 22:08:22 +01:00
dependabot[bot]
c55e0f3bcf build(deps): bump actions/upload-artifact from 3 to 4
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-14 21:55:41 +01:00
Nicholas Malcolm
f2946e6cf6 docs: Fix the Bootstrap script skipping similar name groups
Existing logic used jq's contain which confusingly will do partial string matches. For example a group named "media_admin" will be created then "media" will be skipped saying it already exists.
2023-12-12 04:22:28 +01:00
jakob42
f3e2f8c52d example_configs: Add Kasm configuration example 2023-12-11 10:53:53 +01:00
MinerSebas
70d85524db app: make it possible to serve lldap behind a sub-path 2023-12-07 18:21:49 +01:00
Mohit Raj
ec0737c58a docs(config): clarify docker networking setup 2023-12-03 15:10:51 +01:00
Yevhen Kolomeiko
33f50d13a2 example_configs(bootstrap.sh): Add check is user in group 2023-11-30 11:06:16 +01:00
null
5cd4499328 chore(docs): update jenkins.md
Use the correct Manager DN.
2023-11-23 05:59:35 +01:00
Christian Medel
a65ad14349 example_configs: Add Mastodon and Traccar 2023-11-20 22:05:06 +01:00
Zepmann
2ca5e9e720 Readme: add AUR installation instructions 2023-11-17 07:16:59 +01:00
Valentin Tolmer
4f72153bd4 server: Disallow deleting hardcoded attributes 2023-11-05 16:19:04 +01:00
Valentin Tolmer
829c3f2bb1 server: Prevent regular users from modifying non-editable attributes 2023-11-05 16:06:45 +01:00
themartinslife
a6481dde56 example_configs: add a Jenkins config 2023-11-04 15:41:36 +01:00
Yevhen Kolomeiko
35146ac904 example_configs: Add bootstrap script 2023-11-02 20:49:15 +01:00
Cherryblue
d488802e68 example_configs: Fix display name in wikijs.md
Correction of the display name alias for it to work with wikijs.
2023-11-01 10:23:06 +01:00
nitnelave
927c79bb55 github: Create issue templates 2023-10-30 22:58:52 +01:00
Valentin Tolmer
3b6f24dd17 github: Add CONTRIBUTING guidelines 2023-10-30 22:40:56 +01:00
Valentin Tolmer
8ab900dfce github: update postgres migration sed to handle jwt_storage 2023-10-30 21:59:48 +01:00
Valentin Tolmer
504227eb13 server: Add JWTs to the DB
Otherwise, logging out doesn't actually blacklist the JWT
2023-10-30 21:59:48 +01:00
Hobbabobba
1b97435853 example_configs: Add a working admin user for dokuwiki (#720) 2023-10-30 13:38:13 +01:00
Valentin Tolmer
1fddd87470 server: Simplify RequestFilter's TryInto 2023-10-30 11:31:04 +01:00
dependabot[bot]
af8277dbbd build(deps): bump docker/login-action from 2 to 3
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-30 10:05:11 +01:00
dependabot[bot]
609d0ddb7d build(deps): bump docker/metadata-action from 4 to 5
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 4 to 5.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Upgrade guide](https://github.com/docker/metadata-action/blob/master/UPGRADE.md)
- [Commits](https://github.com/docker/metadata-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-26 13:34:27 +02:00
dependabot[bot]
3df42ae707 build(deps): bump docker/setup-qemu-action from 2 to 3
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-26 08:25:13 +02:00
dependabot[bot]
8f9520b640 build(deps): bump actions/checkout from 4.0.0 to 4.1.1 (#716)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.0.0 to 4.1.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4.0.0...v4.1.1)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-26 04:19:27 +02:00
dependabot[bot]
7c9f61e2eb build(deps): bump docker/build-push-action from 4 to 5 (#677)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-26 03:42:52 +02:00
dependabot[bot]
5275af8f96 build(deps): bump docker/setup-buildx-action from 2 to 3 (#676)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: nitnelave <valentin@tolmer.fr>
2023-10-25 19:55:03 +02:00
Andrew Roberts
0db41f6278 docker: add date-based tagging to matrix jobs 2023-10-23 08:34:24 +02:00
Florian
4574538c76 clippy: fix warning for unwrap_or_default 2023-10-22 20:34:31 +02:00
Florian
9d5714ee0b chore: update repository references 2023-10-22 19:59:36 +02:00
Valentin Tolmer
c6ecf8d58a server: Add graphql support for setting attributes 2023-10-22 16:34:15 +02:00
MI3Guy
9e88bfe6b4 docs: fix primary key in PG migration
When importing data, Postgres doesn't update the auto increment counter for the groups. Creating a group after an import would fail due to duplicate IDs. This manually sets the ID to the max of the IDs + 1.
2023-10-09 16:35:52 +02:00
Simon Broeng Jensen
5bd81780b3 server: Add basic support for Paged Results Control (RFC 2696)
This implements rudimentary support for the Paged
Results Control.

No actual pagination is performed, and we ignore
any requests for specific window sizes for paginated
results.

Instead, the full list of search results is returned
for any searches, and a control is added to the
SearchResultsDone message, informing the client that
there is no further results available.
2023-10-06 13:52:05 +02:00
Simon Broeng Jensen
4fd71ff02f example_configs: Add Apereo CAS Server 2023-10-04 15:02:19 +02:00
dependabot[bot]
f0046692b8 build(deps): bump webpki from 0.22.1 to 0.22.2
Bumps [webpki](https://github.com/briansmith/webpki) from 0.22.1 to 0.22.2.
- [Commits](https://github.com/briansmith/webpki/commits)

---
updated-dependencies:
- dependency-name: webpki
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-04 02:19:09 +02:00
Valentin Tolmer
439fde434b server: Add graphql support for creating/deleting attributes 2023-10-04 02:07:04 +02:00
Valentin Tolmer
2a5fd01439 server: add support for creating a group with attributes 2023-09-29 02:31:20 +02:00
Valentin Tolmer
2c398d0e8e server: Add domain support for creating/deleting attributes 2023-09-29 02:31:20 +02:00
Valentin Tolmer
93e9985a81 server: rename SchemaBackendHandler -> ReadSchemaBackendHandler 2023-09-29 02:31:20 +02:00
stuart938503
ed3be02384 lldap_set_password: Add option to bypass password requirements 2023-09-28 22:39:50 +02:00
Valentin Tolmer
3fadfb1944 server: add support for creating a user with attributes 2023-09-25 01:57:24 +02:00
Valentin Tolmer
81204dcee5 server: add support for updating user attributes 2023-09-25 01:57:24 +02:00
Valentin Tolmer
39a75b2c35 server: read custom attributes from LDAP 2023-09-15 15:26:18 +02:00
Valentin Tolmer
8e1515c27b version: bump to 0.5.1-alpha 2023-09-15 00:52:33 +02:00
Valentin Tolmer
ddfd719884 readme: Update references to nitnelave/lldap to lldap/lldap 2023-09-15 00:28:01 +02:00
Valentin Tolmer
6f04530700 release: 0.5.0 2023-09-14 20:36:32 +02:00
Valentin Tolmer
caf67fdf2b server: Ensure uuid version is at least 1 2023-09-14 20:36:32 +02:00
Valentin Tolmer
034794d58d server: return user-defined attributes for groups in graphql
Part of #67
2023-09-14 13:02:45 +02:00
Valentin Tolmer
e53ce92c96 server: return attributes in graphql
Progress for #67
2023-09-13 22:52:08 +02:00
Charles van Niman
630ac5fd8c example_configs: Add proxmox 2023-09-13 15:14:53 +02:00
Ishan Jain
b269fa0fc7 example_configs: Add thelounge configuration example 2023-09-13 00:29:29 +02:00
Valentin Tolmer
208cc7192e server: Only create the default admin if there are no admins
Fixes #563
2023-09-12 15:54:18 +02:00
MrRulf
80e9145a4f example_configs: Update nextcloud_oidc_authelia.md
Corrected a base URL, points at the nextcloud instance now instead of the authentificator.
Also added additional information for different nextcloud URL configurations.
2023-09-12 14:16:39 +02:00
Valentin Tolmer
78d370d3f4 app: Add a button to clear the avatar 2023-09-12 13:28:46 +02:00
Valentin Tolmer
f279a14693 github: add release bot 2023-09-11 17:49:37 +02:00
Valentin Tolmer
b54bf3c4d5 server: clean up database-mapped types 2023-09-11 17:09:49 +02:00
Valentin Tolmer
582abba793 server: clean up user query
With the new find_with_linked from sea_orm
2023-09-11 17:09:49 +02:00
Valentin Tolmer
94da42ffb9 server: small cleanup 2023-09-11 17:09:49 +02:00
Valentin Tolmer
08d3aef177 server: Update sea-orm, strum 2023-09-11 17:09:49 +02:00
Valentin Tolmer
7671b61a6b server: Add support for querying the OUs 2023-09-11 00:58:15 +02:00
dependabot[bot]
47b308f9b7 build(deps): bump actions/checkout from 3.6.0 to 4.0.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.6.0 to 4.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3.6.0...v4.0.0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 00:35:29 +02:00
dependabot[bot]
1a5931c3df build(deps): bump webpki from 0.22.0 to 0.22.1
Bumps [webpki](https://github.com/briansmith/webpki) from 0.22.0 to 0.22.1.
- [Commits](https://github.com/briansmith/webpki/commits)

---
updated-dependencies:
- dependency-name: webpki
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-10 22:43:24 +02:00
Valentin Tolmer
b3d771e063 server: fix clippy warnings 2023-09-10 22:29:32 +02:00
Valentin Tolmer
134796aa9f server: Switch tests to pretty_assertions 2023-09-10 22:29:32 +02:00
Valentin Tolmer
1598f096e9 server: Upgrade ldap3_proto 2023-09-10 22:29:32 +02:00
Valentin Tolmer
99ed6eface server: Update tracing-forest and take advantage of the span fields 2023-09-10 22:29:32 +02:00
Valentin Tolmer
ce6bf7c548 cargo: Fix getrandom dependency 2023-09-10 22:29:32 +02:00
Charles van Niman
5677ff798f example_configs: add Pdns admin example 2023-08-31 10:32:56 +02:00
Charles van Niman
e47004097a example_configs: Add MinIO configuration 2023-08-31 10:25:49 +02:00
Dedy Martadinata S
5e3a4f3446 github: enable armv7 musl build 2023-08-28 10:42:48 +02:00
dependabot[bot]
8e61ee60d5 build(deps): bump actions/checkout from 3.5.3 to 3.6.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.3 to 3.6.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3.5.3...v3.6.0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-25 15:00:13 +02:00
nitnelave
a426453d7f github: Update rustc version to 1.72, switch to lldap/rust-dev (#657) 2023-08-25 13:50:53 +02:00
Masgalor
1ac9bd0e68 example_configs: Add example config for zulip (#655) 2023-08-24 11:31:36 +02:00
poVoq
a83c305e51 readme: Add links to ergo and thelounge 2023-08-15 15:08:10 +02:00
poVoq
7b171cf59a example_configs: Add ergo.md
For the ergo.chat IRC server
2023-08-15 14:45:11 +02:00
Jacob
b237c71b99 example_configs: Add LibreNMS 2023-08-13 20:40:07 +02:00
poVoq
2eff37684d example_configs: Add config for The Lounge 2023-08-11 07:23:14 +02:00
Chris
836823a5cd Add Zabbix Web example 2023-08-10 20:13:51 +02:00
Chris
e1d4df0b04 example_configs: Update script names in home-assistant.md (#644)
The name of the script did not match the example code and tripped me up until I noticed.
2023-08-10 17:25:39 +02:00
Valentin Tolmer
70bbe7f5ad app: Add the JS warnings to index_local.html 2023-08-05 22:05:30 +02:00
Daniel
6d796df097 app: Add messages to warm any Incompatible browser
Fixes #639
2023-08-05 11:25:55 +02:00
Valentin Tolmer
6cd6b412fe tests: Use an env variable for the private seed 2023-08-04 17:31:22 +02:00
Valentin Tolmer
042429a11d github: fix linguist attributes 2023-08-04 16:09:28 +02:00
Samuel Lorch
c440df631f example_configs: Add Jellyfin Password change 2023-08-03 10:56:17 +02:00
Valentin Tolmer
3247ffc8ea github: only run the coverage after the tests 2023-08-03 10:32:45 +02:00
Valentin Tolmer
ef17c280b1 server: fix smtp encryption parsing 2023-08-03 09:54:12 +02:00
Valentin Tolmer
d0cdfa97c7 server: Add a message ID to sent emails
Fixes #608
2023-08-02 15:34:13 +02:00
Valentin Tolmer
f0bbcfd2c8 set_password: Properly parse the URL, support trailing slashes
Fixes #597
2023-08-02 13:36:22 +02:00
Valentin Tolmer
08b7c6ce33 server: Allow creating a user with multiple objectClass
Fixes #612
2023-08-02 12:15:49 +02:00
Valentin Tolmer
719708dfd0 server: Wrap a lettre error with a friendlier error 2023-08-02 10:38:14 +02:00
Valentin Tolmer
b82cb83318 server: Fix env variable for smtp_encryption
Fixes #611
2023-08-02 10:38:14 +02:00
Valentin Tolmer
d9f4adcb0e ldap: Add support for modifying the password with a modify operation 2023-07-29 12:39:23 +02:00
Valentin Tolmer
e5bc06a617 graphql: sort the groups before returning them 2023-07-29 11:27:50 +02:00
Austin Eschweiler
af49871801 example_configs: Add tandoor recipes 2023-07-27 18:12:36 +02:00
Dedy Martadinata S
7d1f5abc13 dev image: prep for 1.71 (#586)
* Update Dockerfile.dev

* Remove nodejs
* Remove gnu deps
* Add env targeting musl gcc binary
2023-07-18 08:40:18 +07:00
Valentin Tolmer
31a8ba24a0 server,graphql: Add a GraphQL method to get the schema 2023-07-10 17:18:33 +02:00
Valentin Tolmer
9e1b58d033 server,ldap: add encoding for lists and integers 2023-07-10 17:18:33 +02:00
Hobbabobba
1acc8cd78c example_configs: Add squid 2023-07-01 21:49:31 +02:00
Valentin Tolmer
3140af63de server: Use schema to populate attributes 2023-06-29 11:11:20 +02:00
Valentin Tolmer
829ebf59f7 server: Add SchemaBackendHandler trait 2023-06-29 11:11:20 +02:00
Alistair Chapman
4ce145bac2 example_configs: Update Keycloak example for name attributes
Keycloak seems to default to "First name" being `cn` which LLDAP uses for Display Name, resulting in Users getting duplicated display names in Keycloak (like First Last Last), or missing their first name entirely (when they have no DIsplay Name in LLDAP).

This just updates the example config to provide instructions on changing the attribute mapping in Keycloak to fix this.
2023-06-22 10:08:23 +02:00
Mesar Hameed
6ef229f3d0 app: Fix typos that broke accessibility labels 2023-06-18 17:08:54 +02:00
Valentin Tolmer
19b4fd520a scripts: don't export the default attribute schemas 2023-06-15 15:00:52 +02:00
Valentin Tolmer
70146e0b70 server: prepare DB schema for user attributes
First step of #67.
2023-06-14 23:20:37 +02:00
Mitchell Currie
a804368806 Make it more obvious LDAPS is supported (#461)
Update example compose config showing both port and alluding to the environment variable that controls the certificates

Co-authored-by: Dedy Martadinata S <dedyms@proton.me>
2023-06-14 23:32:35 +07:00
Dedy Martadinata S
3ec42fffaa actions: update mariadb healthcheck 2023-06-14 15:14:03 +02:00
dependabot[bot]
95727335a7 build(deps): bump actions/checkout from 3.5.2 to 3.5.3 (#601)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.2 to 3.5.3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3.5.2...v3.5.3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-10 08:03:09 +09:00
Gareth Dunstone
79f9a3a5c2 Update jellyfin.md example (#598)
* Update jellyfin.md example

The LdapAdminBaseDN either doesn't work properly or is used incorrectly here.

This change will make it work.

see: 
- https://github.com/jellyfin/jellyfin-plugin-ldapauth/issues/145

* Update jellyfin.md

Added some more detail about admin groups and user groups.
2023-06-09 09:59:40 +07:00
arcoast
7daebc308b example_configs: Add Mealie configuration 2023-06-07 13:29:41 +02:00
nitnelave
50017cff36 github: create FUNDING.yml 2023-06-07 11:07:13 +09:00
Howard He
f812c9e666 example_configs: fix grafana config 2023-06-02 05:39:47 +02:00
dalz
87a35af693 example_configs: fix dokuwiki config
Previously Dokuwiki couldn't find LDAP groups.
2023-05-21 19:16:26 +09:00
lordratner
4c4a397f66 example_configs: fix typo in home-assistant.md
Error in file name /config/lldap-auth.sh
2023-05-14 10:03:32 +09:00
nitnelave
d720a7812a cargo: set metadata for publishing crates (#577) 2023-05-12 18:06:06 +07:00
nitnelave
d2dec56cca readme: add buymeacoffee link 2023-05-12 16:21:09 +09:00
poVoq
ab2da7b975 example_configs: Add Ejabberd
Basic auth only for now
2023-05-09 14:31:54 +02:00
Anudeep
8f69e4badd example_configs: add chmod to home-assistant instructions 2023-05-06 07:08:41 +02:00
Valentin Tolmer
5bd00f24a2 docker: ignore more files 2023-05-02 16:15:54 +02:00
Valentin Tolmer
ab9ee8d962 tests: allow dead code in common module
We're running afoul of https://github.com/rust-lang/rust/issues/46379,
where each test is compiled independently, so any test that doesn't use
every helper method triggers a dead code warning.
2023-05-02 16:15:54 +02:00
lordratner
852e1586e7 example_configs: Fix a filter in Grafana 2023-05-02 15:45:04 +02:00
Herwig Hochleitner
23b388f3b8 docs: correct env var names in docker template toml 2023-05-02 15:40:16 +02:00
Hobbabobba
22ae2c7124 example_configs: fix zendto memberrole 2023-04-25 20:53:38 +02:00
lordratner
5ad63d31d3 example_docs: add pfsense.md 2023-04-20 18:10:14 +02:00
Tyler Pace
d55d4487ed Add OPNsense example config. (#558)
* Add OPNsense example config.
---------

Co-authored-by: Tyler Pace <tpace@newrelic.com>
2023-04-15 10:06:34 +07:00
Austin Alvarado
4283d27da6 server: Initial stab at e2e tests (#534)
Initial end to end testing. generates unique names for user and groups, and all tests run serially
2023-04-14 11:45:15 -06:00
Valentin Tolmer
4576cf9f2c bump: bump the version to 0.5-alpha, since we have a breaking change 2023-04-14 17:36:04 +02:00
Valentin Tolmer
d1d5d38b32 server: fix incorrect logging 2023-04-14 17:02:00 +02:00
Valentin Tolmer
e5ce98c874 server: Improve the error message in case of duplicate emails 2023-04-14 17:02:00 +02:00
Valentin Tolmer
96b7dbb1c5 server: Make key_seed a secret value 2023-04-14 00:07:54 +02:00
dependabot[bot]
9408b12bc7 build(deps): bump actions/checkout from 3.5.1 to 3.5.2
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.1 to 3.5.2.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3.5.1...v3.5.2)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-13 23:18:22 +02:00
Valentin Tolmer
4e85a4718f server: enforce email and uuid unicity 2023-04-13 17:51:49 +02:00
Valentin Tolmer
d1f1eb8e80 config: Explicit the env variables 2023-04-13 09:22:09 +02:00
Valentin Tolmer
da364746c4 server: Derive the server key from a seed
Fixes #504.
2023-04-13 09:17:05 +02:00
dependabot[bot]
d672f68049 build(deps): bump actions/checkout from 3.5.0 to 3.5.1
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.0 to 3.5.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3.5.0...v3.5.1)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-13 09:03:27 +02:00
Valentin Tolmer
dcca768b6c github: Add more folders to ignore for coverage 2023-04-11 17:17:28 +02:00
Valentin Tolmer
ea69b4bead version: bump to 0.4.4-alpha 2023-04-11 17:17:28 +02:00
Valentin Tolmer
7b4188a376 cargo: update cargo.lock 2023-04-11 17:17:28 +02:00
Valentin Tolmer
252132430c github: always generate artifacts for a release 2023-04-11 15:01:41 +02:00
Valentin Tolmer
7f9bc95c5c release: 0.4.3 2023-04-11 14:41:57 +02:00
Valentin Tolmer
69fca82a86 readme: fix the codecov badge 2023-04-11 14:12:17 +02:00
Valentin Tolmer
9a30cac7b0 healthcheck: check that the server's certificate is the one in the config 2023-04-11 13:51:02 +02:00
Michał Mrozek
558bb37354 server: add support for ec private keys 2023-04-11 10:57:25 +02:00
Dedy Martadinata S
5b74852193 github: Leverage metadata (#532) 2023-04-11 11:03:56 +07:00
Valentin Tolmer
d18cf1ac37 server: decode graphql parameter 2023-04-10 19:10:42 +02:00
Valentin Tolmer
96f55ff28e github: use codecov token only for main push, no token for PRs 2023-04-10 17:29:36 +02:00
Dedy Martadinata S
825f37d360 github: add healthcheck to the test DB services 2023-04-10 17:09:54 +02:00
Austin Alvarado
8eb27c5267 docs: Create Home Assistant config (#535) 2023-04-07 14:59:21 -06:00
budimanjojo
18d9dd6ff9 github: also push to ghcr.io and add docker.io/lldap/lldap 2023-04-05 17:51:23 +02:00
Austin Alvarado
308521c632 Use jq in CI to extract json deterministically (#529)
cut relies on the string being a fixed length, which is subject to change in the  future
2023-04-05 22:20:15 +07:00
Valentin Tolmer
86b2b5148d server: remove default value for SMTP user
Otherwise, not setting the user would default to "admin", which breaks
the unauthenticated workflow. No user specified should mean unauthenticated.

Fixes #520.
2023-04-04 16:27:44 +02:00
Valentin Tolmer
b9e0e4a6dc version: bump cargo.lock 2023-04-04 16:27:44 +02:00
nitnelave
1b8849ead1 version: bump to 0.4.3-alpha (#522) 2023-04-04 13:00:17 +02:00
amiga23
1fe635384f docs: Add email attribute to nextcloud config
Otherwise nextcloud will not set the email address in users profile
2023-04-04 12:14:41 +02:00
Hobbabobba
df16d66753 added Shaarli configuration example (#519)
* Create shaarli.md

* added Shaarli doc

* fixed uid
2023-04-03 18:54:39 +02:00
nitnelave
65e2c24928 github: Add CODEOWNERS 2023-03-31 10:42:53 +02:00
Austin Alvarado
c4b8621e2a app: Fix password reset redirection (#513)
* Fix password reset redirection
* Add password reset enable flag
2023-03-30 09:47:41 -06:00
Valentin Tolmer
88a9f8a97b github: fix github_ref reference 2023-03-28 20:59:38 +02:00
Valentin Tolmer
fc91d59b99 github: Don't skip rebuilding a docker image on main because it was built on a branch 2023-03-28 19:34:43 +02:00
Valentin Tolmer
aad4711056 app: server uncompressed WASM to webkit browsers 2023-03-28 17:33:13 +02:00
Dedy Martadinata S
c7c6d95334 docker: Add DB migration tests in the CI 2023-03-28 13:59:23 +02:00
Valentin Tolmer
84b4c66309 cargo: Update Cargo.lock with latest release 2023-03-28 12:10:04 +02:00
Valentin Tolmer
923d77072b gitattributes: Tag folders as docs, generated or ignored for linguist 2023-03-28 12:10:04 +02:00
Austin Alvarado
758aa7f7f7 docs: Fix md links 2023-03-27 18:08:27 +02:00
Valentin Tolmer
866a74fa29 github: Reduce actions trigger on metadata updates 2023-03-27 16:52:34 +02:00
Valentin Tolmer
36a51070b3 docker: ignore README 2023-03-27 16:52:34 +02:00
Valentin Tolmer
585b65e11d README: Add details about other DBs, migrations 2023-03-27 14:12:00 +02:00
Valentin Tolmer
2c8fe2a481 Revert "workflows: allow action to upload artifacts"
This reverts commit 1b67bad270.
2023-03-27 13:53:21 +02:00
Valentin Tolmer
1b67bad270 workflows: allow action to upload artifacts 2023-03-27 12:45:11 +02:00
Valentin Tolmer
afe91c7cc0 release: 0.4.2 2023-03-27 11:07:24 +02:00
Valentin Tolmer
bd1b7e8809 server: update base64 2023-03-27 10:46:16 +02:00
Valentin Tolmer
ae9b04d4d2 worflows: add codecov token 2023-03-27 10:02:47 +02:00
dependabot[bot]
bd6184554a build(deps): bump actions/checkout from 3.4.0 to 3.5.0 (#494)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.4.0 to 3.5.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3.4.0...v3.5.0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-27 09:14:26 +02:00
Austin Alvarado
834d68a47e docs: fix DB migration, add sqlite migration helper script 2023-03-22 17:07:10 +01:00
Austin Alvarado
05dbe6818d server: Create schema command 2023-03-21 14:16:19 +01:00
Austin Alvarado
80dfeb1293 app: Implement dark theme and toggle 2023-03-21 10:50:17 +01:00
Valentin Tolmer
bf64c091cc docker: Update dockerfiles to build the lldap_set_password tool 2023-03-21 00:03:09 +01:00
Valentin Tolmer
b4d7ada317 lldap_set_password: create the new tool
Fixes #473.
2023-03-21 00:03:09 +01:00
Valentin Tolmer
a07f7ac389 server: ensure first/last name nullable, make avatar long blob in DB
Fixes #474, #486.
2023-03-20 23:42:47 +01:00
Valentin Tolmer
46b8f2a8a5 server: return groups in memberof by cn instead of uid
Fixes #468.
2023-03-20 22:10:38 +01:00
Austin Alvarado
91ada70c7d vscode: Update devcontainer and build instructions 2023-03-20 21:29:54 +01:00
Valentin Tolmer
b2cfc0ed03 app: update yew to 0.19
This is a massive change to all the components, since the interface
changed.

There are opportunities to greatly simplify some components by turning
them into functional_components, but this work has tried to stay as
mechanical as possible.
2023-03-20 12:11:34 +01:00
Valentin Tolmer
8d44717588 app: replace ConsoleService with gloo_console 2023-03-20 12:11:34 +01:00
Valentin Tolmer
f44e8b7659 app: wrap template arguments in braces
To prepare for the migration to yew 1.19
2023-03-20 12:11:34 +01:00
amiga23
07523219d1 docs(dex): Fix group search
The userAttr needs to be the full DN, otherwise the search does not work:
```
❯ ldapsearch -x -H ldap://localhost:3890 -D "cn=admin,ou=people,dc=example,dc=com" -b "ou=groups,dc=example,dc=com" -W "member=bob"
Enter LDAP Password: 
# extended LDIF
#
# LDAPv3
# base <ou=groups,dc=example,dc=com> with scope subtree
# filter: member=bob
# requesting: ALL
#

# search result
search: 2
result: 53 Server is unwilling to perform
text: Unsupported group filter: while parsing a user ID: Missing DN value

# numResponses: 1
```
2023-03-18 00:07:40 +01:00
dependabot[bot]
7f76e2095d build(deps): bump actions/checkout from 3.3.0 to 3.4.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.3.0 to 3.4.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3.3.0...v3.4.0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-17 18:18:13 +01:00
Valentin Tolmer
313fe3e0b7 clippy: fix new warning 2023-03-17 18:13:10 +01:00
Austin Alvarado
c817b31dfc docs: Add DB migration docs 2023-03-17 17:49:24 +01:00
Dedy Martadinata S
9e038f5218 docker: use correct username for chown 2023-03-17 16:23:53 +01:00
Valentin Tolmer
9e479d38fe app: get rid of rollup, gzip the wasm 2023-03-05 16:31:56 +01:00
Valentin Tolmer
2593606f16 docs: add docs about scripting 2023-03-03 16:04:08 +01:00
Valentin Tolmer
1b91cc8ac2 server: update clap and mockall 2023-03-02 10:51:38 +01:00
Valentin Tolmer
28607c4744 server: update various dependencies 2023-03-02 10:51:38 +01:00
Valentin Tolmer
dce73f91ef server: update actix, inline juniper-actix 2023-03-02 10:51:38 +01:00
Valentin Tolmer
07de6062ca server: update tokio 2023-03-02 10:51:38 +01:00
Valentin Tolmer
c9997d4c17 server: statically enforce access control 2023-03-01 11:28:04 +01:00
Luca Tagliavini
322bf26db5 server: allow non authenticated smtp connections 2023-02-25 18:56:49 +01:00
carolosf
98acd68f06 example_configs: Add example for Sonatype Nexus Repository Manager 3 2023-02-23 09:33:35 +01:00
WS
733f990858 example_configs: Add Rancher example 2023-02-20 15:27:00 +01:00
Valentin Tolmer
bebb00aa2e app: improve error message for wrong/expired reset token 2023-02-15 14:43:26 +01:00
Valentin Tolmer
193a0fd710 server: Remove trailing / from the domain URL 2023-02-15 14:43:26 +01:00
Valentin Tolmer
3650a438df docker: fix healthcheck 2023-02-15 11:10:32 +01:00
arcoast
5bee73180d example_configs: add authentik configuration
This should import users, groups & memberships
2023-02-14 18:22:49 +01:00
Valentin Tolmer
672dd96e7e server: add content-type header to the email 2023-02-14 11:22:22 +01:00
Valentin Tolmer
62104b417a app: probe for password reset support 2023-02-13 20:24:20 +01:00
Valentin Tolmer
562ad524c4 server: only add password reset routes if they are enabled 2023-02-13 20:24:20 +01:00
Valentin Tolmer
ea498df78b server: add a test for compare with uniqueMember 2023-02-13 19:31:12 +01:00
Valentin Tolmer
1ce239103c server: removed dbg 2023-02-13 16:14:52 +01:00
Valentin Tolmer
81036943c2 server: Add support for SubString ldap filter 2023-02-13 16:10:14 +01:00
Valentin Tolmer
21e51c3d38 server: Add support for LdapCompare op 2023-02-13 12:59:53 +01:00
DarkSpir
e92947fc3b app: Change input field to password type in change_password ui (#443)
Change input field type for field old_password from its default "text" to "password"

Fixes #442
2023-02-13 09:29:54 +01:00
Juli
94d45f7320 example_configs: Added explanation to Jellyfin Docs 2023-02-12 11:10:52 +01:00
Valentin Tolmer
d04305433f server: use the new into_tuple from sea_orm 2023-02-10 12:57:38 +01:00
Valentin Tolmer
63cbf30dd7 server: upgrade sea-orm to 0.11 2023-02-10 12:57:38 +01:00
Valentin Tolmer
96eb17a963 server: fix clippy warning
The clippy::uninlined_format_args warning in 1.67 was downgraded to
pedantic in 1.67.1 due to lack of support in rust-analyzer, so we're not
updating that one yet.
2023-02-10 12:03:23 +01:00
Valentin Tolmer
8f2c5b397c server: allow NULL for display_name
Fixes #387.
2023-02-10 11:19:22 +01:00
Rex Zhang
648848c816 example_configs: Add note for Gitea's simple auth mode 2023-02-08 10:30:23 +01:00
Diptesh Choudhuri
58b9c28a0b example_configs: Add Dex example
Fixes #428.
2023-02-01 13:02:52 +01:00
dependabot[bot]
c3d18dbbe8 build(deps): bump docker/build-push-action from 3 to 4
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3 to 4.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-31 11:26:05 +01:00
dependabot[bot]
1e6a0edcfb build(deps): bump bumpalo from 3.10.0 to 3.12.0
Bumps [bumpalo](https://github.com/fitzgen/bumpalo) from 3.10.0 to 3.12.0.
- [Release notes](https://github.com/fitzgen/bumpalo/releases)
- [Changelog](https://github.com/fitzgen/bumpalo/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fitzgen/bumpalo/compare/3.10.0...3.12.0)

---
updated-dependencies:
- dependency-name: bumpalo
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-24 14:55:43 +01:00
Valentin Tolmer
d56de80381 server: Update lettre 2023-01-24 14:39:58 +01:00
Valentin Tolmer
3fa100be0c server: update sea-orm dependency
Fixes #405
2023-01-24 10:51:19 +01:00
Dedy Martadinata S
df1169e06d docker: simplify binary build, add db integration test 2023-01-22 11:10:26 +01:00
arcoast
0ae1597ecd example_configs: Add Wikijs example
In response to https://github.com/nitnelave/lldap/pull/424#discussion_r1083280235
2023-01-22 09:49:00 +01:00
Igor Rzegocki
d722be8896 server: add option to use insecure SMTP connection 2023-01-19 11:30:25 +01:00
Valentin Tolmer
9018e6fa34 server, refactor: Add a conversion from bool for the filters 2023-01-17 15:09:06 +01:00
Luca Tagliavini
807fd10d13 server: Add support for DN filters 2023-01-17 14:21:57 +01:00
Valentin Tolmer
f979e16b95 server: Fix healthcheck return code
The healthcheck was not returning a non-zero code when failing, due to
an extra layer of Results
2023-01-16 17:35:08 +01:00
Valentin Tolmer
955a559c21 clippy: fix warning 2023-01-13 15:50:03 +01:00
Valentin Tolmer
e458aca3e3 db: Change the DB storage type to NaiveDateTime
The entire internals of the server now work using only NaiveDateTime,
since we know they are all UTC. At the fringes (LDAP, GraphQL, JWT
tokens) we convert back into UTC to make sure we have a clear API.

This allows us to be compatible with Postgres (which doesn't support
DateTime<UTC>, only NaiveDateTime).

This change is backwards compatible since in SQlite with
Sea-query/Sea-ORM, the UTC datetimes are stored without a timezone, as
simple strings. It's the same format as NaiveDateTime.

Fixes #87.
2023-01-13 15:50:03 +01:00
Valentin Tolmer
692bbb00f1 db: Change the version number from u8 to i16
This is the smallest integer compatible with all of MySQL, Postgres and
SQlite.

This is a backwards-compatible change for SQlite since both are
represented as "integer", and all u8 values can be represented as i16.
2023-01-13 15:50:03 +01:00
poVoq
260b545a54 example_configs,gitea: add additional attributes and group sync
Not extensively tested, but group/team sync seems to work in Forgejo.
2023-01-09 17:53:44 +01:00
Dedy Martadinata S
3a43b7a4c2 docker: simplify ci and better package release artifacts 2023-01-06 16:34:22 +01:00
dependabot[bot]
c87adfeecc build(deps): bump actions/checkout from 3.2.0 to 3.3.0 (#410)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.2.0 to 3.3.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3.2.0...v3.3.0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-06 13:13:54 +01:00
Dedy Martadinata S
d7cc10fa00 ci: fetch missing web components 2023-01-05 15:36:01 +01:00
Austin Alvarado
14531fa258 docker: upgrade alpine in base dockerfile
This allows us to upgrade rustc to past 1.65, which is required by sea-orm.
2023-01-04 08:24:40 +01:00
Austin Alvarado
1e5603dce2 docker: Add VSCode devcontainer 2023-01-03 18:11:59 +01:00
dependabot[bot]
c64d32e2c0 build(deps): bump actions/checkout from 3.1.0 to 3.2.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.1.0 to 3.2.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3.1.0...v3.2.0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-12 21:23:30 +01:00
Valentin Tolmer
665e525f0a server: fix user password setting
It used to try to set all user IDs to the same, which would fail if
there is more than 1 user.
2022-11-30 08:38:21 +01:00
Valentin Tolmer
09a0522e2d server: move domain types to a separate file 2022-11-25 15:35:48 +01:00
Valentin Tolmer
e89b1538af server,app: migrate to sea-orm 2022-11-25 15:35:48 +01:00
Waldemar Heinze
a3a27f0049 docker: update Rust to v1.65.0 2022-11-25 10:07:50 +01:00
Waldemar Heinze
a4408cfacc server: prefer immutable path 2022-11-25 00:14:29 +01:00
Waldemar Heinze
a3216a4550 server: fix clippy's suggestions 2022-11-24 23:52:57 +01:00
Waldemar Heinze
2668ea4553 server: make host configurable to enable IPv6 support
This change also separates the API host and the LDAP host for further customization.
2022-11-24 23:39:11 +01:00
Michał Mrozek
dd7e392626 server: use async api for email sending
Fixes #378
2022-11-24 14:47:56 +01:00
MrOnak
80fc94c4db example_configs: Add Kanboard 2022-11-22 12:13:27 +01:00
Norm
ffc59af345 example_configs: Update Nextcloud and add tutorial for OIDC with Authelia 2022-11-18 14:05:16 +01:00
dependabot[bot]
03ad10dfc5 build(deps): bump Swatinem/rust-cache from 1 to 2
Bumps [Swatinem/rust-cache](https://github.com/Swatinem/rust-cache) from 1 to 2.
- [Release notes](https://github.com/Swatinem/rust-cache/releases)
- [Changelog](https://github.com/Swatinem/rust-cache/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Swatinem/rust-cache/compare/v1...v2)

---
updated-dependencies:
- dependency-name: Swatinem/rust-cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-18 11:05:43 +01:00
Hobbabobba
eb26019a52 example_configs: Added zendto
* Create zendto.md

* Update README.md
2022-11-10 11:06:29 +01:00
Hobbabobba
69d0308f46 example_configs: Add vaultwarden sync 2022-11-04 15:34:14 +01:00
Lewis Larsen
ba0dc33583 app: front end improvements
Added colour to required asterisks
    Added padding to the footer
    Added bootstrap class to select elements
    Added various icons to buttons
    Fixed various button layouts
    Reworded some messages
    Moved around some form elements

 Fixes #12
2022-11-03 15:40:02 +01:00
Dedy Martadinata S
e0c0efcb2f readme: use nodejs 16 for the build instructions
As there are many distributions, and different nodejs out there, define nodejs version to match the CI build.
2022-11-02 08:19:31 +01:00
Igor Rzegocki
e3b1810229 docker: add tzdata package, to support TZ env variable 2022-11-01 12:57:49 +01:00
Valentin Tolmer
e81c87f288 server,app: Add support for resetting your password with email
Instead of just username

Fixes #267
2022-10-29 15:04:45 +02:00
Valentin Tolmer
234cb70b97 server: fix handling of present filters
If the filter name was not in the list of attributes to return, it
wouldn't be counted as a valid attribute, meaning that the aliases of
attributes were not recognized.

Fixes #351
2022-10-26 09:29:02 +02:00
Indrek Haav
201e3a93eb Ensure generated JWT doesn't include quotation mark 2022-10-20 13:42:19 +02:00
Valentin Tolmer
27144ee37e server: Add support for creating a user through LDAP 2022-10-20 10:09:17 +02:00
Valentin Tolmer
2477439ecc server: Improve rootDSE
Matches the case-insensitive "objectclass" filter, fix the reported
version, and declares the name context and some other attributes.

Potential fix to #330.
2022-10-19 17:35:45 +02:00
Valentin Tolmer
ff66e918cf server: increase max payload size to 16MB
Fixes #337
2022-10-19 17:28:25 +02:00
Valentin Tolmer
ee7dc39afa example_configs: Add quotes to authelia filters 2022-10-19 14:13:55 +02:00
Valentin Tolmer
4c69f917e7 server: Improve equality handling in filters
Now the columns are checked and mapped to user columns, to avoid any
ambiguity.

Fixes #341.
2022-10-19 08:43:38 +02:00
Valentin Tolmer
8d19678e39 server: refactor sql backend handler
And add some missing tests
2022-10-18 13:04:59 +02:00
Valentin Tolmer
bf42517077 nextest: add configuration 2022-10-17 14:39:44 +02:00
Valentin Tolmer
35aa656677 server: refactor ldap_handler
Split it into several files, move them into the domain folder, introduce
`LdapError` for better control flow.
2022-10-17 14:39:44 +02:00
Valentin Tolmer
0be440efc8 server: Start versioning the DB schema
In preparation for #67.
2022-10-17 09:38:37 +02:00
Roman
eefe65c042 example_configs: Add Dell iDrac 2022-10-16 16:10:47 +02:00
Roman
a42a532929 example_configs: Add WeKan sample config 2022-10-15 14:42:16 +02:00
Hobbabobba
3bb07db63f example_configs: fix docuwiki group filter 2022-10-12 22:22:48 +02:00
Valentin Tolmer
32850d4ff9 ldap: add entryUUID to the default fields
It should help with #293.
2022-10-12 18:35:40 +02:00
Dedy Martadinata
92178d2e77 github: automate release artifact creation 2022-10-12 17:49:41 +02:00
Valentin Tolmer
d592b10c87 docker: install gosu 2022-10-12 16:44:52 +02:00
Valentin Tolmer
188a92d124 docker: add healthcheck 2022-10-12 16:44:52 +02:00
Valentin Tolmer
3aaf53442b server: implement healthcheck 2022-10-12 16:44:52 +02:00
Valentin Tolmer
01d4b6e1fc lock: update Cargo.lock 2022-10-12 11:14:42 +02:00
Floris
a2dfca0e37 readme: Update to reflect new stable user env settings 2022-10-11 09:31:26 +02:00
Valentin Tolmer
b3f64c6efe Bump version to 0.4.2-alpha 2022-10-10 21:09:34 +02:00
Valentin Tolmer
32f28d664e Bump to version 0.4.1 2022-10-10 17:46:34 +02:00
Hobbabobba
412f4fa644 example_config: add Docuwiki 2022-10-09 13:11:26 +02:00
dependabot[bot]
4ffa565e51 build(deps): bump actions/checkout from 2 to 3.1.0 (#314)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.1.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3.1.0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: nitnelave <valentin.tolmer@gmail.com>
2022-10-08 06:33:30 +02:00
Hobbabobba
2f9ea4f10f example_configs: add hedgedoc
Co-authored-by: nitnelave <valentin.tolmer@gmail.com>
2022-10-07 21:19:55 +02:00
Dedy Martadinata
123fdc5baf docker: use the locally-downloaded assets
Change the index to the local one to use the locally-downloaded fonts and css.
2022-10-07 19:22:20 +02:00
Valentin Tolmer
5402aa5aa2 server: Silence error message when creating DB
Fixes #300
2022-09-30 15:12:15 +02:00
Valentin Tolmer
8069516283 server: Add support for PKCS1 keys
Fixes #288
2022-09-30 13:56:03 +02:00
Valentin Tolmer
6c21f2ef4b clippy: fix warning by implementing Eq 2022-09-27 06:54:29 +02:00
Valentin Tolmer
516893f1f7 server: Fix query building of chained ands/ors
Fixes #303
2022-09-27 05:14:57 +02:00
Marco Dura
1660cb1fbb example_config: grafana fix typos and attributes 2022-09-22 15:22:30 +02:00
Valentin Tolmer
7e1ce10df1 server: allow every config value to be specified as a file
By using https://crates.io/crates/figment_file_provider_adapter

Fixes https://github.com/nitnelave/lldap/issues/263
2022-09-14 11:16:50 +02:00
arcoast
b6ee918ca9 example_configs: add Airsonic 2022-09-10 14:32:56 +02:00
Valentin Tolmer
24efd61464 github: fix static wasm-pack build 2022-09-10 13:12:20 +02:00
Valentin Tolmer
0b6b274cfa example_configs: add Nextcloud 2022-09-10 12:47:41 +02:00
Alexander Olsson
8b01271e94 readme: Fix typo
Co-authored-by: alol <alexander.olsson@flaxplax.com>
2022-08-16 16:53:35 -05:00
Valentin Tolmer
d536addf0a migration-tool: misc cleanup 2022-08-09 13:03:28 +02:00
Valentin Tolmer
2ca083541e migration-tool: Import users' avatars 2022-08-09 13:03:28 +02:00
Valentin Tolmer
686bdc0cb1 app: Add support for modifying an avatar 2022-08-09 13:03:28 +02:00
Valentin Tolmer
60c594438c ldap: Stop returning empty attributes 2022-08-09 13:03:28 +02:00
Valentin Tolmer
b130965264 ldap: return user's avatar 2022-08-09 13:03:28 +02:00
Valentin Tolmer
697a64991d server: Change attribute values to bytes 2022-08-09 13:03:28 +02:00
Valentin Tolmer
3acc448048 server: Add support for users' avatars in GrahpQL 2022-08-09 13:03:28 +02:00
Valentin Tolmer
0e3c5120da app: Switch yew_form dependency back to main repo 2022-08-09 13:03:28 +02:00
Valentin Tolmer
7707367c35 migration-tool: Extract the JWT from the JSON response
The response used to contain just the JWT, but now it's wrapped in JSON.

Fixes #282.
2022-08-03 22:34:19 +02:00
Valentin Tolmer
122e08790f docker: fix tag typo 2022-08-01 18:26:47 +02:00
Valentin Tolmer
64556fc744 server: stop returning "dn" as an attribute
It's already part of the base response

Fixes #254.
2022-08-01 18:26:47 +02:00
Valentin Tolmer
134a9366f5 server: create private key with 400 permissions
Fixes #261.
2022-08-01 17:43:37 +02:00
Dedy Martadinata S
f69b729eb2 Update docker-build-static.yml 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
2ac47d5c85 cleanup Dockerfile.dev 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
26d3d84de0 use lldap dev image 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
b413935932 some cleanup 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
e6ae726304 manual fetch aarch64 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
520277b611 update tag 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
8cdfedddbd add os in tag 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
5312400a3f add Dockerfile.dev 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
551f5abc4b manual fetch 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
10d826fc46 using script 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
252bd6cf39 use custom dev image 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
ba44dea7b6 add ca-cert 2022-08-01 17:31:13 +02:00
Dedy Martadinata S
b9c823e01a Build using musl 2022-08-01 17:31:13 +02:00
Valentin Tolmer
c108921dcf server: Add a log message when search is restricted
Fixes #264.
2022-08-01 14:02:24 +02:00
Valentin Tolmer
36eed1e091 README: Document the build process, add systemd service
Fixes #269.
2022-08-01 09:14:39 +02:00
Valentin Tolmer
897704fab3 server: Fix extra error message when DB doesn't exist
Fixes #270
2022-08-01 09:14:39 +02:00
Valentin Tolmer
9f70910283 docs: Update the docker config template to add smtp_encryption 2022-08-01 09:14:39 +02:00
dependabot[bot]
3e3c9b97ae build(deps): bump juniper from 0.15.9 to 0.15.10
Bumps [juniper](https://github.com/graphql-rust/juniper) from 0.15.9 to 0.15.10.
- [Release notes](https://github.com/graphql-rust/juniper/releases)
- [Changelog](https://github.com/graphql-rust/juniper/blob/master/release.toml)
- [Commits](https://github.com/graphql-rust/juniper/compare/juniper-v0.15.9...juniper-v0.15.10)

---
updated-dependencies:
- dependency-name: juniper
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-30 19:22:59 +02:00
Valentin Tolmer
8c1ea11b95 server: add an option to use STARTTLS for smtp 2022-07-30 15:58:58 +02:00
Valentin Tolmer
cd0ab378ef server: deprecate smtp.tls_required, add smtp_encryption 2022-07-30 15:58:58 +02:00
Matteo Bonora
5a27ae4862 readme: Add version to docker-compose examples 2022-07-29 10:56:24 +02:00
Jun-Cheol Park
05719642ca Fix: Change input filed to password type in change_password ui (#273) 2022-07-26 11:07:44 +02:00
Iván Izaguirre
5c584536b5 frontend: Add UUID and creation date
This exposes the new info in the GraphQL API, and adds it to the frontend.
2022-07-21 12:10:37 +02:00
Valentin Tolmer
4ba0db4e9e migration_tool: Switch from OpenSSL to Rustls 2022-07-15 15:49:15 +02:00
Valentin Tolmer
5e4ed9ee17 docker: remove libssl-dev 2022-07-15 15:49:15 +02:00
Valentin Tolmer
c399ff2bfa server: switch from OpenSSL to Rustls 2022-07-15 15:49:15 +02:00
Frank Moskal
9e37a06514 server: allow admin email to be set via config 2022-07-13 14:32:35 +02:00
Valentin Tolmer
294ce77a47 server: Fix misc clippy warnings 2022-07-13 12:43:51 +02:00
Dedy Martadinata S
24c6b4a879 docker: Remove debian build, revert to default alpine 2022-07-13 11:41:26 +02:00
Jacob Yundt
2c2696a8c3 docker: Add support for SMTP password file (#240)
Similar to LLDAP_JWT_SECRET and LLDAP_LDAP_USER_PASS, add option to use an
ENV variable to specify the file of the SMTP password:
LLDAP_SMTP_OPTIONS__PASSWORD_FILE
2022-07-13 10:52:40 +02:00
Dedy Martadinata S
479d1e7635 readme: Add details about latest tag 2022-07-13 10:38:31 +02:00
Dedy Martadinata S
3a723460e5 docker: Add volume support 2022-07-13 08:09:36 +02:00
Valentin Tolmer
8011756658 readme: Correct RAM estimate 2022-07-12 12:37:27 +02:00
Dedy Martadinata S
46546dac27 docker: Add support for UID:GID
Adds support for the UID/GID env variables in Docker via `gosu`.
2022-07-12 10:37:08 +02:00
Dedy Martadinata S
9a869a1474 add bash, entrypoint deps. 2022-07-11 16:09:22 +02:00
Sebastian Thiel
09797695aa Fix typo in README.md 2022-07-11 15:38:11 +02:00
Dedy Martadinata S
4f2cf45427 docker: build static binaries, add alpine targets 2022-07-11 15:36:59 +02:00
Valentin Tolmer
901eb7f469 example_configs: Add XBackBone 2022-07-11 12:30:46 +02:00
216 changed files with 21514 additions and 6729 deletions

2
.config/nextest.toml Normal file
View File

@@ -0,0 +1,2 @@
[profile.default]
fail-fast = false

26
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,26 @@
FROM rust:1.72
ARG USERNAME=lldapdev
# We need to keep the user as 1001 to match the GitHub runner's UID.
# See https://github.com/actions/checkout/issues/956.
ARG USER_UID=1001
ARG USER_GID=$USER_UID
# Create the user
RUN groupadd --gid $USER_GID $USERNAME \
&& useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
&& apt-get update \
&& apt-get install -y sudo \
&& echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
&& chmod 0440 /etc/sudoers.d/$USERNAME
RUN apt update && \
apt install -y --no-install-recommends libssl-dev musl-dev make perl curl gzip && \
rm -rf /var/lib/apt/lists/*
RUN RUSTFLAGS=-Ctarget-feature=-crt-static cargo install wasm-pack \
&& rustup target add wasm32-unknown-unknown
USER $USERNAME
ENV CARGO_HOME=/home/$USERNAME/.cargo
ENV SHELL=/bin/bash

View File

@@ -0,0 +1,8 @@
{
"name": "LLDAP dev",
"build": { "dockerfile": "Dockerfile" },
"forwardPorts": [
3890,
17170
]
}

View File

@@ -2,6 +2,7 @@
.git/*
.github/*
.gitignore
.gitattributes
# Don't track cargo generated files
target/*
@@ -17,6 +18,7 @@ Dockerfile
*.md
LICENSE
CHANGELOG.md
README.md
docs/*
example_configs/*
@@ -28,12 +30,24 @@ package.json
# Pre-build binaries
*.tar.gz
# VSCode dirs
.vscode
.devcontainer
# Created databases
*.db
*.db-shm
*.db-wal
# These are backup files generated by rustfmt
**/*.rs.bk
# Various config files that shouldn't be tracked
.env
lldap_config.toml
server_key
users.db*
screenshot.png
recipe.json
lldap_config.toml
cert.pem
key.pem

10
.gitattributes vendored Normal file
View File

@@ -0,0 +1,10 @@
example_configs/** linguist-documentation
docs/** linguist-documentation
*.md linguist-documentation
lldap_config.docker_template.toml linguist-documentation
schema.graphql linguist-generated
.github/** -linguist-detectable
.devcontainer/** -linguist-detectable
.config/** -linguist-detectable

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
* @nitnelave

5
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
# These are supported funding model platforms
github: [lldap]
custom: ['https://bmc.link/nitnelave']

29
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,29 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Logs**
If applicable, add logs to explain the problem.
LLDAP should be started in verbose mode (`LLDAP_VERBOSE=true` env variable, or `verbose = true` in the config). Include the logs in triple-backtick "```"
If integrating with another service, please add its configuration (paste it or screenshot it) as well as any useful logs or screenshots (showing the error, for instance).
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE REQUEST]"
labels: enhancement
assignees: ''
---
**Motivation**
Why do you want the feature? What problem do you have, what use cases would it enable?
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered. You can include workarounds that are currently possible.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,25 @@
---
name: Integration request
about: Request for integration with a service
title: "[INTEGRATION]"
labels: integration
assignees: ''
---
**Checklist**
- [ ] Check if there is already an [example config](https://github.com/lldap/lldap/tree/main/example_configs) for it.
- [ ] Try to figure out the configuration values for the new service yourself.
- You can use other example configs for inspiration.
- If you're having trouble, you can ask on [Discord](https://discord.gg/h5PEdRMNyP) or create an issue.
- If you succeed, make sure to contribute an example configuration, or a configuration guide.
- If you hit a block because of an unimplemented feature, create an issue.
**Description of the service**
Quick summary of what the service is and how it's using LDAP. Link to the service's documentation on configuring LDAP.
**What you've tried**
A sample configuration that you've tried.
**What's not working**
Error logs, error screenshots, features that are not working, missing features.

2
.github/codecov.yml vendored
View File

@@ -10,3 +10,5 @@ ignore:
- "docs"
- "example_configs"
- "migration-tool"
- "scripts"
- "set-password"

View File

@@ -1,68 +0,0 @@
FROM debian:bullseye AS lldap
ARG DEBIAN_FRONTEND=noninteractive
ARG TARGETPLATFORM
RUN apt update && apt install -y wget
WORKDIR /dim
COPY bin/ bin/
COPY web/ web/
RUN mkdir -p target/
RUN mkdir -p /lldap/app
RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \
mv bin/amd64-bin/lldap target/lldap && \
mv bin/amd64-bin/migration-tool target/migration-tool && \
chmod +x target/lldap && \
chmod +x target/migration-tool && \
ls -la target/ . && \
pwd \
; fi
RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
mv bin/aarch64-bin/lldap target/lldap && \
mv bin/aarch64-bin/migration-tool target/migration-tool && \
chmod +x target/lldap && \
chmod +x target/migration-tool && \
ls -la target/ . && \
pwd \
; fi
RUN if [ "${TARGETPLATFORM}" = "linux/arm/v7" ]; then \
mv bin/armhf-bin/lldap target/lldap && \
mv bin/armhf-bin/migration-tool target/migration-tool && \
chmod +x target/lldap && \
chmod +x target/migration-tool && \
ls -la target/ . && \
pwd \
; fi
# Web and App dir
COPY docker-entrypoint.sh /docker-entrypoint.sh
COPY lldap_config.docker_template.toml /lldap/
RUN cp target/lldap /lldap/ && \
cp target/migration-tool /lldap/ && \
cp -R web/index.html \
web/pkg \
web/static \
/lldap/app/
RUN set -x \
&& for file in $(cat /lldap/app/static/libraries.txt); do wget -P app/static "$file"; done \
&& for file in $(cat /lldap/app/static/fonts/fonts.txt); do wget -P app/static/fonts "$file"; done \
&& chmod a+r -R .
FROM debian:bullseye
ENV UID=1000
ENV GID=1000
ENV USER=lldap
RUN apt update && \
apt install -y --no-install-recommends tini ca-certificates && \
apt clean && \
rm -rf /var/lib/apt/lists/* && \
groupadd -g $GID $USER && useradd --system -m -g $USER --uid $UID $USER
COPY --from=lldap --chown=$CONTAINERUSER:$CONTAINERUSER /lldap /app
COPY --from=lldap --chown=$CONTAINERUSER:$CONTAINERUSER /docker-entrypoint.sh /docker-entrypoint.sh
WORKDIR /app
USER $USER
ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"]
CMD ["run", "--config-file", "/data/lldap_config.toml"]

30
.github/workflows/Dockerfile.ci.alpine vendored Normal file
View File

@@ -0,0 +1,30 @@
FROM localhost:5000/lldap/lldap:alpine-base
# Taken directly from https://github.com/tianon/gosu/blob/master/INSTALL.md
ENV GOSU_VERSION 1.17
RUN set -eux; \
\
apk add --no-cache --virtual .gosu-deps \
ca-certificates \
dpkg \
gnupg \
; \
\
dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
\
# verify the signature
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
gpgconf --kill all; \
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
\
# clean up fetch dependencies
apk del --no-network .gosu-deps; \
\
chmod +x /usr/local/bin/gosu; \
# verify that the binary works
gosu --version; \
gosu nobody true
COPY --chown=$USER:$USER docker-entrypoint.sh /docker-entrypoint.sh

View File

@@ -0,0 +1,84 @@
FROM debian:bullseye AS lldap
ARG DEBIAN_FRONTEND=noninteractive
ARG TARGETPLATFORM
RUN apt update && apt install -y wget
WORKDIR /dim
COPY bin/ bin/
COPY web/ web/
RUN mkdir -p target/
RUN mkdir -p /lldap/app
RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \
mv bin/x86_64-unknown-linux-musl-lldap-bin/lldap target/lldap && \
mv bin/x86_64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/x86_64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
; fi
RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
mv bin/aarch64-unknown-linux-musl-lldap-bin/lldap target/lldap && \
mv bin/aarch64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/aarch64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
; fi
RUN if [ "${TARGETPLATFORM}" = "linux/arm/v7" ]; then \
mv bin/armv7-unknown-linux-musleabihf-lldap-bin/lldap target/lldap && \
mv bin/armv7-unknown-linux-musleabihf-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/armv7-unknown-linux-musleabihf-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
; fi
# Web and App dir
COPY lldap_config.docker_template.toml /lldap/
COPY web/index_local.html web/index.html
RUN cp target/lldap /lldap/ && \
cp target/lldap_migration_tool /lldap/ && \
cp target/lldap_set_password /lldap/ && \
cp -R web/index.html \
web/pkg \
web/static \
/lldap/app/
WORKDIR /lldap
RUN set -x \
&& for file in $(cat /lldap/app/static/libraries.txt); do wget -P app/static "$file"; done \
&& for file in $(cat /lldap/app/static/fonts/fonts.txt); do wget -P app/static/fonts "$file"; done \
&& chmod a+r -R .
FROM alpine:3.16
WORKDIR /app
ENV UID=1000
ENV GID=1000
ENV USER=lldap
RUN apk add --no-cache tini ca-certificates bash tzdata && \
addgroup -g $GID $USER && \
adduser \
--disabled-password \
--gecos "" \
--home "$(pwd)" \
--ingroup "$USER" \
--no-create-home \
--uid "$UID" \
"$USER" && \
mkdir -p /data && \
chown $USER:$USER /data
COPY --from=lldap --chown=$USER:$USER /lldap /app
VOLUME ["/data"]
HEALTHCHECK CMD ["/app/lldap", "healthcheck", "--config-file", "/data/lldap_config.toml"]
WORKDIR /app
ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"]
CMD ["run", "--config-file", "/data/lldap_config.toml"]

View File

@@ -0,0 +1,3 @@
FROM localhost:5000/lldap/lldap:alpine-base
COPY --chown=$USER:$USER docker-entrypoint-rootless.sh /docker-entrypoint.sh
USER $USER

31
.github/workflows/Dockerfile.ci.debian vendored Normal file
View File

@@ -0,0 +1,31 @@
FROM localhost:5000/lldap/lldap:debian-base
# Taken directly from https://github.com/tianon/gosu/blob/master/INSTALL.md
ENV GOSU_VERSION 1.17
RUN set -eux; \
# save list of currently installed packages for later so we can clean up
savedAptMark="$(apt-mark showmanual)"; \
apt-get update; \
apt-get install -y --no-install-recommends ca-certificates gnupg wget; \
rm -rf /var/lib/apt/lists/*; \
\
dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
\
# verify the signature
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
gpgconf --kill all; \
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
\
# clean up fetch dependencies
apt-mark auto '.*' > /dev/null; \
[ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; \
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
\
chmod +x /usr/local/bin/gosu; \
# verify that the binary works
gosu --version; \
gosu nobody true
COPY --chown=$USER:$USER docker-entrypoint.sh /docker-entrypoint.sh

View File

@@ -0,0 +1,79 @@
FROM debian:bullseye AS lldap
ARG DEBIAN_FRONTEND=noninteractive
ARG TARGETPLATFORM
RUN apt update && apt install -y wget
WORKDIR /dim
COPY bin/ bin/
COPY web/ web/
RUN mkdir -p target/
RUN mkdir -p /lldap/app
RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \
mv bin/x86_64-unknown-linux-musl-lldap-bin/lldap target/lldap && \
mv bin/x86_64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/x86_64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
; fi
RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
mv bin/aarch64-unknown-linux-musl-lldap-bin/lldap target/lldap && \
mv bin/aarch64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/aarch64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
; fi
RUN if [ "${TARGETPLATFORM}" = "linux/arm/v7" ]; then \
mv bin/armv7-unknown-linux-musleabihf-lldap-bin/lldap target/lldap && \
mv bin/armv7-unknown-linux-musleabihf-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/armv7-unknown-linux-musleabihf-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
; fi
# Web and App dir
COPY docker-entrypoint.sh /docker-entrypoint.sh
COPY lldap_config.docker_template.toml /lldap/
COPY web/index_local.html web/index.html
RUN cp target/lldap /lldap/ && \
cp target/lldap_migration_tool /lldap/ && \
cp target/lldap_set_password /lldap/ && \
cp -R web/index.html \
web/pkg \
web/static \
/lldap/app/
WORKDIR /lldap
RUN set -x \
&& for file in $(cat /lldap/app/static/libraries.txt); do wget -P app/static "$file"; done \
&& for file in $(cat /lldap/app/static/fonts/fonts.txt); do wget -P app/static/fonts "$file"; done \
&& chmod a+r -R .
FROM debian:bullseye-slim
ENV UID=1000
ENV GID=1000
ENV USER=lldap
RUN apt update && \
apt install -y --no-install-recommends tini openssl ca-certificates tzdata && \
apt clean && \
rm -rf /var/lib/apt/lists/* && \
groupadd -g $GID $USER && useradd --system -m -g $USER --uid $UID $USER && \
mkdir -p /data && chown $USER:$USER /data
COPY --from=lldap --chown=$USER:$USER /lldap /app
COPY --from=lldap --chown=$USER:$USER /docker-entrypoint.sh /docker-entrypoint.sh
VOLUME ["/data"]
WORKDIR /app
ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"]
CMD ["run", "--config-file", "/data/lldap_config.toml"]
HEALTHCHECK CMD ["/app/lldap", "healthcheck", "--config-file", "/data/lldap_config.toml"]

View File

@@ -0,0 +1,3 @@
FROM localhost:5000/lldap/lldap:debian-base
COPY --chown=$USER:$USER docker-entrypoint-rootless.sh /docker-entrypoint.sh
USER $USER

40
.github/workflows/Dockerfile.dev vendored Normal file
View File

@@ -0,0 +1,40 @@
# Keep tracking base image
FROM rust:1.74-slim-bookworm
# Set needed env path
ENV PATH="/opt/armv7l-linux-musleabihf-cross/:/opt/armv7l-linux-musleabihf-cross/bin/:/opt/aarch64-linux-musl-cross/:/opt/aarch64-linux-musl-cross/bin/:/opt/x86_64-linux-musl-cross/:/opt/x86_64-linux-musl-cross/bin/:$PATH"
# Set building env
ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse \
CARGO_NET_GIT_FETCH_WITH_CLI=true \
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER=armv7l-linux-musleabihf-gcc \
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-musl-gcc \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=x86_64-linux-musl-gcc \
CC_armv7_unknown_linux_musleabihf=armv7l-linux-musleabihf-gcc \
CC_x86_64_unknown_linux_musl=x86_64-linux-musl-gcc \
CC_aarch64_unknown_linux_musl=aarch64-linux-musl-gcc
### Install Additional Build Tools
RUN apt update && \
apt install -y --no-install-recommends curl git wget make perl pkg-config tar jq gzip && \
apt clean && \
rm -rf /var/lib/apt/lists/*
### Add musl-gcc aarch64, x86_64 and armv7l
RUN wget -c https://musl.cc/x86_64-linux-musl-cross.tgz && \
tar zxf ./x86_64-linux-musl-cross.tgz -C /opt && \
wget -c https://musl.cc/aarch64-linux-musl-cross.tgz && \
tar zxf ./aarch64-linux-musl-cross.tgz -C /opt && \
wget -c http://musl.cc/armv7l-linux-musleabihf-cross.tgz && \
tar zxf ./armv7l-linux-musleabihf-cross.tgz -C /opt && \
rm ./x86_64-linux-musl-cross.tgz && \
rm ./aarch64-linux-musl-cross.tgz && \
rm ./armv7l-linux-musleabihf-cross.tgz
### Add musl target
RUN rustup target add x86_64-unknown-linux-musl && \
rustup target add aarch64-unknown-linux-musl && \
rustup target add armv7-unknown-linux-musleabihf
CMD ["bash"]

View File

@@ -0,0 +1,742 @@
name: Docker Static
on:
push:
branches:
- 'main'
paths-ignore:
- 'docs/**'
- 'example_configs/**'
release:
types:
- 'published'
pull_request:
branches:
- 'main'
paths-ignore:
- 'docs/**'
- 'example_configs/**'
workflow_dispatch:
inputs:
msg:
description: "Set message"
default: "Manual trigger"
env:
CARGO_TERM_COLOR: always
### CI Docs
# build-ui , create/compile the web
### install wasm
### run app/build.sh
### upload artifacts
# build-bin
## build-armhf, build-aarch64, build-amd64 , create binary for respective arch
#######################################################################################
# GitHub actions randomly timeout when downloading musl-gcc, using custom dev image #
# Look into .github/workflows/Dockerfile.dev for development image details #
# Using lldap dev image based on https://hub.docker.com/_/rust and musl-gcc bundled #
# lldap/rust-dev:latest #
#######################################################################################
# Cargo build
### armv7, aarch64 and amd64 is musl based
# build-ui,builds-armhf, build-aarch64, build-amd64 will upload artifacts will be used next job
# lldap-test
### will run lldap with postgres, mariadb and sqlite backend, do selfcheck command.
# Build docker image
### Triplet docker image arch with debian and alpine base
# build-docker-image job will fetch artifacts and run Dockerfile.ci then push the image.
### Look into .github/workflows/Dockerfile.ci.debian or .github/workflowds/Dockerfile.ci.alpine
# Create release artifacts
### Fetch artifacts
### Clean up web artifact
### Setup folder structure
### Compress
### Upload
# cache based on Cargo.lock per cargo target
jobs:
pre_job:
continue-on-error: true
runs-on: ubuntu-latest
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@master
with:
concurrent_skipping: 'outdated_runs'
skip_after_successful_duplicate: ${{ github.ref != 'refs/heads/main' }}
paths_ignore: '["**/*.md", "**/docs/**", "example_configs/**", "*.sh", ".gitignore", "lldap_config.docker_template.toml"]'
do_not_skip: '["workflow_dispatch", "schedule"]'
cancel_others: true
build-ui:
runs-on: ubuntu-latest
needs: pre_job
if: ${{ needs.pre_job.outputs.should_skip != 'true' || github.event_name == 'release' }}
container:
image: lldap/rust-dev:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.1
- uses: actions/cache@v4
with:
path: |
/usr/local/cargo/bin
/usr/local/cargo/registry/index
/usr/local/cargo/registry/cache
/usr/local/cargo/git/db
target
key: lldap-ui-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
lldap-ui-
- name: Add wasm target (rust)
run: rustup target add wasm32-unknown-unknown
- name: Install wasm-pack with cargo
run: cargo install wasm-pack || true
env:
RUSTFLAGS: ""
- name: Build frontend
run: ./app/build.sh
- name: Check build path
run: ls -al app/
- name: Upload ui artifacts
uses: actions/upload-artifact@v4
with:
name: ui
path: app/
build-bin:
runs-on: ubuntu-latest
needs: pre_job
if: ${{ needs.pre_job.outputs.should_skip != 'true' || github.event_name == 'release' }}
strategy:
fail-fast: false
matrix:
target: [armv7-unknown-linux-musleabihf, aarch64-unknown-linux-musl, x86_64-unknown-linux-musl]
container:
image: lldap/rust-dev:latest
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -Ctarget-feature=+crt-static
CARGO_HOME: ${GITHUB_WORKSPACE}/.cargo
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.1
- uses: actions/cache@v4
with:
path: |
.cargo/bin
.cargo/registry/index
.cargo/registry/cache
.cargo/git/db
target
key: lldap-bin-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
lldap-bin-${{ matrix.target }}-
- name: Compile ${{ matrix.target }} lldap and tools
run: cargo build --target=${{ matrix.target }} --release -p lldap -p lldap_migration_tool -p lldap_set_password
- name: Check path
run: ls -al target/release
- name: Upload ${{ matrix.target}} lldap artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target}}-lldap-bin
path: target/${{ matrix.target }}/release/lldap
- name: Upload ${{ matrix.target }} migration tool artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}-lldap_migration_tool-bin
path: target/${{ matrix.target }}/release/lldap_migration_tool
- name: Upload ${{ matrix.target }} password tool artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}-lldap_set_password-bin
path: target/${{ matrix.target }}/release/lldap_set_password
lldap-database-init-test:
needs: [build-ui,build-bin]
name: LLDAP database init test
runs-on: ubuntu-latest
services:
mariadb:
image: mariadb:latest
ports:
- 3306:3306
env:
MARIADB_USER: lldapuser
MARIADB_PASSWORD: lldappass
MARIADB_DATABASE: lldap
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 1
options: >-
--name mariadb
--health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3
postgresql:
image: postgres:latest
ports:
- 5432:5432
env:
POSTGRES_USER: lldapuser
POSTGRES_PASSWORD: lldappass
POSTGRES_DB: lldap
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
--name postgresql
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: x86_64-unknown-linux-musl-lldap-bin
path: bin/
- name: Set executables to LLDAP
run: chmod +x bin/lldap
- name: Run lldap with postgres DB and healthcheck
run: |
bin/lldap run &
sleep 10s
bin/lldap healthcheck
env:
LLDAP_database_url: postgres://lldapuser:lldappass@localhost/lldap
LLDAP_ldap_port: 3890
LLDAP_http_port: 17170
- name: Run lldap with mariadb DB (MySQL Compatible) and healthcheck
run: |
bin/lldap run &
sleep 10s
bin/lldap healthcheck
env:
LLDAP_database_url: mysql://lldapuser:lldappass@localhost/lldap
LLDAP_ldap_port: 3891
LLDAP_http_port: 17171
- name: Run lldap with sqlite DB and healthcheck
run: |
bin/lldap run &
sleep 10s
bin/lldap healthcheck
env:
LLDAP_database_url: sqlite://users.db?mode=rwc
LLDAP_ldap_port: 3892
LLDAP_http_port: 17172
- name: Check DB container logs
run: |
docker logs -n 20 mariadb
docker logs -n 20 postgresql
lldap-database-migration-test:
needs: [build-ui,build-bin]
name: LLDAP database migration test
runs-on: ubuntu-latest
services:
postgresql:
image: postgres:latest
ports:
- 5432:5432
env:
POSTGRES_USER: lldapuser
POSTGRES_PASSWORD: lldappass
POSTGRES_DB: lldap
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
--name postgresql
mariadb:
image: mariadb:latest
ports:
- 3306:3306
env:
MARIADB_USER: lldapuser
MARIADB_PASSWORD: lldappass
MARIADB_DATABASE: lldap
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 1
options: >-
--name mariadb
--health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3
mysql:
image: mysql:latest
ports:
- 3307:3306
env:
MYSQL_USER: lldapuser
MYSQL_PASSWORD: lldappass
MYSQL_DATABASE: lldap
MYSQL_ALLOW_EMPTY_PASSWORD: 1
options: >-
--name mysql
--health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Checkout scripts
uses: actions/checkout@v4.1.1
with:
sparse-checkout: 'scripts'
- name: Download LLDAP artifacts
uses: actions/download-artifact@v4
with:
name: x86_64-unknown-linux-musl-lldap-bin
path: bin/
- name: Download LLDAP set password
uses: actions/download-artifact@v4
with:
name: x86_64-unknown-linux-musl-lldap_set_password-bin
path: bin/
- name: Set executables to LLDAP and LLDAP set password
run: |
chmod +x bin/lldap
chmod +x bin/lldap_set_password
- name: Install sqlite3 and ldap-utils for exporting and searching dummy user
run: sudo apt update && sudo apt install -y sqlite3 ldap-utils
- name: Run lldap with sqlite DB and healthcheck
run: |
bin/lldap run &
sleep 10s
bin/lldap healthcheck
env:
LLDAP_database_url: sqlite://users.db?mode=rwc
LLDAP_ldap_port: 3890
LLDAP_http_port: 17170
LLDAP_LDAP_USER_PASS: ldappass
LLDAP_JWT_SECRET: somejwtsecret
- name: Create dummy user
run: |
TOKEN=$(curl -X POST -H "Content-Type: application/json" -d '{"username": "admin", "password": "ldappass"}' http://localhost:17170/auth/simple/login | jq -r .token)
echo "$TOKEN"
curl 'http://localhost:17170/api/graphql' -H 'Content-Type: application/json' -H "Authorization: Bearer ${TOKEN//[$'\t\r\n ']}" --data-binary '{"query":"mutation{\n createUser(user:\n {\n id: \"dummyuser\",\n email: \"dummyuser@example.com\"\n }\n )\n {\n id\n email\n }\n}\n\n\n"}' --compressed
bin/lldap_set_password --base-url http://localhost:17170 --admin-username admin --admin-password ldappass --token $TOKEN --username dummyuser --password dummypassword
- name: Test Dummy User, This will be checked again after importing
run: |
ldapsearch -H ldap://localhost:3890 -LLL -D "uid=dummyuser,ou=people,dc=example,dc=com" -w 'dummypassword' -s "One" -b "ou=people,dc=example,dc=com"
- name: Stop LLDAP sqlite
run: pkill lldap
- name: Export and Converting to Postgress
run: |
bash ./scripts/sqlite_dump_commands.sh | sqlite3 ./users.db > ./dump.sql
sed -i -r -e "s/X'([[:xdigit:]]+'[^'])/'\\\x\\1/g" -e ":a; s/(INSERT INTO (user_attribute_schema|jwt_storage)\(.*\) VALUES\(.*),1([^']*\);)$/\1,true\3/; s/(INSERT INTO (user_attribute_schema|jwt_storage)\(.*\) VALUES\(.*),0([^']*\);)$/\1,false\3/; ta" -e '1s/^/BEGIN;\n/' -e '$aCOMMIT;' ./dump.sql
- name: Create schema on postgres
run: |
bin/lldap create_schema -d postgres://lldapuser:lldappass@localhost:5432/lldap
- name: Copy converted db to postgress and import
run: |
docker cp ./dump.sql postgresql:/tmp/dump.sql
docker exec postgresql bash -c "psql -U lldapuser -d lldap < /tmp/dump.sql" | tee import.log
rm ./dump.sql
! grep ERROR import.log > /dev/null
- name: Export and Converting to mariadb
run: |
bash ./scripts/sqlite_dump_commands.sh | sqlite3 ./users.db > ./dump.sql
cp ./dump.sql ./dump-no-sed.sql
sed -i -r -e "s/([^']'[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{9})\+00:00'([^'])/\1'\2/g" \-e 's/^INSERT INTO "?([a-zA-Z0-9_]+)"?/INSERT INTO `\1`/' -e '1s/^/START TRANSACTION;\n/' -e '$aCOMMIT;' ./dump.sql
sed -i '1 i\SET FOREIGN_KEY_CHECKS = 0;' ./dump.sql
- name: Create schema on mariadb
run: bin/lldap create_schema -d mysql://lldapuser:lldappass@localhost:3306/lldap
- name: Copy converted db to mariadb and import
run: |
docker cp ./dump.sql mariadb:/tmp/dump.sql
docker exec mariadb bash -c "mariadb -ulldapuser -plldappass -f lldap < /tmp/dump.sql" | tee import.log
rm ./dump.sql
! grep ERROR import.log > /dev/null
- name: Export and Converting to mysql
run: |
bash ./scripts/sqlite_dump_commands.sh | sqlite3 ./users.db > ./dump.sql
sed -i -r -e 's/^INSERT INTO "?([a-zA-Z0-9_]+)"?/INSERT INTO `\1`/' -e '1s/^/START TRANSACTION;\n/' -e '$aCOMMIT;' ./dump.sql
sed -i '1 i\SET FOREIGN_KEY_CHECKS = 0;' ./dump.sql
- name: Create schema on mysql
run: bin/lldap create_schema -d mysql://lldapuser:lldappass@localhost:3307/lldap
- name: Copy converted db to mysql and import
run: |
docker cp ./dump.sql mysql:/tmp/dump.sql
docker exec mysql bash -c "mysql -ulldapuser -plldappass -f lldap < /tmp/dump.sql" | tee import.log
rm ./dump.sql
! grep ERROR import.log > /dev/null
- name: Run lldap with postgres DB and healthcheck again
run: |
bin/lldap run &
sleep 10s
bin/lldap healthcheck
env:
LLDAP_database_url: postgres://lldapuser:lldappass@localhost:5432/lldap
LLDAP_ldap_port: 3891
LLDAP_http_port: 17171
LLDAP_LDAP_USER_PASS: ldappass
LLDAP_JWT_SECRET: somejwtsecret
- name: Run lldap with mariaDB and healthcheck again
run: |
bin/lldap run &
sleep 10s
bin/lldap healthcheck
env:
LLDAP_database_url: mysql://lldapuser:lldappass@localhost:3306/lldap
LLDAP_ldap_port: 3892
LLDAP_http_port: 17172
LLDAP_JWT_SECRET: somejwtsecret
- name: Run lldap with mysql and healthcheck again
run: |
bin/lldap run &
sleep 10s
bin/lldap healthcheck
env:
LLDAP_database_url: mysql://lldapuser:lldappass@localhost:3307/lldap
LLDAP_ldap_port: 3893
LLDAP_http_port: 17173
LLDAP_JWT_SECRET: somejwtsecret
- name: Test Dummy User Postgres
run: ldapsearch -H ldap://localhost:3891 -LLL -D "uid=dummyuser,ou=people,dc=example,dc=com" -w 'dummypassword' -s "One" -b "ou=people,dc=example,dc=com"
- name: Test Dummy User MariaDB
run: ldapsearch -H ldap://localhost:3892 -LLL -D "uid=dummyuser,ou=people,dc=example,dc=com" -w 'dummypassword' -s "One" -b "ou=people,dc=example,dc=com"
- name: Test Dummy User MySQL
run: ldapsearch -H ldap://localhost:3893 -LLL -D "uid=dummyuser,ou=people,dc=example,dc=com" -w 'dummypassword' -s "One" -b "ou=people,dc=example,dc=com"
########################################
#### BUILD BASE IMAGE ##################
########################################
build-docker-image:
needs: [build-ui, build-bin]
name: Build Docker image
runs-on: ubuntu-latest
strategy:
matrix:
container: ["debian","alpine"]
include:
- container: alpine
platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: |
type=ref,event=pr
type=semver,pattern=v{{version}}
type=semver,pattern=v{{major}}
type=semver,pattern=v{{major}}.{{minor}}
type=semver,pattern=v{{version}},suffix=
type=semver,pattern=v{{major}},suffix=
type=semver,pattern=v{{major}}.{{minor}},suffix=
type=raw,value=latest,enable={{ is_default_branch }}
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }},suffix=
type=raw,value=latest,enable={{ is_default_branch }},suffix=
type=raw,value={{ date 'YYYY-MM-DD' }},enable={{ is_default_branch }}
type=raw,value={{ date 'YYYY-MM-DD' }},enable={{ is_default_branch }},suffix=
- container: debian
platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: |
type=ref,event=pr
type=semver,pattern=v{{version}}
type=semver,pattern=v{{major}}
type=semver,pattern=v{{major}}.{{minor}}
type=raw,value=latest,enable={{ is_default_branch }}
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=raw,value={{ date 'YYYY-MM-DD' }},enable={{ is_default_branch }}
services:
registry:
image: registry:2
ports:
- 5000:5000
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.1
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: bin
- name: Download llap ui artifacts
uses: actions/download-artifact@v4
with:
name: ui
path: web
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
- name: Setup buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: network=host
- name: Docker ${{ matrix.container }} Base meta
id: meta-base
uses: docker/metadata-action@v5
with:
# list of Docker images to use as base name for tags
images: |
localhost:5000/lldap/lldap
tags: ${{ matrix.container }}-base
- name: Build ${{ matrix.container }} Base Docker Image
uses: docker/build-push-action@v5
with:
context: .
# On PR will fail, force fully uncomment push: true, or docker image will fail for next steps
#push: ${{ github.event_name != 'pull_request' }}
push: true
platforms: ${{ matrix.platforms }}
file: ./.github/workflows/Dockerfile.ci.${{ matrix.container }}-base
tags: |
${{ steps.meta-base.outputs.tags }}
labels: ${{ steps.meta-base.outputs.labels }}
cache-from: type=gha,mode=max
cache-to: type=gha,mode=max
#####################################
#### build variants docker image ####
#####################################
- name: Docker ${{ matrix.container }}-rootless meta
id: meta-rootless
uses: docker/metadata-action@v5
with:
# list of Docker images to use as base name for tags
images: |
nitnelave/lldap
lldap/lldap
ghcr.io/lldap/lldap
# Wanted Docker tags
# vX-alpine
# vX.Y-alpine
# vX.Y.Z-alpine
# latest
# latest-alpine
# stable
# stable-alpine
# YYYY-MM-DD
# YYYY-MM-DD-alpine
#################
# vX-debian
# vX.Y-debian
# vX.Y.Z-debian
# latest-debian
# stable-debian
# YYYY-MM-DD-debian
#################
# Check matrix for tag list definition
flavor: |
latest=false
suffix=-${{ matrix.container }}-rootless
tags: ${{ matrix.tags }}
- name: Docker ${{ matrix.container }} meta
id: meta-standard
uses: docker/metadata-action@v5
with:
# list of Docker images to use as base name for tags
images: |
nitnelave/lldap
lldap/lldap
ghcr.io/lldap/lldap
# Wanted Docker tags
# vX-alpine
# vX.Y-alpine
# vX.Y.Z-alpine
# latest
# latest-alpine
# stable
# stable-alpine
# YYYY-MM-DD
# YYYY-MM-DD-alpine
#################
# vX-debian
# vX.Y-debian
# vX.Y.Z-debian
# latest-debian
# stable-debian
# YYYY-MM-DD-debian
#################
# Check matrix for tag list definition
flavor: |
latest=false
suffix=-${{ matrix.container }}
tags: ${{ matrix.tags }}
# Docker login to nitnelave/lldap and lldap/lldap
- name: Login to Nitnelave/LLDAP Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: nitnelave
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build ${{ matrix.container }}-rootless Docker Image
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: ${{ matrix.platforms }}
file: ./.github/workflows/Dockerfile.ci.${{ matrix.container }}-rootless
tags: |
${{ steps.meta-rootless.outputs.tags }}
labels: ${{ steps.meta-rootless.outputs.labels }}
cache-from: type=gha,mode=max
cache-to: type=gha,mode=max
### This docker build always the last, due :latest tag pushed multiple times, for whatever variants may added in future add docker build above this
- name: Build ${{ matrix.container }} Docker Image
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: ${{ matrix.platforms }}
file: ./.github/workflows/Dockerfile.ci.${{ matrix.container }}
tags: |
${{ steps.meta-standard.outputs.tags }}
labels: ${{ steps.meta-standard.outputs.labels }}
cache-from: type=gha,mode=max
cache-to: type=gha,mode=max
- name: Update repo description
if: github.event_name != 'pull_request'
uses: peter-evans/dockerhub-description@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repository: nitnelave/lldap
- name: Update lldap repo description
if: github.event_name != 'pull_request'
uses: peter-evans/dockerhub-description@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repository: lldap/lldap
###############################################################
### Download artifacts, clean up ui, upload to release page ###
###############################################################
create-release-artifacts:
needs: [build-ui, build-bin]
name: Create release artifacts
if: github.event_name == 'release'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: bin/
- name: Check file
run: ls -alR bin/
- name: Fixing Filename
run: |
mv bin/aarch64-unknown-linux-musl-lldap-bin/lldap bin/aarch64-lldap
mv bin/x86_64-unknown-linux-musl-lldap-bin/lldap bin/amd64-lldap
mv bin/armv7-unknown-linux-musleabihf-lldap-bin/lldap bin/armhf-lldap
mv bin/aarch64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool bin/aarch64-lldap_migration_tool
mv bin/x86_64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool bin/amd64-lldap_migration_tool
mv bin/armv7-unknown-linux-musleabihf-lldap_migration_tool-bin/lldap_migration_tool bin/armhf-lldap_migration_tool
mv bin/aarch64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password bin/aarch64-lldap_set_password
mv bin/x86_64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password bin/amd64-lldap_set_password
mv bin/armv7-unknown-linux-musleabihf-lldap_set_password-bin/lldap_set_password bin/armhf-lldap_set_password
chmod +x bin/*-lldap
chmod +x bin/*-lldap_migration_tool
chmod +x bin/*-lldap_set_password
- name: Download llap ui artifacts
uses: actions/download-artifact@v4
with:
name: ui
path: web
- name: UI (web) artifacts cleanup
run: mkdir app && mv web/index.html app/index.html && mv web/static app/static && mv web/pkg app/pkg
- name: Fetch web components
run: |
sudo apt update
sudo apt install wget
for file in $(cat app/static/libraries.txt); do wget -P app/static "$file"; done
for file in $(cat app/static/fonts/fonts.txt); do wget -P app/static/fonts "$file"; done
chmod a+r -R .
- name: Setup LLDAP dir for packing
run: |
mkdir aarch64-lldap
mkdir amd64-lldap
mkdir armhf-lldap
mv bin/aarch64-lldap aarch64-lldap/lldap
mv bin/amd64-lldap amd64-lldap/lldap
mv bin/armhf-lldap armhf-lldap/lldap
mv bin/aarch64-lldap_migration_tool aarch64-lldap/lldap_migration_tool
mv bin/amd64-lldap_migration_tool amd64-lldap/lldap_migration_tool
mv bin/armhf-lldap_migration_tool armhf-lldap/lldap_migration_tool
mv bin/aarch64-lldap_set_password aarch64-lldap/lldap_set_password
mv bin/amd64-lldap_set_password amd64-lldap/lldap_set_password
mv bin/armhf-lldap_set_password armhf-lldap/lldap_set_password
cp -r app aarch64-lldap/
cp -r app amd64-lldap/
cp -r app armhf-lldap/
ls -alR aarch64-lldap/
ls -alR amd64-lldap/
ls -alR armhf-lldap/
- name: Packing LLDAP and Web UI
run: |
tar -czvf aarch64-lldap.tar.gz aarch64-lldap/
tar -czvf amd64-lldap.tar.gz amd64-lldap/
tar -czvf armhf-lldap.tar.gz armhf-lldap/
- name: Upload compressed release
uses: ncipollo/release-action@v1
id: create_release
with:
allowUpdates: true
artifacts: aarch64-lldap.tar.gz,
amd64-lldap.tar.gz,
armhf-lldap.tar.gz
env:
GITHUB_TOKEN: ${{ github.token }}

View File

@@ -1,410 +0,0 @@
name: Docker
on:
push:
branches:
- 'main'
release:
types:
- 'published'
pull_request:
branches:
- 'main'
workflow_dispatch:
inputs:
msg:
description: "Set message"
default: "Manual trigger"
env:
CARGO_TERM_COLOR: always
RUSTC_WRAPPER: sccache
SCCACHE_DIR: $GITHUB_WORKSPACE/.sccache
SCCACHE_VERSION: v0.3.0
LINK: https://github.com/mozilla/sccache/releases/download
# In total 5 jobs, all of the jobs are containerized
# ---
# build-ui , create/compile the web
## Use rustlang/rust:nighlty image
### Install nodejs from nodesource repo
### install wasm
### install rollup
### run app/build.sh
### upload artifacts
# builds-armhf, build-aarch64, build-amd64 create binary for respective arch
## Use rustlang/rust:nightly image
### Add non native architecture dpkg --add-architecture XXX
### Install dev tool gcc g++, etc per respective arch
### Cargo build
### Upload artifacts
## the CARGO_ env
#CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER: arm-linux-gnueabihf-gcc
#OPENSSL_INCLUDE_DIR: "/usr/include/openssl/"
#OPENSSL_LIB_DIR: "/usr/lib/arm-linux-gnueabihf/"
# This will determine which architecture lib will be used.
# build-ui,builds-armhf, build-aarch64, build-amd64 will upload artifacts will be used next job
# build-docker-image job will fetch artifacts and run Dockerfile.ci then push the image.
# On current https://hub.docker.com/_/rust
# 1-bullseye, 1.61-bullseye, 1.61.0-bullseye, bullseye, 1, 1.61, 1.61.0, latest
# cache
## .sccache
## cargo
## target
jobs:
build-ui:
runs-on: ubuntu-latest
container:
image: rust:1.61
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -Ctarget-feature=-crt-static
steps:
- name: install runtime
run: apt update && apt install -y gcc-x86-64-linux-gnu g++-x86-64-linux-gnu libc6-dev libssl-dev
- name: setup node repo LTS
run: curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
- name: install nodejs
run: apt install -y nodejs && npm -g install npm
- name: smoke test
run: rustc --version
- name: Install sccache (ubuntu-latest)
run: |
SCCACHE_FILE=sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl
mkdir -p $HOME/.local/bin
curl -L "$LINK/$SCCACHE_VERSION/$SCCACHE_FILE.tar.gz" | tar xz
mv -f $SCCACHE_FILE/sccache $HOME/.local/bin/sccache
chmod +x $HOME/.local/bin/sccache
echo "$HOME/.local/bin" >> $GITHUB_PATH
- uses: actions/cache@v3
with:
path: |
.sccache
/usr/local/cargo
target
key: lldap-ui-${{ github.sha }}
restore-keys: |
lldap-ui-
- name: Checkout repository
uses: actions/checkout@v2
- name: install cargo wasm
run: cargo install wasm-pack
- name: install rollup nodejs
run: npm install -g rollup
- name: build frontend
run: ./app/build.sh
- name: check path
run: ls -al app/
- name: upload ui artifacts
uses: actions/upload-artifact@v3
with:
name: ui
path: app/
build-armhf:
runs-on: ubuntu-latest
container:
image: rust:1.61
env:
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER: arm-linux-gnueabihf-gcc
OPENSSL_INCLUDE_DIR: "/usr/include/openssl/"
OPENSSL_LIB_DIR: "/usr/lib/arm-linux-gnueabihf/"
CARGO_TERM_COLOR: always
RUSTFLAGS: -Ctarget-feature=-crt-static
steps:
- name: add armhf architecture
run: dpkg --add-architecture armhf
- name: install runtime
run: apt update && apt install -y gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf libc6-armhf-cross libc6-dev-armhf-cross libssl-dev:armhf tar
- name: smoke test
run: rustc --version
- name: add armhf target
run: rustup target add armv7-unknown-linux-gnueabihf
- name: smoke test
run: rustc --version
- name: Install sccache (ubuntu-latest)
run: |
SCCACHE_FILE=sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl
mkdir -p $HOME/.local/bin
curl -L "$LINK/$SCCACHE_VERSION/$SCCACHE_FILE.tar.gz" | tar xz
mv -f $SCCACHE_FILE/sccache $HOME/.local/bin/sccache
chmod +x $HOME/.local/bin/sccache
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Checkout repository
uses: actions/checkout@v2
- uses: actions/cache@v3
with:
path: |
.sccache
/usr/local/cargo
target
key: lldap-bin-armhf-${{ github.sha }}
restore-keys: |
lldap-bin-armhf-
- name: compile armhf
run: cargo build --target=armv7-unknown-linux-gnueabihf --release -p lldap -p migration-tool
- name: check path
run: ls -al target/release
- name: upload armhf lldap artifacts
uses: actions/upload-artifact@v3
with:
name: armhf-lldap-bin
path: target/armv7-unknown-linux-gnueabihf/release/lldap
- name: upload armhfmigration-tool artifacts
uses: actions/upload-artifact@v3
with:
name: armhf-migration-tool-bin
path: target/armv7-unknown-linux-gnueabihf/release/migration-tool
build-aarch64:
runs-on: ubuntu-latest
container:
image: rust:1.61
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
OPENSSL_INCLUDE_DIR: "/usr/include/openssl/"
OPENSSL_LIB_DIR: "/usr/lib/aarch64-linux-gnu/"
CARGO_TERM_COLOR: always
RUSTFLAGS: -Ctarget-feature=-crt-static
steps:
- name: add arm64 architecture
run: dpkg --add-architecture arm64
- name: install runtime
run: apt update && apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libc6-arm64-cross libc6-dev-arm64-cross libssl-dev:arm64 tar
- name: smoke test
run: rustc --version
- name: Checkout repository
uses: actions/checkout@v2
- name: add arm64 target
run: rustup target add aarch64-unknown-linux-gnu
- name: smoke test
run: rustc --version
- name: Install sccache (ubuntu-latest)
run: |
SCCACHE_FILE=sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl
mkdir -p $HOME/.local/bin
curl -L "$LINK/$SCCACHE_VERSION/$SCCACHE_FILE.tar.gz" | tar xz
mv -f $SCCACHE_FILE/sccache $HOME/.local/bin/sccache
chmod +x $HOME/.local/bin/sccache
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Checkout repository
uses: actions/checkout@v2
- uses: actions/cache@v3
with:
path: |
.sccache
/usr/local/cargo
target
key: lldap-bin-aarch64-${{ github.sha }}
restore-keys: |
lldap-bin-aarch64-
- name: compile aarch64
run: cargo build --target=aarch64-unknown-linux-gnu --release -p lldap -p migration-tool
- name: check path
run: ls -al target/aarch64-unknown-linux-gnu/release/
- name: upload aarch64 lldap artifacts
uses: actions/upload-artifact@v3
with:
name: aarch64-lldap-bin
path: target/aarch64-unknown-linux-gnu/release/lldap
- name: upload aarch64 migration-tool artifacts
uses: actions/upload-artifact@v3
with:
name: aarch64-migration-tool-bin
path: target/aarch64-unknown-linux-gnu/release/migration-tool
build-amd64:
runs-on: ubuntu-latest
container:
image: rust:1.61
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -Ctarget-feature=-crt-static
steps:
- name: install runtime
run: apt update && apt install -y gcc-x86-64-linux-gnu g++-x86-64-linux-gnu libc6-dev libssl-dev tar
- name: smoke test
run: rustc --version
- name: Install sccache (ubuntu-latest)
run: |
SCCACHE_FILE=sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl
mkdir -p $HOME/.local/bin
curl -L "$LINK/$SCCACHE_VERSION/$SCCACHE_FILE.tar.gz" | tar xz
mv -f $SCCACHE_FILE/sccache $HOME/.local/bin/sccache
chmod +x $HOME/.local/bin/sccache
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Checkout repository
uses: actions/checkout@v2
- name: cargo & sscache cache
uses: actions/cache@v3
with:
path: |
.sccache
/usr/local/cargo
target
key: lldap-bin-amd64-${{ github.sha }}
restore-keys: |
lldap-bin-amd64-
#- name: add cargo chef
# run: cargo install cargo-chef
#- name: chef prepare
# run: cargo chef prepare --recipe-path recipe.json
#- name: cook?
# run: cargo chef cook --release --recipe-path recipe.json
- name: compile amd64
run: cargo build --target=x86_64-unknown-linux-gnu --release -p lldap -p migration-tool
- name: check path
run: ls -al target/x86_64-unknown-linux-gnu/release/
- name: upload amd64 lldap artifacts
uses: actions/upload-artifact@v3
with:
name: amd64-lldap-bin
path: target/x86_64-unknown-linux-gnu/release/lldap
- name: upload amd64 migration-tool artifacts
uses: actions/upload-artifact@v3
with:
name: amd64-migration-tool-bin
path: target/x86_64-unknown-linux-gnu/release/migration-tool
build-docker-image:
needs: [build-ui,build-armhf,build-aarch64,build-amd64]
name: Build Docker image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: fetch repo
uses: actions/checkout@v2
- name: Download armhf lldap artifacts
uses: actions/download-artifact@v3
with:
name: armhf-lldap-bin
path: bin/armhf-bin
- name: Download armhf migration-tool artifacts
uses: actions/download-artifact@v3
with:
name: armhf-migration-tool-bin
path: bin/armhf-bin
- name: Download aarch64 lldap artifacts
uses: actions/download-artifact@v3
with:
name: aarch64-lldap-bin
path: bin/aarch64-bin
- name: Download aarch64 migration-tool artifacts
uses: actions/download-artifact@v3
with:
name: aarch64-migration-tool-bin
path: bin/aarch64-bin
- name: Download amd64 lldap artifacts
uses: actions/download-artifact@v3
with:
name: amd64-lldap-bin
path: bin/amd64-bin
- name: Download amd64 migration-tool artifacts
uses: actions/download-artifact@v3
with:
name: amd64-migration-tool-bin
path: bin/amd64-bin
- name: check bin path
run: ls -al bin/
- name: Download llap ui artifacts
uses: actions/download-artifact@v3
with:
name: ui
path: web
- name: setup qemu
uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
# list of Docker images to use as base name for tags
images: |
nitnelave/lldap
# generate Docker tags based on the following events/attributes
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: parse tag
uses: gacts/github-slug@v1
id: slug
- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push latest
if: github.event_name != 'release'
uses: docker/build-push-action@v3
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
file: ./.github/workflows/Dockerfile.ci
tags: nitnelave/lldap:latest
#cache-from: type=gha
#cache-to: type=gha,mode=max
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
- name: Build and push release
if: github.event_name == 'release'
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
# Tag as latest, stable, semver, major, major.minor and major.minor.patch.
file: ./.github/workflows/Dockerfile.ci
tags: nitnelave/lldap:stable, nitnelave/lldap:v${{ steps.slug.outputs.version-semantic }}, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}.${{ steps.slug.outputs.version-patch }}
#cache-from: type=gha
#cache-to: type=gha,mode=max
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
- name: Update repo description
if: github.event_name != 'pull_request'
uses: peter-evans/dockerhub-description@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repository: nitnelave/lldap

20
.github/workflows/release-bot.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Release Bot
on:
release:
types: [published]
jobs:
comment:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: nflaig/release-comment-on-pr@master
with:
token: ${{ secrets.RELEASE_BOT_TOKEN }}
message: |
Thank you everyone for the contribution!
This feature is now available in the latest release, [${releaseTag}](${releaseUrl}).
You can support LLDAP by starring our repo, contributing some configuration examples and becoming a sponsor.

View File

@@ -13,7 +13,6 @@ jobs:
pre_job:
continue-on-error: true
runs-on: ubuntu-latest
# Map a step output to a job output
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
@@ -22,7 +21,7 @@ jobs:
with:
concurrent_skipping: 'outdated_runs'
skip_after_successful_duplicate: 'true'
paths_ignore: '["**/*.md", "**/docs/**", "example_configs/**", "*.sh"]'
paths_ignore: '["**/*.md", "**/docs/**", "example_configs/**", "*.sh", ".dockerignore", ".gitignore", "lldap_config.docker_template.toml", "Dockerfile"]'
do_not_skip: '["workflow_dispatch", "schedule"]'
cancel_others: true
@@ -34,8 +33,8 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v1
uses: actions/checkout@v4.1.1
- uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --verbose --workspace
- name: Run tests
@@ -53,9 +52,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
uses: actions/checkout@v4.1.1
- uses: Swatinem/rust-cache@v1
- uses: Swatinem/rust-cache@v2
- name: Run cargo clippy
uses: actions-rs/cargo@v1
@@ -70,9 +69,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
uses: actions/checkout@v4.1.1
- uses: Swatinem/rust-cache@v1
- uses: Swatinem/rust-cache@v2
- name: Run cargo fmt
uses: actions-rs/cargo@v1
@@ -82,19 +81,21 @@ jobs:
coverage:
name: Code coverage
needs: pre_job
needs:
- pre_job
- test
if: ${{ needs.pre_job.outputs.should_skip != 'true' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
uses: actions/checkout@v4.1.1
- name: Install Rust
run: rustup toolchain install nightly --component llvm-tools-preview && rustup component add llvm-tools-preview --toolchain stable-x86_64-unknown-linux-gnu
- uses: taiki-e/install-action@cargo-llvm-cov
- uses: Swatinem/rust-cache@v1
- uses: Swatinem/rust-cache@v2
- name: Generate code coverage for unit test
run: cargo llvm-cov --workspace --no-report
@@ -102,6 +103,14 @@ jobs:
run: cargo llvm-cov --no-run --lcov --output-path lcov.info
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
if: github.ref != 'refs/heads/main' || github.event_name != 'push'
with:
files: lcov.info
fail_ci_if_error: true
- name: Upload coverage to Codecov (main)
uses: codecov/codecov-action@v3
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
with:
files: lcov.info
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

1
.gitignore vendored
View File

@@ -23,6 +23,7 @@ server_key
*.tar.gz
# Misc
.vscode
.env
recipe.json
lldap_config.toml

View File

@@ -5,7 +5,173 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.5.0] 2023-09-14
### Breaking
- Emails and UUIDs are now enforced to be unique.
- If you have several users with the same email, you'll have to disambiguate
them. You can do that by either issuing SQL commands directly
(`UPDATE users SET email = 'x@x' WHERE user_id = 'bob';`), or by reverting
to a 0.4.x version of LLDAP and editing the user through the web UI.
An error will prevent LLDAP 0.5+ from starting otherwise.
- This was done to prevent account takeover for systems that allow to
login via email.
### Added
- The server private key can be set as a seed from an env variable (#504).
- This is especially useful when you have multiple containers, they don't
need to share a writeable folder.
- Added support for changing the password through a plain LDAP Modify
operation (as opposed to an extended operation), to allow Jellyfin
to change password (#620).
- Allow creating a user with multiple objectClass (#612).
- Emails now have a message ID (#608).
- Added a warning for browsers that have WASM/JS disabled (#639).
- Added support for querying OUs in LDAP (#669).
- Added a button to clear the avatar in the UI (#358).
### Changed
- Groups are now sorted by name in the web UI (#623).
- ARM build now uses musl (#584).
- Improved logging.
- Default admin user is only created if there are no admins (#563).
- That allows you to remove the default admin, making it harder to
bruteforce.
### Fixed
- Fixed URL parsing with a trailing slash in the password setting utility
(#597).
In addition to all that, there was significant progress towards #67,
user-defined attributes. That complex feature will unblock integration with many
systems, including PAM authentication.
### New services
- Ejabberd
- Ergo
- LibreNMS
- Mealie
- MinIO
- OpnSense
- PfSense
- PowerDnsAdmin
- Proxmox
- Squid
- Tandoor recipes
- TheLounge
- Zabbix-web
- Zulip
## [0.4.3] 2023-04-11
The repository has changed from `nitnelave/lldap` to `lldap/lldap`, both on GitHub
and on DockerHub (although we will keep publishing the images to
`nitnelave/lldap` for the foreseeable future). All data on GitHub has been
migrated, and the new docker images are available both on DockerHub and on the
GHCR under `lldap/lldap`.
### Added
- EC private keys are not supported for LDAPS.
### Changed
- SMTP user no longer has a default value (and instead defaults to unauthenticated).
### Fixed
- WASM payload is now delivered uncompressed to Safari due to a Safari bug.
- Password reset no longer redirects to login page.
- NextCloud config should add the "mail" attribute.
- GraphQL parameters are now urldecoded, to support special characters in usernames.
- Healthcheck correctly checks the server certificate.
### New services
- Home Assistant
- Shaarli
## [0.4.2] - 2023-03-27
### Added
- Add support for MySQL/MariaDB/PostgreSQL, in addition to SQLite.
- Healthcheck command for docker setups.
- User creation through LDAP.
- IPv6 support.
- Dev container for VsCode.
- Add support for DN LDAP filters.
- Add support for SubString LDAP filters.
- Add support for LdapCompare operation.
- Add support for unencrypted/unauthenticated SMTP connection.
- Add a command to setup the database schema.
- Add a tool to set a user's password from the command line.
- Added consistent release artifacts.
### Changed
- Payload is now compressed, reducing the size to 700kb.
- entryUUID is returned in the default LDAP fields.
- Slightly improved support for LDAP browsing tools.
- Password reset can be identified by email (instead of just username).
- Various front-end improvements, and support for dark mode.
- Add content-type header to the password reset email, fixing rendering issues in some clients.
- Identify groups with "cn" instead of "uid" in memberOf field.
### Removed
- Removed dependency on nodejs/rollup.
### Fixed
- Email is now using the async API.
- Fix handling of empty/null names (display, first, last).
- Obscured old password field when changing password.
- Respect user setting to disable password resets.
- Fix handling of "present" filters with unknown attributes.
- Fix handling of filters that could lead to an ambiguous SQL query.
### New services
- Authentik
- Dell iDRAC
- Dex
- Kanboard
- NextCloud + OIDC or Authelia
- Nexus
- SUSE Rancher
- VaultWarden
- WeKan
- WikiJS
- ZendTo
### Dependencies (highlights)
- Upgraded Yew to 0.19
- Upgraded actix to 0.13
- Upgraded clap to 4
- Switched from sea-query to sea-orm 0.11
## [0.4.1] - 2022-10-10
### Added
- Added support for STARTTLS for SMTP.
- Added support for user profile pictures, including importing them from OpenLDAP.
- Added support for every config value to be specified in a file.
- Added support for PKCS1 keys.
### Changed
- The `dn` attribute is no longer returned as an attribute (it's still part of the response).
- Empty attributes are no longer returned.
- The docker image now uses the locally-downloaded assets.
## [0.4.0] - 2022-07-08

97
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,97 @@
# How to contribute to LLDAP
## Did you find a bug?
- Make sure there isn't already an [issue](https://github.com/lldap/lldap/issues?q=is%3Aissue+is%3Aopen) for it.
- Check if the bug still happens with the `latest` docker image, or the `main` branch if you compile it yourself.
- [Create an issue](https://github.com/lldap/lldap/issues/new) on GitHub. What makes a great issue:
- A quick summary of the bug.
- Steps to reproduce.
- LLDAP _verbose_ logs when reproducing the bug. Verbose mode can be set through environment variables (`LLDAP_VERBOSE=true`) or in the config (`verbose = true`).
- What you expected to happen.
- What actually happened.
- Other notes (what you tried, why you think it's happening, ...).
## Are you requesting integration with a new service?
- Check if there is already an [example config](https://github.com/lldap/lldap/tree/main/example_configs) for it.
- Try to figure out the configuration values for the new service yourself.
- You can use other example configs for inspiration.
- If you're having trouble, you can ask on [Discord](https://discord.gg/h5PEdRMNyP)
- If you succeed, make sure to contribute an example configuration, or a configuration guide.
- If you hit a block because of an unimplemented feature, go to the next section.
## Are you asking for a new feature?
- Make sure there isn't already an [issue](https://github.com/lldap/lldap/issues?q=is%3Aissue+is%3Aopen) for it.
- [Create an issue](https://github.com/lldap/lldap/issues/new) on GitHub. What makes a great feature request:
- A quick summary of the feature.
- Motivation: what problem does the feature solve?
- Workarounds: what are the currently possible solutions to the problem, however bad?
## Do you want to work on a PR?
That's great! There are 2 main ways to contribute to the project: documentation and code.
### Documentation
The simplest way to contribute is to submit a configuration guide for a new
service: it can be an example configuration file, or a markdown guide
explaining the steps necessary to configure the service.
We also have some
[documentation](https://github.com/lldap/lldap/tree/main/docs) with more
advanced guides (scripting, migrations, ...) you can contribute to.
### Code
If you don't know what to start with, check out the
[good first issues](https://github.com/lldap/lldap/labels/good%20first%20issue).
Otherwise, if you want to fix a specific bug or implement a feature, make sure
to start by creating an issue for it (if it doesn't already exist). There, we
can discuss whether it would be likely to be accepted and consider design
issues. That will save you from going down a wrong path, creating an entire PR
before getting told that it doesn't align with the project or the design is
flawed!
Once we agree on what to do in the issue, you can start working on the PR. A good quality PR has:
- A description of the change.
- The format we use for both commit titles and PRs is:
`tag: Do the thing`
The tag can be: server, app, docker, example_configs, ... It's a broad category.
The rest of the title should be an imperative sentence (see for instance [Commit Message
Guidelines](https://gist.github.com/robertpainsi/b632364184e70900af4ab688decf6f53)).
- The PR should refer to the issue it's addressing (e.g. "Fix #123").
- Explain the _why_ of the change.
- But also the _how_.
- Highlight any potential flaw or limitation.
- The code change should be as small as possible while solving the problem.
- Don't try to code-golf to change fewer characters, but keep logically separate changes in
different PRs.
- Add tests if possible.
- The tests should highlight the original issue in case of a bug.
- Ideally, we can apply the tests without the rest of the change and they would fail. With the
change, they pass.
- In some areas, there is no test infrastructure in place (e.g. for frontend changes). In that
case, do some manual testing and include the results (logs for backend changes, screenshot of a
successful service integration, screenshot of the frontend change).
- For backend changes, the tests should cover a significant portion of the new code paths, or
everything if possible. You can also add more tests to cover existing code.
- Of course, make sure all the existing tests pass. This will be checked anyway in the GitHub CI.
### Workflow
We use [GitHub Flow](https://docs.github.com/en/get-started/quickstart/github-flow):
- Fork the repository.
- (Optional) Create a new branch, or just use `main` in your fork.
- Make your change.
- Create a PR.
- Address the comments by adding more commits to your branch (or to `main`).
- The PR gets merged (the commits get squashed to a single one).
- (Optional) You can delete your branch/fork.
## Reminder
We're all volunteers, so be kind to each other! And since we're doing that in our free time, some
things can take a longer than expected.

3467
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,16 +3,23 @@ members = [
"server",
"auth",
"app",
"migration-tool"
"migration-tool",
"set-password",
]
default-members = ["server"]
# TODO: remove when there's a new release.
[patch.crates-io.yew_form]
git = 'https://github.com/sassman/yew_form/'
rev = '67050812695b7a8a90b81b0637e347fc6629daed'
resolver = "2"
[patch.crates-io.yew_form_derive]
git = 'https://github.com/sassman/yew_form/'
rev = '67050812695b7a8a90b81b0637e347fc6629daed'
[profile.release]
lto = true
[profile.release.package.lldap_app]
opt-level = 's'
[patch.crates-io.opaque-ke]
git = 'https://github.com/nitnelave/opaque-ke/'
branch = 'zeroize_1.5'
[patch.crates-io.lber]
git = 'https://github.com/inejge/ldap3/'

View File

@@ -1,5 +1,5 @@
# Build image
FROM rust:alpine3.14 AS chef
FROM rust:alpine3.16 AS chef
RUN set -x \
# Add user
@@ -11,7 +11,7 @@ RUN set -x \
--uid 10001 \
app \
# Install required packages
&& apk add npm openssl-dev musl-dev make perl curl
&& apk add openssl-dev musl-dev make perl curl gzip
USER app
WORKDIR /app
@@ -19,7 +19,6 @@ WORKDIR /app
RUN set -x \
# Install build tools
&& RUSTFLAGS=-Ctarget-feature=-crt-static cargo install wasm-pack cargo-chef \
&& npm install rollup \
&& rustup target add wasm32-unknown-unknown
# Prepare the dependency list.
@@ -32,27 +31,58 @@ FROM chef AS builder
COPY --from=planner /tmp/recipe.json recipe.json
RUN cargo chef cook --release -p lldap_app --target wasm32-unknown-unknown \
&& cargo chef cook --release -p lldap \
&& cargo chef cook --release -p migration-tool
&& cargo chef cook --release -p lldap_migration_tool \
&& cargo chef cook --release -p lldap_set_password
# Copy the source and build the app and server.
COPY --chown=app:app . .
RUN cargo build --release -p lldap -p migration-tool \
RUN cargo build --release -p lldap -p lldap_migration_tool -p lldap_set_password \
# Build the frontend.
&& ./app/build.sh
# Final image
FROM alpine:3.14
FROM alpine:3.16
ENV GOSU_VERSION 1.14
# Fetch gosu from git
RUN set -eux; \
\
apk add --no-cache --virtual .gosu-deps \
ca-certificates \
dpkg \
gnupg \
; \
\
dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
\
# verify the signature
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
command -v gpgconf && gpgconf --kill all || :; \
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
\
# clean up fetch dependencies
apk del --no-network .gosu-deps; \
\
chmod +x /usr/local/bin/gosu; \
# verify that the binary works
gosu --version; \
gosu nobody true
WORKDIR /app
COPY --from=builder /app/app/index_local.html app/index.html
COPY --from=builder /app/app/static app/static
COPY --from=builder /app/app/pkg app/pkg
COPY --from=builder /app/target/release/lldap /app/target/release/migration-tool ./
COPY --from=builder /app/target/release/lldap /app/target/release/lldap_migration_tool /app/target/release/lldap_set_password ./
COPY docker-entrypoint.sh lldap_config.docker_template.toml ./
RUN set -x \
&& apk add --no-cache bash \
&& apk add --no-cache bash tzdata \
&& for file in $(cat app/static/libraries.txt); do wget -P app/static "$file"; done \
&& for file in $(cat app/static/fonts/fonts.txt); do wget -P app/static/fonts "$file"; done \
&& chmod a+r -R .
@@ -64,3 +94,4 @@ EXPOSE ${LDAP_PORT} ${HTTP_PORT}
ENTRYPOINT ["/app/docker-entrypoint.sh"]
CMD ["run", "--config-file", "/data/lldap_config.toml"]
HEALTHCHECK CMD ["/app/lldap", "healthcheck", "--config-file", "/data/lldap_config.toml"]

414
README.md
View File

@@ -5,14 +5,15 @@
</p>
<p align="center">
<a href="https://github.com/nitnelave/lldap/actions/workflows/rust.yml?query=branch%3Amain">
<a href="https://github.com/lldap/lldap/actions/workflows/rust.yml?query=branch%3Amain">
<img
src="https://github.com/nitnelave/lldap/actions/workflows/rust.yml/badge.svg"
src="https://github.com/lldap/lldap/actions/workflows/rust.yml/badge.svg"
alt="Build"/>
</a>
<a href="https://discord.gg/h5PEdRMNyP">
<img alt="Discord" src="https://img.shields.io/discord/898492935446876200?label=discord&logo=discord" />
</a>
<a href="https://twitter.com/nitnelave1?ref_src=twsrc%5Etfw">
<img
src="https://img.shields.io/twitter/follow/nitnelave1?style=social"
@@ -23,25 +24,38 @@
src="https://img.shields.io/badge/unsafe-forbidden-success.svg"
alt="Unsafe forbidden"/>
</a>
<a href="https://app.codecov.io/gh/nitnelave/lldap">
<img alt="Codecov" src="https://img.shields.io/codecov/c/github/nitnelave/lldap" />
<a href="https://app.codecov.io/gh/lldap/lldap">
<img alt="Codecov" src="https://img.shields.io/codecov/c/github/lldap/lldap" />
</a>
<br/>
<a href="https://www.buymeacoffee.com/nitnelave" target="_blank">
<img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" >
</a>
</p>
- [About](#About)
- [Installation](#Installation)
- [With Docker](#With-Docker)
- [From source](#From-source)
- [Cross-compilation](#Cross-compilation)
- [Client configuration](#Client-configuration)
- [Compatible services](#compatible-services)
- [General configuration guide](#general-configuration-guide)
- [Sample cient configurations](#Sample-client-configurations)
- [Comparisons with other services](#Comparisons-with-other-services)
- [vs OpenLDAP](#vs-openldap)
- [vs FreeIPA](#vs-freeipa)
- [I can't log in!](#i-cant-log-in)
- [Contributions](#Contributions)
- [About](#about)
- [Installation](#installation)
- [With Docker](#with-docker)
- [With Kubernetes](#with-kubernetes)
- [From a package repository](#from-a-package-repository)
- [From source](#from-source)
- [Backend](#backend)
- [Frontend](#frontend)
- [Cross-compilation](#cross-compilation)
- [Usage](#usage)
- [Recommended architecture](#recommended-architecture)
- [Client configuration](#client-configuration)
- [Compatible services](#compatible-services)
- [General configuration guide](#general-configuration-guide)
- [Sample client configurations](#sample-client-configurations)
- [Incompatible services](#incompatible-services)
- [Migrating from SQLite](#migrating-from-sqlite)
- [Comparisons with other services](#comparisons-with-other-services)
- [vs OpenLDAP](#vs-openldap)
- [vs FreeIPA](#vs-freeipa)
- [vs Kanidm](#vs-kanidm)
- [I can't log in!](#i-cant-log-in)
- [Contributions](#contributions)
## About
@@ -51,7 +65,7 @@ many backends, from KeyCloak to Authelia to Nextcloud and
[more](#compatible-services)!
<img
src="https://raw.githubusercontent.com/nitnelave/lldap/master/screenshot.png"
src="https://raw.githubusercontent.com/lldap/lldap/master/screenshot.png"
alt="Screenshot of the user list page"
width="50%"
align="right"
@@ -62,10 +76,11 @@ edit their own details or reset their password by email.
The goal is _not_ to provide a full LDAP server; if you're interested in that,
check out OpenLDAP. This server is a user management system that is:
* simple to setup (no messing around with `slapd`),
* simple to manage (friendly web UI),
* low resources,
* opinionated with basic defaults so you don't have to understand the
- simple to setup (no messing around with `slapd`),
- simple to manage (friendly web UI),
- low resources,
- opinionated with basic defaults so you don't have to understand the
subtleties of LDAP.
It mostly targets self-hosting servers, with open-source components like
@@ -76,13 +91,17 @@ For more features (OAuth/OpenID support, reverse proxy, ...) you can install
other components (KeyCloak, Authelia, ...) using this server as the source of
truth for users, via LDAP.
By default, the data is stored in SQLite, but you can swap the backend with
MySQL/MariaDB or PostgreSQL.
## Installation
### With Docker
The image is available at `nitnelave/lldap`. You should persist the `/data`
folder, which contains your configuration, the database and the private key
file.
The image is available at `lldap/lldap`. You should persist the `/data`
folder, which contains your configuration and the SQLite database (you can
remove this step if you use a different DB and configure with environment
variables only).
Configure the server by copying the `lldap_config.docker_template.toml` to
`/data/lldap_config.toml` and updating the configuration values (especially the
@@ -90,26 +109,38 @@ Configure the server by copying the `lldap_config.docker_template.toml` to
Environment variables should be prefixed with `LLDAP_` to override the
configuration.
If the `lldap_config.toml` doesn't exist when starting up, LLDAP will use
default one. The default admin password is `password`, you can change the
password later using the web interface.
Secrets can also be set through a file. The filename should be specified by the
variables `LLDAP_JWT_SECRET_FILE` or `LLDAP_LDAP_USER_PASS_FILE`, and the file
variables `LLDAP_JWT_SECRET_FILE` or `LLDAP_KEY_SEED_FILE`, and the file
contents are loaded into the respective configuration parameters. Note that
`_FILE` variables take precedence.
Example for docker compose:
- You can use either the `:latest` tag image or `:stable` as used in this example.
- `:latest` tag image contains recently pushed code or feature tests, in which some instability can be expected.
- If `UID` and `GID` no defined LLDAP will use default `UID` and `GID` number `1000`.
- If no `TZ` is set, default `UTC` timezone will be used.
- You can generate the secrets by running `./generate_secrets.sh`
```yaml
version: "3"
volumes:
lldap_data:
driver: local
services:
lldap:
image: nitnelave/lldap:stable
# Change this to the user:group you want.
user: "33:33"
image: lldap/lldap:stable
ports:
# For LDAP
- "3890:3890"
# For LDAP, not recommended to expose, see Usage section.
#- "3890:3890"
# For LDAPS (LDAP Over SSL), enable port if LLDAP_LDAPS_OPTIONS__ENABLED set true, look env below
#- "6360:6360"
# For the web front-end
- "17170:17170"
volumes:
@@ -117,29 +148,105 @@ services:
# Alternatively, you can mount a local folder
# - "./lldap_data:/data"
environment:
- UID=####
- GID=####
- TZ=####/####
- LLDAP_JWT_SECRET=REPLACE_WITH_RANDOM
- LLDAP_LDAP_USER_PASS=REPLACE_WITH_PASSWORD
- LLDAP_KEY_SEED=REPLACE_WITH_RANDOM
- LLDAP_LDAP_BASE_DN=dc=example,dc=com
# If using LDAPS, set enabled true and configure cert and key path
# - LLDAP_LDAPS_OPTIONS__ENABLED=true
# - LLDAP_LDAPS_OPTIONS__CERT_FILE=/path/to/certfile.crt
# - LLDAP_LDAPS_OPTIONS__KEY_FILE=/path/to/keyfile.key
# You can also set a different database:
# - LLDAP_DATABASE_URL=mysql://mysql-user:password@mysql-server/my-database
# - LLDAP_DATABASE_URL=postgres://postgres-user:password@postgres-server/my-database
```
Then the service will listen on two ports, one for LDAP and one for the web
front-end.
### With Kubernetes
See https://github.com/Evantage-WS/lldap-kubernetes for a LLDAP deployment for Kubernetes
You can bootstrap your lldap instance (users, groups)
using [bootstrap.sh](example_configs/bootstrap/bootstrap.md#kubernetes-job).
It can be run by Argo CD for managing users in git-opt way, or as a one-shot job.
### From a package repository
**Do not open issues in this repository for problems with third-party
pre-built packages. Report issues downstream.**
Depending on the distribution you use, it might be possible to install lldap
from a package repository, officially supported by the distribution or
community contributed.
#### Debian, CentOS Fedora, OpenSUSE, Ubuntu
The package for these distributions can be found at [LLDAP OBS](https://software.opensuse.org//download.html?project=home%3AMasgalor%3ALLDAP&package=lldap).
- When using the distributed package, the default login is `admin/password`. You can change that from the web UI after starting the service.
#### Arch Linux
Arch Linux offers unofficial support through the [Arch User Repository
(AUR)](https://wiki.archlinux.org/title/Arch_User_Repository).
Available package descriptions in AUR are:
- [lldap](https://aur.archlinux.org/packages/lldap) - Builds the latest stable version.
- [lldap-bin](https://aur.archlinux.org/packages/lldap-bin) - Uses the latest
pre-compiled binaries from the [releases in this repository](https://github.com/lldap/lldap/releases).
This package is recommended if you want to run lldap on a system with
limited resources.
- [lldap-git](https://aur.archlinux.org/packages/lldap-git) - Builds the
latest main branch code.
The package descriptions can be used
[to create and install packages](https://wiki.archlinux.org/title/Arch_User_Repository#Getting_started).
Each package places lldap's configuration file at `/etc/lldap.toml` and offers
[systemd service](https://wiki.archlinux.org/title/systemd#Using_units)
`lldap.service` to (auto-)start and stop lldap.
### From source
#### Backend
To compile the project, you'll need:
- curl and gzip: `sudo apt install curl gzip`
- Rust/Cargo: [rustup.rs](https://rustup.rs/)
Then you can compile the server (and the migration tool if you want):
```shell
cargo build --release -p lldap -p lldap_migration_tool
```
The resulting binaries will be in `./target/release/`. Alternatively, you can
just run `cargo run -- run` to run the server.
#### Frontend
To bring up the server, you'll need to compile the frontend. In addition to
cargo, you'll need:
`cargo`, you'll need WASM-pack, which can be installed by running `cargo install wasm-pack`.
* WASM-pack: `cargo install wasm-pack`
* rollup.js: `npm install rollup`
Then you can build the frontend files with
Then you can build the frontend files with `./app/build.sh` (you'll need to run
this after every front-end change to update the WASM package served).
```shell
./app/build.sh
```
To bring up the server, just run `cargo run`. The default config is in
`src/infra/configuration.rs`, but you can override it by creating an
`lldap_config.toml`, setting environment variables or passing arguments to
`cargo run`.
(you'll need to run this after every front-end change to update the WASM
package served).
The default config is in `src/infra/configuration.rs`, but you can override it
by creating an `lldap_config.toml`, setting environment variables or passing
arguments to `cargo run`. Have a look at the docker template:
`lldap_config.docker_template.toml`.
You can also install it as a systemd service, see
[lldap.service](example_configs/lldap.service).
### Cross-compilation
@@ -163,6 +270,47 @@ You can then get the compiled server binary in
Raspberry Pi (or other target), with the folder structure maintained (`app`
files in an `app` folder next to the binary).
## Usage
The simplest way to use LLDAP is through the web front-end. There you can
create users, set passwords, add them to groups and so on. Users can also
connect to the web UI and change their information, or request a password reset
link (if you configured the SMTP client).
Creating and managing custom attributes is currently in Beta. It's not
supported in the Web UI. The recommended way is to use
[Zepmann/lldap-cli](https://github.com/Zepmann/lldap-cli), a
community-contributed CLI frontend.
LLDAP is also very scriptable, through its GraphQL API. See the
[Scripting](docs/scripting.md) docs for more info.
### Recommended architecture
If you are using containers, a sample architecture could look like this:
- A reverse proxy (e.g. nginx or Traefik)
- An authentication service (e.g. Authelia, Authentik or KeyCloak) connected to
LLDAP to provide authentication for non-authenticated services, or to provide
SSO with compatible ones.
- The LLDAP service, with the web port exposed to Traefik.
- The LDAP port doesn't need to be exposed, since only the other containers
will access it.
- You can also set up LDAPS if you want to expose the LDAP port to the
internet (not recommended) or for an extra layer of security in the
inter-container communication (though it's very much optional).
- The default LLDAP container starts up as root to fix up some files'
permissions before downgrading the privilege to the given user. However,
you can (should?) use the `*-rootless` version of the images to be able to
start directly as that user, once you got the permissions right. Just don't
forget to change from the `UID/GID` env vars to the `uid` docker-compose
field.
- Any other service that needs to connect to LLDAP for authentication (e.g.
NextCloud) can be added to a shared network with LLDAP. The finest
granularity is a network for each pair of LLDAP-service, but there are often
coarser granularities that make sense (e.g. a network for the \*arr stack and
LLDAP).
## Client configuration
### Compatible services
@@ -176,14 +324,15 @@ the config).
### General configuration guide
To configure the services that will talk to LLDAP, here are the values:
- The LDAP user DN is from the configuration. By default,
`cn=admin,ou=people,dc=example,dc=com`.
- The LDAP password is from the configuration (same as to log in to the web
UI).
- The users are all located in `ou=people,` + the base DN, so by default user
`bob` is at `cn=bob,ou=people,dc=example,dc=com`.
- Similarly, the groups are located in `ou=groups`, so the group `family`
will be at `cn=family,ou=groups,dc=example,dc=com`.
- The LDAP user DN is from the configuration. By default,
`cn=admin,ou=people,dc=example,dc=com`.
- The LDAP password is from the configuration (same as to log in to the web
UI).
- The users are all located in `ou=people,` + the base DN, so by default user
`bob` is at `cn=bob,ou=people,dc=example,dc=com`.
- Similarly, the groups are located in `ou=groups`, so the group `family`
will be at `cn=family,ou=groups,dc=example,dc=com`.
Testing group membership through `memberOf` is supported, so you can have a
filter like: `(memberOf=cn=admins,ou=groups,dc=example,dc=com)`.
@@ -198,75 +347,150 @@ administration access to many services.
Some specific clients have been tested to work and come with sample
configuration files, or guides. See the [`example_configs`](example_configs)
folder for help with:
- [Apache Guacamole](example_configs/apacheguacamole.md)
- [Authelia](example_configs/authelia_config.yml)
- [Bookstack](example_configs/bookstack.env.example)
- [Calibre-Web](example_configs/calibre_web.md)
- [Dolibarr](example_configs/dolibarr.md)
- [Emby](example_configs/emby.md)
- [Gitea](example_configs/gitea.md)
- [Grafana](example_configs/grafana_ldap_config.toml)
- [Jellyfin](example_configs/jellyfin.md)
- [Jisti Meet](example_configs/jitsi_meet.conf)
- [KeyCloak](example_configs/keycloak.md)
- [Matrix](example_configs/matrix_synapse.yml)
- [Organizr](example_configs/Organizr.md)
- [Portainer](example_configs/portainer.md)
- [Seafile](example_configs/seafile.md)
- [Syncthing](example_configs/syncthing.md)
- [WG Portal](example_configs/wg_portal.env.example)
- [Airsonic Advanced](example_configs/airsonic-advanced.md)
- [Apache Guacamole](example_configs/apacheguacamole.md)
- [Apereo CAS Server](example_configs/apereo_cas_server.md)
- [Authelia](example_configs/authelia_config.yml)
- [Authentik](example_configs/authentik.md)
- [Bookstack](example_configs/bookstack.env.example)
- [Calibre-Web](example_configs/calibre_web.md)
- [Dell iDRAC](example_configs/dell_idrac.md)
- [Dex](example_configs/dex_config.yml)
- [Dokuwiki](example_configs/dokuwiki.md)
- [Dolibarr](example_configs/dolibarr.md)
- [Ejabberd](example_configs/ejabberd.md)
- [Emby](example_configs/emby.md)
- [Ergo IRCd](example_configs/ergo.md)
- [Gitea](example_configs/gitea.md)
- [GitLab](example_configs/gitlab.md)
- [Grafana](example_configs/grafana_ldap_config.toml)
- [Grocy](example_configs/grocy.md)
- [Hedgedoc](example_configs/hedgedoc.md)
- [Home Assistant](example_configs/home-assistant.md)
- [Jellyfin](example_configs/jellyfin.md)
- [Jenkins](example_configs/jenkins.md)
- [Jitsi Meet](example_configs/jitsi_meet.conf)
- [Kasm](example_configs/kasm.md)
- [KeyCloak](example_configs/keycloak.md)
- [LibreNMS](example_configs/librenms.md)
- [Mastodon](example_configs/mastodon.env.example)
- [Matrix](example_configs/matrix_synapse.yml)
- [Mealie](example_configs/mealie.md)
- [MinIO](example_configs/minio.md)
- [Nextcloud](example_configs/nextcloud.md)
- [Nexus](example_configs/nexus.md)
- [Organizr](example_configs/Organizr.md)
- [Portainer](example_configs/portainer.md)
- [PowerDNS Admin](example_configs/powerdns_admin.md)
- [Proxmox VE](example_configs/proxmox.md)
- [Rancher](example_configs/rancher.md)
- [Seafile](example_configs/seafile.md)
- [Shaarli](example_configs/shaarli.md)
- [Squid](example_configs/squid.md)
- [Syncthing](example_configs/syncthing.md)
- [TheLounge](example_configs/thelounge.md)
- [Traccar](example_configs/traccar.xml)
- [Vaultwarden](example_configs/vaultwarden.md)
- [WeKan](example_configs/wekan.md)
- [WG Portal](example_configs/wg_portal.env.example)
- [WikiJS](example_configs/wikijs.md)
- [XBackBone](example_configs/xbackbone_config.php)
- [Zendto](example_configs/zendto.md)
- [Zitadel](example_configs/zitadel.md)
- [Zulip](example_configs/zulip.md)
### Incompatible services
Though we try to be maximally compatible, not every feature is supported; LLDAP
is not a fully-featured LDAP server, intentionally so.
LDAP browsing tools are generally not supported, though they could be. If you
need to use one but it behaves weirdly, please file a bug.
Some services use features that are not implemented, or require specific
attributes. You can try to create those attributes (see custom attributes in
the [Usage](#usage) section).
Finally, some services require password hashes so they can validate themselves
the user's password without contacting LLDAP. This is not and will not be
supported, it's incompatible with our password hashing scheme (a zero-knowledge
proof). Furthermore, it's generally not recommended in terms of security, since
it duplicates the places from which a password hash could leak.
In that category, the most prominent is Synology. It is, to date, the only
service that seems definitely incompatible with LLDAP.
## Migrating from SQLite
If you started with an SQLite database and would like to migrate to
MySQL/MariaDB or PostgreSQL, check out the [DB
migration docs](/docs/database_migration.md).
## Comparisons with other services
### vs OpenLDAP
OpenLDAP is a monster of a service that implements all of LDAP and all of its
extensions, plus some of its own. That said, if you need all that flexibility,
it might be what you need! Note that installation can be a bit painful
(figuring out how to use `slapd`) and people have mixed experiences following
tutorials online. If you don't configure it properly, you might end up storing
passwords in clear, so a breach of your server would reveal all the stored
passwords!
[OpenLDAP](https://www.openldap.org) is a monster of a service that implements
all of LDAP and all of its extensions, plus some of its own. That said, if you
need all that flexibility, it might be what you need! Note that installation
can be a bit painful (figuring out how to use `slapd`) and people have mixed
experiences following tutorials online. If you don't configure it properly, you
might end up storing passwords in clear, so a breach of your server would
reveal all the stored passwords!
OpenLDAP doesn't come with a UI: if you want a web interface, you'll have to
install one (not that many that look nice) and configure it.
install one (not that many look nice) and configure it.
LLDAP is much simpler to setup, has a much smaller image (10x smaller, 20x if
you add PhpLdapAdmin), and comes packed with its own purpose-built wed UI.
you add PhpLdapAdmin), and comes packed with its own purpose-built web UI.
However, it's not as flexible as OpenLDAP.
### vs FreeIPA
FreeIPA is the one-stop shop for identity management: LDAP, Kerberos, NTP, DNS,
Samba, you name it, it has it. In addition to user management, it also does
security policies, single sign-on, certificate management, linux account
management and so on.
[FreeIPA](http://www.freeipa.org) is the one-stop shop for identity management:
LDAP, Kerberos, NTP, DNS, Samba, you name it, it has it. In addition to user
management, it also does security policies, single sign-on, certificate
management, linux account management and so on.
If you need all of that, go for it! Keep in mind that a more complex system is
more complex to maintain, though.
LLDAP is much lighter to run (<100 MB RAM including the DB), easier to
LLDAP is much lighter to run (<10 MB RAM including the DB), easier to
configure (no messing around with DNS or security policies) and simpler to
use. It also comes conveniently packed in a docker container.
### vs Kanidm
[Kanidm](https://kanidm.com) is an up-and-coming Rust identity management
platform, covering all your bases: OAuth, Linux accounts, SSH keys, Radius,
WebAuthn. It comes with a (read-only) LDAPS server.
It's fairly easy to install and does much more; but their LDAP server is
read-only, and by having more moving parts it is inherently more complex. If
you don't need to modify the users through LDAP and you're planning on
installing something like [KeyCloak](https://www.keycloak.org) to provide
modern identity protocols, check out Kanidm.
## I can't log in!
If you just set up the server, can get to the login page but the password you
set isn't working, try the following:
- (For docker): Make sure that the `/data` folder is persistent, either to a
docker volume or mounted from the host filesystem.
- Check if there is a `lldap_config.toml` file (either in `/data` for docker
or in the current directory). If there isn't, copy
`lldap_config.docker_template.toml` there, and fill in the various values
(passwords, secrets, ...).
- Check if there is a `users.db` file (either in `/data` for docker or where
you specified the DB URL, which defaults to the current directory). If
there isn't, check that the user running the command (user with ID 10001
for docker) has the rights to write to the `/data` folder. If in doubt, you
can `chmod 777 /data` (or whatever the folder) to make it world-writeable.
- Make sure you restart the server.
- If it's still not working, join the
[Discord server](https://discord.gg/h5PEdRMNyP) to ask for help.
- (For docker): Make sure that the `/data` folder is persistent, either to a
docker volume or mounted from the host filesystem.
- Check if there is a `lldap_config.toml` file (either in `/data` for docker
or in the current directory). If there isn't, copy
`lldap_config.docker_template.toml` there, and fill in the various values
(passwords, secrets, ...).
- Check if there is a `users.db` file (either in `/data` for docker or where
you specified the DB URL, which defaults to the current directory). If
there isn't, check that the user running the command (user with ID 10001
for docker) has the rights to write to the `/data` folder. If in doubt, you
can `chmod 777 /data` (or whatever the folder) to make it world-writeable.
- Make sure you restart the server.
- If it's still not working, join the
[Discord server](https://discord.gg/h5PEdRMNyP) to ask for help.
## Contributions

View File

@@ -1,25 +1,33 @@
[package]
name = "lldap_app"
version = "0.4.0"
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
description = "Frontend for LLDAP"
edition = "2021"
homepage = "https://github.com/lldap/lldap"
license = "GPL-3.0-only"
name = "lldap_app"
repository = "https://github.com/lldap/lldap"
version = "0.5.1-alpha"
include = ["src/**/*", "queries/**/*", "Cargo.toml", "../schema.graphql"]
[dependencies]
anyhow = "1"
base64 = "0.13"
gloo-console = "0.2.3"
gloo-file = "0.2.3"
gloo-net = "*"
graphql_client = "0.10"
http = "0.2"
jwt = "0.13"
rand = "0.8"
serde = "1"
serde_json = "1"
url-escape = "0.1.1"
validator = "=0.14"
validator_derive = "*"
wasm-bindgen = "0.2"
yew = "0.18"
yewtil = "*"
yew-router = "0.15"
yew_form = "0.1.8"
yew_form_derive = "*"
wasm-bindgen-futures = "*"
yew = "0.19.3"
yew-router = "0.16"
# Needed because of https://github.com/tkaitchuck/aHash/issues/95
indexmap = "=1.6.2"
@@ -29,6 +37,7 @@ version = "0.3"
features = [
"Document",
"Element",
"FileReader",
"HtmlDocument",
"HtmlInputElement",
"HtmlOptionElement",
@@ -47,5 +56,18 @@ features = [
path = "../auth"
features = [ "opaque_client" ]
[dependencies.image]
features = ["jpeg"]
default-features = false
version = "0.24"
[dependencies.yew_form]
git = "https://github.com/jfbilodeau/yew_form"
rev = "4b9fabffb63393ec7626a4477fd36de12a07fac9"
[dependencies.yew_form_derive]
git = "https://github.com/jfbilodeau/yew_form"
rev = "4b9fabffb63393ec7626a4477fd36de12a07fac9"
[lib]
crate-type = ["cdylib"]

View File

@@ -6,22 +6,12 @@ then
>&2 echo '`wasm-pack` not found. Try running `cargo install wasm-pack`'
exit 1
fi
wasm-pack build --target web
ROLLUP_BIN=$(which rollup 2>/dev/null)
if [ -f ../node_modules/rollup/dist/bin/rollup ]
if ! which gzip > /dev/null 2>&1
then
ROLLUP_BIN=../node_modules/rollup/dist/bin/rollup
elif [ -f node_modules/rollup/dist/bin/rollup ]
then
ROLLUP_BIN=node_modules/rollup/dist/bin/rollup
fi
if [ -z "$ROLLUP_BIN" ]
then
>&2 echo '`rollup` not found. Try running `npm install rollup`'
>&2 echo '`gzip` not found.'
exit 1
fi
$ROLLUP_BIN ./main.js --format iife --file ./pkg/bundle.js --globals bootstrap:bootstrap
wasm-pack build --target web --release
gzip -9 -k -f pkg/lldap_app_bg.wasm

View File

@@ -4,17 +4,22 @@
<head>
<meta charset="utf-8" />
<title>LLDAP Administration</title>
<script src="/pkg/bundle.js" defer></script>
<base href="/">
<script src="static/main.js" type="module" defer></script>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css"
href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-nightshade.min.css"
rel="preload stylesheet"
integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x"
integrity="sha384-CvItGYrXmque42UjYhp+bjRR8tgQz78Nlwk42gYsNzBc6y0DuXNtdUaRzr1cl2uK"
crossorigin="anonymous"
as="style" />
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-/bQdsTh/da6pkI1MST/rWKFNjaCP5gBSY4sEBT38Q/9RBh9AH40zEOg7Hlq2THRZ"
crossorigin="anonymous"></script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/js/darkmode.min.js"
integrity="sha384-A4SLs39X/aUfwRclRaXvNeXNBTLZdnZdHhhteqbYFS2jZTRD79tKeFeBn7SGXNpi"
crossorigin="anonymous"></script>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css"
@@ -29,11 +34,33 @@
href="https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap" />
<link
rel="stylesheet"
href="/static/style.css" />
href="static/style.css" />
<script>
function inDarkMode(){
return darkmode.inDarkMode;
}
</script>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<noscript>
<!-- This will be displayed if the user doesn't have JavaScript enabled. -->
LLDAP requires JavaScript, please switch to a compatible browser or
enable it.
</noscript>
<script>
/* Detect if the user has WASM support. */
if (typeof WebAssembly === 'undefined') {
const pWASMMsg = document.createElement("p")
pWASMMsg.innerHTML = `
LLDAP requires WASM and JIT for JavaScript, please switch to a
compatible browser or enable it.
`
document.body.appendChild(pWASMMsg)
}
</script>
</body>
</html>

View File

@@ -4,15 +4,18 @@
<head>
<meta charset="utf-8" />
<title>LLDAP Administration</title>
<script src="/pkg/bundle.js" defer></script>
<script src="/static/main.js" type="module" defer></script>
<link
href="/static/bootstrap.min.css"
href="/static/bootstrap-nightshade.min.css"
rel="preload stylesheet"
integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x"
integrity="sha384-CvItGYrXmque42UjYhp+bjRR8tgQz78Nlwk42gYsNzBc6y0DuXNtdUaRzr1cl2uK"
as="style" />
<script
src="/static/bootstrap.bundle.min.js"
integrity="sha384-/bQdsTh/da6pkI1MST/rWKFNjaCP5gBSY4sEBT38Q/9RBh9AH40zEOg7Hlq2THRZ"></script>
<script
src="/static/darkmode.min.js"
integrity="sha384-A4SLs39X/aUfwRclRaXvNeXNBTLZdnZdHhhteqbYFS2jZTRD79tKeFeBn7SGXNpi"></script>
<link
rel="stylesheet"
href="/static/bootstrap-icons.css"
@@ -28,10 +31,32 @@
<link
rel="stylesheet"
href="/static/style.css" />
<script>
function inDarkMode(){
return darkmode.inDarkMode;
}
</script>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<noscript>
<!-- This will be displayed if the user doesn't have JavaScript enabled. -->
LLDAP requires JavaScript, please switch to a compatible browser or
enable it.
</noscript>
<script>
/* Detect if the user has WASM support. */
if (typeof WebAssembly === 'undefined') {
const pWASMMsg = document.createElement("p")
pWASMMsg.innerHTML = `
LLDAP requires WASM and JIT for JavaScript, please switch to a
compatible browser or enable it.
`
document.body.appendChild(pWASMMsg)
}
</script>
</body>
</html>

View File

@@ -1,6 +0,0 @@
import init, { run_app } from './pkg/lldap_app.js';
async function main() {
await init('/pkg/lldap_app_bg.wasm');
run_app();
}
main()

View File

@@ -2,6 +2,8 @@ query GetGroupDetails($id: Int!) {
group(groupId: $id) {
id
displayName
creationDate
uuid
users {
id
displayName

View File

@@ -2,5 +2,6 @@ query GetGroupList {
groups {
id
displayName
creationDate
}
}

View File

@@ -5,7 +5,9 @@ query GetUserDetails($id: String!) {
displayName
firstName
lastName
avatar
creationDate
uuid
groups {
id
displayName

View File

@@ -52,23 +52,25 @@ pub struct Props {
}
impl CommonComponent<AddGroupMemberComponent> for AddGroupMemberComponent {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg {
Msg::UserListResponse(response) => {
self.user_list = Some(response?.users);
self.common.cancel_task();
}
Msg::SubmitAddMember => return self.submit_add_member(),
Msg::SubmitAddMember => return self.submit_add_member(ctx),
Msg::AddMemberResponse(response) => {
response?;
self.common.cancel_task();
let user = self
.selected_user
.as_ref()
.expect("Could not get selected user")
.clone();
// Remove the user from the dropdown.
self.common.on_user_added_to_group.emit(user);
ctx.props().on_user_added_to_group.emit(user);
}
Msg::SelectionChanged(option_props) => {
let was_some = self.selected_user.is_some();
@@ -88,23 +90,25 @@ impl CommonComponent<AddGroupMemberComponent> for AddGroupMemberComponent {
}
impl AddGroupMemberComponent {
fn get_user_list(&mut self) {
fn get_user_list(&mut self, ctx: &Context<Self>) {
self.common.call_graphql::<ListUserNames, _>(
ctx,
list_user_names::Variables { filters: None },
Msg::UserListResponse,
"Error trying to fetch user list",
);
}
fn submit_add_member(&mut self) -> Result<bool> {
fn submit_add_member(&mut self, ctx: &Context<Self>) -> Result<bool> {
let user_id = match self.selected_user.clone() {
None => return Ok(false),
Some(user) => user.id,
};
self.common.call_graphql::<AddUserToGroup, _>(
ctx,
add_user_to_group::Variables {
user: user_id,
group: self.common.group_id,
group: ctx.props().group_id,
},
Msg::AddMemberResponse,
"Error trying to initiate adding the user to a group",
@@ -112,8 +116,8 @@ impl AddGroupMemberComponent {
Ok(true)
}
fn get_selectable_user_list(&self, user_list: &[User]) -> Vec<User> {
let user_groups = self.common.users.iter().collect::<HashSet<_>>();
fn get_selectable_user_list(&self, ctx: &Context<Self>, user_list: &[User]) -> Vec<User> {
let user_groups = ctx.props().users.iter().collect::<HashSet<_>>();
user_list
.iter()
.filter(|u| !user_groups.contains(u))
@@ -126,41 +130,39 @@ impl Component for AddGroupMemberComponent {
type Message = Msg;
type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
fn create(ctx: &Context<Self>) -> Self {
let mut res = Self {
common: CommonComponentParts::<Self>::create(props, link),
common: CommonComponentParts::<Self>::create(),
user_list: None,
selected_user: None,
};
res.get_user_list();
res.get_user_list(ctx);
res
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update_and_report_error(
self,
ctx,
msg,
self.common.on_error.clone(),
ctx.props().on_error.clone(),
)
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.common.change(props)
}
fn view(&self) -> Html {
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
if let Some(user_list) = &self.user_list {
let to_add_user_list = self.get_selectable_user_list(user_list);
let to_add_user_list = self.get_selectable_user_list(ctx, user_list);
#[allow(unused_braces)]
let make_select_option = |user: User| {
html_nested! {
<SelectOption value=user.id.clone() text=user.display_name.clone() key=user.id />
<SelectOption value={user.id.clone()} text={user.display_name.clone()} key={user.id} />
}
};
html! {
<div class="row">
<div class="col-sm-3">
<Select on_selection_change=self.common.callback(Msg::SelectionChanged)>
<Select on_selection_change={link.callback(Msg::SelectionChanged)}>
{
to_add_user_list
.into_iter()
@@ -169,12 +171,13 @@ impl Component for AddGroupMemberComponent {
}
</Select>
</div>
<div class="col-sm-1">
<div class="col-3">
<button
class="btn btn-success"
disabled=self.selected_user.is_none() || self.common.is_task_running()
onclick=self.common.callback(|_| Msg::SubmitAddMember)>
{"Add"}
class="btn btn-secondary"
disabled={self.selected_user.is_none() || self.common.is_task_running()}
onclick={link.callback(|_| Msg::SubmitAddMember)}>
<i class="bi-person-plus me-2"></i>
{"Add to group"}
</button>
</div>
</div>

View File

@@ -64,16 +64,18 @@ pub struct Props {
}
impl CommonComponent<AddUserToGroupComponent> for AddUserToGroupComponent {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg {
Msg::GroupListResponse(response) => {
self.group_list = Some(response?.groups.into_iter().map(Into::into).collect());
self.common.cancel_task();
}
Msg::SubmitAddGroup => return self.submit_add_group(),
Msg::SubmitAddGroup => return self.submit_add_group(ctx),
Msg::AddGroupResponse(response) => {
response?;
self.common.cancel_task();
// Adding the user to the group succeeded, we're not in the process of adding a
// group anymore.
let group = self
@@ -82,7 +84,7 @@ impl CommonComponent<AddUserToGroupComponent> for AddUserToGroupComponent {
.expect("Could not get selected group")
.clone();
// Remove the group from the dropdown.
self.common.on_user_added_to_group.emit(group);
ctx.props().on_user_added_to_group.emit(group);
}
Msg::SelectionChanged(option_props) => {
let was_some = self.selected_group.is_some();
@@ -102,22 +104,24 @@ impl CommonComponent<AddUserToGroupComponent> for AddUserToGroupComponent {
}
impl AddUserToGroupComponent {
fn get_group_list(&mut self) {
fn get_group_list(&mut self, ctx: &Context<Self>) {
self.common.call_graphql::<GetGroupList, _>(
ctx,
get_group_list::Variables,
Msg::GroupListResponse,
"Error trying to fetch group list",
);
}
fn submit_add_group(&mut self) -> Result<bool> {
fn submit_add_group(&mut self, ctx: &Context<Self>) -> Result<bool> {
let group_id = match &self.selected_group {
None => return Ok(false),
Some(group) => group.id,
};
self.common.call_graphql::<AddUserToGroup, _>(
ctx,
add_user_to_group::Variables {
user: self.common.username.clone(),
user: ctx.props().username.clone(),
group: group_id,
},
Msg::AddGroupResponse,
@@ -126,8 +130,8 @@ impl AddUserToGroupComponent {
Ok(true)
}
fn get_selectable_group_list(&self, group_list: &[Group]) -> Vec<Group> {
let user_groups = self.common.groups.iter().collect::<HashSet<_>>();
fn get_selectable_group_list(&self, props: &Props, group_list: &[Group]) -> Vec<Group> {
let user_groups = props.groups.iter().collect::<HashSet<_>>();
group_list
.iter()
.filter(|g| !user_groups.contains(g))
@@ -139,41 +143,39 @@ impl AddUserToGroupComponent {
impl Component for AddUserToGroupComponent {
type Message = Msg;
type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
fn create(ctx: &Context<Self>) -> Self {
let mut res = Self {
common: CommonComponentParts::<Self>::create(props, link),
common: CommonComponentParts::<Self>::create(),
group_list: None,
selected_group: None,
};
res.get_group_list();
res.get_group_list(ctx);
res
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update_and_report_error(
self,
ctx,
msg,
self.common.on_error.clone(),
ctx.props().on_error.clone(),
)
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.common.change(props)
}
fn view(&self) -> Html {
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
if let Some(group_list) = &self.group_list {
let to_add_group_list = self.get_selectable_group_list(group_list);
let to_add_group_list = self.get_selectable_group_list(ctx.props(), group_list);
#[allow(unused_braces)]
let make_select_option = |group: Group| {
html_nested! {
<SelectOption value=group.id.to_string() text=group.display_name key=group.id />
<SelectOption value={group.id.to_string()} text={group.display_name} key={group.id} />
}
};
html! {
<div class="row">
<div class="col-sm-3">
<Select on_selection_change=self.common.callback(Msg::SelectionChanged)>
<Select on_selection_change={link.callback(Msg::SelectionChanged)}>
{
to_add_group_list
.into_iter()
@@ -182,12 +184,13 @@ impl Component for AddUserToGroupComponent {
}
</Select>
</div>
<div class="col-sm-1">
<div class="col-sm-3">
<button
class="btn btn-success"
disabled=self.selected_group.is_none() || self.common.is_task_running()
onclick=self.common.callback(|_| Msg::SubmitAddGroup)>
{"Add"}
class="btn btn-secondary"
disabled={self.selected_group.is_none() || self.common.is_task_running()}
onclick={link.callback(|_| Msg::SubmitAddGroup)}>
<i class="bi-person-plus me-2"></i>
{"Add to group"}
</button>
</div>
</div>

View File

@@ -9,161 +9,208 @@ use crate::{
logout::LogoutButton,
reset_password_step1::ResetPasswordStep1Form,
reset_password_step2::ResetPasswordStep2Form,
router::{AppRoute, Link, NavButton},
router::{AppRoute, Link, Redirect},
user_details::UserDetails,
user_table::UserTable,
},
infra::cookies::get_cookie,
};
use yew::prelude::*;
use yew::services::ConsoleService;
use yew_router::{
agent::{RouteAgentDispatcher, RouteRequest},
route::Route,
router::Router,
service::RouteService,
infra::{api::HostService, cookies::get_cookie},
};
use gloo_console::error;
use wasm_bindgen::prelude::*;
use yew::{
function_component,
html::Scope,
prelude::{html, Component, Html},
Context,
};
use yew_router::{
prelude::{History, Location},
scope_ext::RouterScopeExt,
BrowserRouter, Switch,
};
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = darkmode)]
fn toggleDarkMode(doSave: bool);
#[wasm_bindgen]
fn inDarkMode() -> bool;
}
#[function_component(DarkModeToggle)]
pub fn dark_mode_toggle() -> Html {
html! {
<div class="form-check form-switch">
<input class="form-check-input" onclick={|_| toggleDarkMode(true)} type="checkbox" id="darkModeToggle" checked={inDarkMode()}/>
<label class="form-check-label" for="darkModeToggle">{"Dark mode"}</label>
</div>
}
}
#[function_component(AppContainer)]
pub fn app_container() -> Html {
html! {
<BrowserRouter>
<App />
</BrowserRouter>
}
}
pub struct App {
link: ComponentLink<Self>,
user_info: Option<(String, bool)>,
redirect_to: Option<AppRoute>,
route_dispatcher: RouteAgentDispatcher,
password_reset_enabled: Option<bool>,
}
pub enum Msg {
Login((String, bool)),
Logout,
PasswordResetProbeFinished(anyhow::Result<bool>),
}
impl Component for App {
type Message = Msg;
type Properties = ();
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
let mut app = Self {
link,
fn create(ctx: &Context<Self>) -> Self {
let app = Self {
user_info: get_cookie("user_id")
.unwrap_or_else(|e| {
ConsoleService::error(&e.to_string());
error!(&e.to_string());
None
})
.and_then(|u| {
get_cookie("is_admin")
.map(|so| so.map(|s| (u, s == "true")))
.unwrap_or_else(|e| {
ConsoleService::error(&e.to_string());
error!(&e.to_string());
None
})
}),
redirect_to: Self::get_redirect_route(),
route_dispatcher: RouteAgentDispatcher::new(),
redirect_to: Self::get_redirect_route(ctx),
password_reset_enabled: None,
};
app.apply_initial_redirections();
ctx.link().send_future(async move {
Msg::PasswordResetProbeFinished(HostService::probe_password_reset().await)
});
app.apply_initial_redirections(ctx);
app
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
let history = ctx.link().history().unwrap();
match msg {
Msg::Login((user_name, is_admin)) => {
self.user_info = Some((user_name.clone(), is_admin));
self.route_dispatcher
.send(RouteRequest::ChangeRoute(Route::from(
self.redirect_to.take().unwrap_or_else(|| {
if is_admin {
AppRoute::ListUsers
} else {
AppRoute::UserDetails(user_name.clone())
}
}),
)));
history.push(self.redirect_to.take().unwrap_or_else(|| {
if is_admin {
AppRoute::ListUsers
} else {
AppRoute::UserDetails {
user_id: user_name.clone(),
}
}
}));
}
Msg::Logout => {
self.user_info = None;
self.redirect_to = None;
history.push(AppRoute::Login);
}
Msg::PasswordResetProbeFinished(Ok(enabled)) => {
self.password_reset_enabled = Some(enabled);
}
Msg::PasswordResetProbeFinished(Err(err)) => {
self.password_reset_enabled = Some(false);
error!(&format!(
"Could not probe for password reset support: {err:#}"
));
}
}
if self.user_info.is_none() {
self.route_dispatcher
.send(RouteRequest::ReplaceRoute(Route::from(AppRoute::Login)));
}
true
}
fn change(&mut self, _: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
let link = self.link.clone();
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link().clone();
let is_admin = self.is_admin();
let password_reset_enabled = self.password_reset_enabled;
html! {
<div class="container shadow-sm py-3">
{self.view_banner()}
<div>
{self.view_banner(ctx)}
<div class="container py-3 bg-kug">
<div class="row justify-content-center" style="padding-bottom: 80px;">
<div class="shadow-sm py-3" style="max-width: 1000px">
<Router<AppRoute>
render = Router::render(move |s| Self::dispatch_route(s, &link, is_admin))
<main class="py-3" style="max-width: 1000px">
<Switch<AppRoute>
render={Switch::render(move |routes| Self::dispatch_route(routes, &link, is_admin, password_reset_enabled))}
/>
</div>
</main>
</div>
{self.view_footer()}
</div>
</div>
}
}
}
impl App {
fn get_redirect_route() -> Option<AppRoute> {
let route_service = RouteService::<()>::new();
let current_route = route_service.get_path();
if current_route.is_empty()
|| current_route == "/"
|| current_route.contains("login")
|| current_route.contains("reset-password")
{
None
} else {
use yew_router::Switch;
AppRoute::from_route_part::<()>(current_route, None).0
}
// Get the page to land on after logging in, defaulting to the index.
fn get_redirect_route(ctx: &Context<Self>) -> Option<AppRoute> {
let route = ctx.link().history().unwrap().location().route::<AppRoute>();
route.filter(|route| {
!matches!(
route,
AppRoute::Index
| AppRoute::Login
| AppRoute::StartResetPassword
| AppRoute::FinishResetPassword { token: _ }
)
})
}
fn apply_initial_redirections(&mut self) {
let route_service = RouteService::<()>::new();
let current_route = route_service.get_path();
if current_route.contains("reset-password") {
return;
}
match &self.user_info {
None => {
self.route_dispatcher
.send(RouteRequest::ReplaceRoute(Route::from(AppRoute::Login)));
fn apply_initial_redirections(&self, ctx: &Context<Self>) {
let history = ctx.link().history().unwrap();
let route = history.location().route::<AppRoute>();
let redirection = match (route, &self.user_info, &self.redirect_to) {
(
Some(AppRoute::StartResetPassword | AppRoute::FinishResetPassword { token: _ }),
_,
_,
) => {
if self.password_reset_enabled == Some(false) {
Some(AppRoute::Login)
} else {
None
}
}
Some((user_name, is_admin)) => match &self.redirect_to {
Some(url) => {
self.route_dispatcher
.send(RouteRequest::ReplaceRoute(Route::from(url.clone())));
(None, _, _) | (_, None, _) => Some(AppRoute::Login),
// User is logged in, a URL was given, don't redirect.
(_, Some(_), Some(_)) => None,
(_, Some((user_name, is_admin)), None) => {
if *is_admin {
Some(AppRoute::ListUsers)
} else {
Some(AppRoute::UserDetails {
user_id: user_name.clone(),
})
}
None => {
if *is_admin {
self.route_dispatcher
.send(RouteRequest::ReplaceRoute(Route::from(AppRoute::ListUsers)));
} else {
self.route_dispatcher
.send(RouteRequest::ReplaceRoute(Route::from(
AppRoute::UserDetails(user_name.clone()),
)));
}
}
},
}
};
if let Some(redirect_to) = redirection {
history.push(redirect_to);
}
}
fn dispatch_route(switch: AppRoute, link: &ComponentLink<Self>, is_admin: bool) -> Html {
fn dispatch_route(
switch: &AppRoute,
link: &Scope<Self>,
is_admin: bool,
password_reset_enabled: Option<bool>,
) -> Html {
match switch {
AppRoute::Login => html! {
<LoginForm on_logged_in=link.callback(Msg::Login)/>
<LoginForm on_logged_in={link.callback(Msg::Login)} password_reset_enabled={password_reset_enabled.unwrap_or(false)}/>
},
AppRoute::CreateUser => html! {
<CreateUserForm/>
@@ -171,7 +218,10 @@ impl App {
AppRoute::Index | AppRoute::ListUsers => html! {
<div>
<UserTable />
<NavButton classes="btn btn-primary" route=AppRoute::CreateUser>{"Create a user"}</NavButton>
<Link classes="btn btn-primary" to={AppRoute::CreateUser}>
<i class="bi-person-plus me-2"></i>
{"Create a user"}
</Link>
</div>
},
AppRoute::CreateGroup => html! {
@@ -180,34 +230,46 @@ impl App {
AppRoute::ListGroups => html! {
<div>
<GroupTable />
<NavButton classes="btn btn-primary" route=AppRoute::CreateGroup>{"Create a group"}</NavButton>
<Link classes="btn btn-primary" to={AppRoute::CreateGroup}>
<i class="bi-plus-circle me-2"></i>
{"Create a group"}
</Link>
</div>
},
AppRoute::GroupDetails(group_id) => html! {
<GroupDetails group_id=group_id />
AppRoute::GroupDetails { group_id } => html! {
<GroupDetails group_id={*group_id} />
},
AppRoute::UserDetails(username) => html! {
<UserDetails username=username is_admin=is_admin />
AppRoute::UserDetails { user_id } => html! {
<UserDetails username={user_id.clone()} is_admin={is_admin} />
},
AppRoute::ChangePassword(username) => html! {
<ChangePasswordForm username=username is_admin=is_admin />
AppRoute::ChangePassword { user_id } => html! {
<ChangePasswordForm username={user_id.clone()} is_admin={is_admin} />
},
AppRoute::StartResetPassword => html! {
<ResetPasswordStep1Form />
AppRoute::StartResetPassword => match password_reset_enabled {
Some(true) => html! { <ResetPasswordStep1Form /> },
Some(false) => {
html! { <Redirect to={AppRoute::Login}/> }
}
None => html! {},
},
AppRoute::FinishResetPassword(token) => html! {
<ResetPasswordStep2Form token=token />
AppRoute::FinishResetPassword { token } => match password_reset_enabled {
Some(true) => html! { <ResetPasswordStep2Form token={token.clone()} /> },
Some(false) => {
html! { <Redirect to={AppRoute::Login}/> }
}
None => html! {},
},
}
}
fn view_banner(&self) -> Html {
fn view_banner(&self, ctx: &Context<Self>) -> Html {
html! {
<header class="p-3 mb-4 border-bottom shadow-sm">
<header class="p-2 mb-3 border-bottom">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<a href="/" class="d-flex align-items-center mb-2 mb-lg-0 me-md-5 text-dark text-decoration-none">
<h1>{"LLDAP"}</h1>
<a href={yew_router::utils::base_url().unwrap_or("/".to_string())} class="d-flex align-items-center mt-2 mb-lg-0 me-md-5 text-decoration-none">
<h2>{"LLDAP"}</h2>
</a>
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
@@ -215,71 +277,85 @@ impl App {
<>
<li>
<Link
classes="nav-link px-2 link-dark h4"
route=AppRoute::ListUsers>
classes="nav-link px-2 h6"
to={AppRoute::ListUsers}>
<i class="bi-people me-2"></i>
{"Users"}
</Link>
</li>
<li>
<Link
classes="nav-link px-2 link-dark h4"
route=AppRoute::ListGroups>
classes="nav-link px-2 h6"
to={AppRoute::ListGroups}>
<i class="bi-collection me-2"></i>
{"Groups"}
</Link>
</li>
</>
} } else { html!{} } }
</ul>
<div class="dropdown text-end">
<a href="#"
class="d-block link-dark text-decoration-none dropdown-toggle"
id="dropdownUser"
data-bs-toggle="dropdown"
aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
fill="currentColor"
class="bi bi-person-circle"
viewBox="0 0 16 16">
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
</svg>
</a>
{if let Some((user_id, _)) = &self.user_info { html! {
<ul
class="dropdown-menu text-small dropdown-menu-lg-end"
aria-labelledby="dropdownUser1"
style="">
<li>
<Link
classes="dropdown-item"
route=AppRoute::UserDetails(user_id.clone())>
{"Profile"}
</Link>
</li>
<li><hr class="dropdown-divider" /></li>
<li>
<LogoutButton on_logged_out=self.link.callback(|_| Msg::Logout) />
</li>
</ul>
} } else { html!{} } }
</div>
{ self.view_user_menu(ctx) }
<DarkModeToggle />
</div>
</div>
</header>
}
}
fn view_user_menu(&self, ctx: &Context<Self>) -> Html {
if let Some((user_id, _)) = &self.user_info {
let link = ctx.link();
html! {
<div class="dropdown text-end">
<a href="#"
class="d-block nav-link text-decoration-none dropdown-toggle"
id="dropdownUser"
data-bs-toggle="dropdown"
aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
fill="currentColor"
class="bi bi-person-circle"
viewBox="0 0 16 16">
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
</svg>
<span class="ms-2">
{user_id}
</span>
</a>
<ul
class="dropdown-menu text-small dropdown-menu-lg-end"
aria-labelledby="dropdownUser1"
style="">
<li>
<Link
classes="dropdown-item"
to={AppRoute::UserDetails{ user_id: user_id.clone() }}>
{"View details"}
</Link>
</li>
<li><hr class="dropdown-divider" /></li>
<li>
<LogoutButton on_logged_out={link.callback(|_| Msg::Logout)} />
</li>
</ul>
</div>
}
} else {
html! {}
}
}
fn view_footer(&self) -> Html {
html! {
<footer class="text-center text-muted fixed-bottom bg-light">
<footer class="text-center fixed-bottom text-muted bg-light py-2">
<div>
<span>{format!("LLDAP version {}", env!("CARGO_PKG_VERSION"))}</span>
</div>
<div>
<a href="https://github.com/nitnelave/lldap" class="me-4 text-reset">
<a href="https://github.com/lldap/lldap" class="me-4 text-reset">
<i class="bi-github"></i>
</a>
<a href="https://discord.gg/h5PEdRMNyP" class="me-4 text-reset">
@@ -290,7 +366,7 @@ impl App {
</a>
</div>
<div>
<span>{"License "}<a href="https://github.com/nitnelave/lldap/blob/main/LICENSE" class="link-secondary">{"GNU GPL"}</a></span>
<span>{"License "}<a href="https://github.com/lldap/lldap/blob/main/LICENSE" class="link-secondary">{"GNU GPL"}</a></span>
</div>
</footer>
}

View File

@@ -1,34 +1,27 @@
use crate::{
components::router::{AppRoute, NavButton},
components::router::{AppRoute, Link},
infra::{
api::HostService,
common_component::{CommonComponent, CommonComponentParts},
},
};
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{anyhow, bail, Result};
use gloo_console::error;
use lldap_auth::*;
use validator_derive::Validate;
use yew::{prelude::*, services::ConsoleService};
use yew::prelude::*;
use yew_form::Form;
use yew_form_derive::Model;
use yew_router::{
agent::{RouteAgentDispatcher, RouteRequest},
route::Route,
};
use yew_router::{prelude::History, scope_ext::RouterScopeExt};
#[derive(PartialEq, Eq)]
#[derive(PartialEq, Eq, Default)]
enum OpaqueData {
#[default]
None,
Login(opaque::client::login::ClientLogin),
Registration(opaque::client::registration::ClientRegistration),
}
impl Default for OpaqueData {
fn default() -> Self {
OpaqueData::None
}
}
impl OpaqueData {
fn take(&mut self) -> Self {
std::mem::take(self)
@@ -36,7 +29,7 @@ impl OpaqueData {
}
/// The fields of the form, with the constraints.
#[derive(Model, Validate, PartialEq, Clone, Default)]
#[derive(Model, Validate, PartialEq, Eq, Clone, Default)]
pub struct FormModel {
#[validate(custom(
function = "empty_or_long",
@@ -61,10 +54,9 @@ pub struct ChangePasswordForm {
common: CommonComponentParts<Self>,
form: Form<FormModel>,
opaque_data: OpaqueData,
route_dispatcher: RouteAgentDispatcher,
}
#[derive(Clone, PartialEq, Properties)]
#[derive(Clone, PartialEq, Eq, Properties)]
pub struct Props {
pub username: String,
pub is_admin: bool,
@@ -80,15 +72,20 @@ pub enum Msg {
}
impl CommonComponent<ChangePasswordForm> for ChangePasswordForm {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
use anyhow::Context;
match msg {
Msg::FormUpdate => Ok(true),
Msg::Submit => {
if !self.form.validate() {
bail!("Check the form for errors");
}
if self.common.is_admin {
self.handle_msg(Msg::SubmitNewPassword)
if ctx.props().is_admin {
self.handle_msg(ctx, Msg::SubmitNewPassword)
} else {
let old_password = self.form.model().old_password;
if old_password.is_empty() {
@@ -100,14 +97,14 @@ impl CommonComponent<ChangePasswordForm> for ChangePasswordForm {
.context("Could not initialize login")?;
self.opaque_data = OpaqueData::Login(login_start_request.state);
let req = login::ClientLoginStartRequest {
username: self.common.username.clone(),
username: ctx.props().username.clone().into(),
login_start_request: login_start_request.message,
};
self.common.call_backend(
HostService::login_start,
req,
ctx,
HostService::login_start(req),
Msg::AuthenticationStartResponse,
)?;
);
Ok(true)
}
}
@@ -119,34 +116,33 @@ impl CommonComponent<ChangePasswordForm> for ChangePasswordForm {
|e| {
// Common error, we want to print a full error to the console but only a
// simple one to the user.
ConsoleService::error(&format!(
"Invalid username or password: {}",
e
));
error!(&format!("Invalid username or password: {}", e));
anyhow!("Invalid username or password")
},
)?;
}
_ => panic!("Unexpected data in opaque_data field"),
};
self.handle_msg(Msg::SubmitNewPassword)
self.handle_msg(ctx, Msg::SubmitNewPassword)
}
Msg::SubmitNewPassword => {
let mut rng = rand::rngs::OsRng;
let new_password = self.form.model().password;
let registration_start_request =
opaque::client::registration::start_registration(&new_password, &mut rng)
.context("Could not initiate password change")?;
let registration_start_request = opaque::client::registration::start_registration(
new_password.as_bytes(),
&mut rng,
)
.context("Could not initiate password change")?;
let req = registration::ClientRegistrationStartRequest {
username: self.common.username.clone(),
username: ctx.props().username.clone().into(),
registration_start_request: registration_start_request.message,
};
self.opaque_data = OpaqueData::Registration(registration_start_request.state);
self.common.call_backend(
HostService::register_start,
req,
ctx,
HostService::register_start(req),
Msg::RegistrationStartResponse,
)?;
);
Ok(true)
}
Msg::RegistrationStartResponse(res) => {
@@ -166,22 +162,20 @@ impl CommonComponent<ChangePasswordForm> for ChangePasswordForm {
registration_upload: registration_finish.message,
};
self.common.call_backend(
HostService::register_finish,
req,
ctx,
HostService::register_finish(req),
Msg::RegistrationFinishResponse,
)
);
}
_ => panic!("Unexpected data in opaque_data field"),
}?;
};
Ok(false)
}
Msg::RegistrationFinishResponse(response) => {
self.common.cancel_task();
if response.is_ok() {
self.route_dispatcher
.send(RouteRequest::ChangeRoute(Route::from(
AppRoute::UserDetails(self.common.username.clone()),
)));
ctx.link().history().unwrap().push(AppRoute::UserDetails {
user_id: ctx.props().username.clone(),
});
}
response?;
Ok(true)
@@ -198,28 +192,38 @@ impl Component for ChangePasswordForm {
type Message = Msg;
type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
fn create(_: &Context<Self>) -> Self {
ChangePasswordForm {
common: CommonComponentParts::<Self>::create(props, link),
common: CommonComponentParts::<Self>::create(),
form: yew_form::Form::<FormModel>::new(FormModel::default()),
opaque_data: OpaqueData::None,
route_dispatcher: RouteAgentDispatcher::new(),
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
CommonComponentParts::<Self>::update(self, msg)
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, ctx, msg)
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.common.change(props)
}
fn view(&self) -> Html {
let is_admin = self.common.is_admin;
fn view(&self, ctx: &Context<Self>) -> Html {
let is_admin = ctx.props().is_admin;
let link = ctx.link();
type Field = yew_form::Field<FormModel>;
html! {
<>
<div class="mb-2 mt-2">
<h5 class="fw-bold">
{"Change password"}
</h5>
</div>
{
if let Some(e) = &self.common.error {
html! {
<div class="alert alert-danger mt-3 mb-3">
{e.to_string() }
</div>
}
} else { html! {} }
}
<form
class="form">
{if !is_admin { html! {
@@ -230,82 +234,81 @@ impl Component for ChangePasswordForm {
</label>
<div class="col-sm-10">
<Field
form=&self.form
form={&self.form}
field_name="old_password"
input_type="password"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="current-password"
oninput=self.common.callback(|_| Msg::FormUpdate) />
oninput={link.callback(|_| Msg::FormUpdate)} />
<div class="invalid-feedback">
{&self.form.field_message("old_password")}
</div>
</div>
</div>
}} else { html! {} }}
<div class="form-group row">
<div class="form-group row mb-3">
<label for="new_password"
class="form-label col-sm-2 col-form-label">
{"New password*:"}
{"New Password"}
<span class="text-danger">{"*"}</span>
{":"}
</label>
<div class="col-sm-10">
<Field
form=&self.form
form={&self.form}
field_name="password"
input_type="password"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="new-password"
oninput=self.common.callback(|_| Msg::FormUpdate) />
oninput={link.callback(|_| Msg::FormUpdate)} />
<div class="invalid-feedback">
{&self.form.field_message("password")}
</div>
</div>
</div>
<div class="form-group row">
<div class="form-group row mb-3">
<label for="confirm_password"
class="form-label col-sm-2 col-form-label">
{"Confirm password*:"}
{"Confirm Password"}
<span class="text-danger">{"*"}</span>
{":"}
</label>
<div class="col-sm-10">
<Field
form=&self.form
form={&self.form}
field_name="confirm_password"
input_type="password"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="new-password"
oninput=self.common.callback(|_| Msg::FormUpdate) />
oninput={link.callback(|_| Msg::FormUpdate)} />
<div class="invalid-feedback">
{&self.form.field_message("confirm_password")}
</div>
</div>
</div>
<div class="form-group row">
<div class="form-group row justify-content-center">
<button
class="btn btn-primary col-sm-1 col-form-label"
class="btn btn-primary col-auto col-form-label"
type="submit"
disabled=self.common.is_task_running()
onclick=self.common.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})>
{"Submit"}
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})}>
<i class="bi-save me-2"></i>
{"Save changes"}
</button>
<Link
classes="btn btn-secondary ms-2 col-auto col-form-label"
to={AppRoute::UserDetails{user_id: ctx.props().username.clone()}}>
<i class="bi-arrow-return-left me-2"></i>
{"Back"}
</Link>
</div>
</form>
{ if let Some(e) = &self.common.error {
html! {
<div class="alert alert-danger">
{e.to_string() }
</div>
}
} else { html! {} }
}
<div>
<NavButton
classes="btn btn-primary"
route=AppRoute::UserDetails(self.common.username.clone())>
{"Back"}
</NavButton>
</div>
</>
}
}

View File

@@ -3,15 +3,12 @@ use crate::{
infra::common_component::{CommonComponent, CommonComponentParts},
};
use anyhow::{bail, Result};
use gloo_console::log;
use graphql_client::GraphQLQuery;
use validator_derive::Validate;
use yew::prelude::*;
use yew::services::ConsoleService;
use yew_form_derive::Model;
use yew_router::{
agent::{RouteAgentDispatcher, RouteRequest},
route::Route,
};
use yew_router::{prelude::History, scope_ext::RouterScopeExt};
#[derive(GraphQLQuery)]
#[graphql(
@@ -24,11 +21,10 @@ pub struct CreateGroup;
pub struct CreateGroupForm {
common: CommonComponentParts<Self>,
route_dispatcher: RouteAgentDispatcher,
form: yew_form::Form<CreateGroupModel>,
}
#[derive(Model, Validate, PartialEq, Clone, Default)]
#[derive(Model, Validate, PartialEq, Eq, Clone, Default)]
pub struct CreateGroupModel {
#[validate(length(min = 1, message = "Groupname is required"))]
groupname: String,
@@ -41,7 +37,11 @@ pub enum Msg {
}
impl CommonComponent<CreateGroupForm> for CreateGroupForm {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg {
Msg::Update => Ok(true),
Msg::SubmitForm => {
@@ -53,6 +53,7 @@ impl CommonComponent<CreateGroupForm> for CreateGroupForm {
name: model.groupname,
};
self.common.call_graphql::<CreateGroup, _>(
ctx,
req,
Msg::CreateGroupResponse,
"Error trying to create group",
@@ -60,12 +61,11 @@ impl CommonComponent<CreateGroupForm> for CreateGroupForm {
Ok(true)
}
Msg::CreateGroupResponse(response) => {
ConsoleService::log(&format!(
log!(&format!(
"Created group '{}'",
&response?.create_group.display_name
));
self.route_dispatcher
.send(RouteRequest::ChangeRoute(Route::from(AppRoute::ListGroups)));
ctx.link().history().unwrap().push(AppRoute::ListGroups);
Ok(true)
}
}
@@ -80,44 +80,42 @@ impl Component for CreateGroupForm {
type Message = Msg;
type Properties = ();
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
fn create(_: &Context<Self>) -> Self {
Self {
common: CommonComponentParts::<Self>::create(props, link),
route_dispatcher: RouteAgentDispatcher::new(),
common: CommonComponentParts::<Self>::create(),
form: yew_form::Form::<CreateGroupModel>::new(CreateGroupModel::default()),
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
CommonComponentParts::<Self>::update(self, msg)
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, ctx, msg)
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.common.change(props)
}
fn view(&self) -> Html {
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
type Field = yew_form::Field<CreateGroupModel>;
html! {
<div class="row justify-content-center">
<form class="form shadow-sm py-3" style="max-width: 636px">
<form class="form py-3" style="max-width: 636px">
<div class="row mb-3">
<h5 class="fw-bold">{"Create a group"}</h5>
</div>
<div class="form-group row mb-3">
<label for="groupname"
class="form-label col-4 col-form-label">
{"Group name*:"}
{"Group name"}
<span class="text-danger">{"*"}</span>
{":"}
</label>
<div class="col-8">
<Field
form=&self.form
form={&self.form}
field_name="groupname"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="groupname"
oninput=self.common.callback(|_| Msg::Update) />
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("groupname")}
</div>
@@ -127,8 +125,9 @@ impl Component for CreateGroupForm {
<button
class="btn btn-primary col-auto col-form-label"
type="submit"
disabled=self.common.is_task_running()
onclick=self.common.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})>
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})}>
<i class="bi-save me-2"></i>
{"Submit"}
</button>
</div>

View File

@@ -5,17 +5,14 @@ use crate::{
common_component::{CommonComponent, CommonComponentParts},
},
};
use anyhow::{bail, Context, Result};
use anyhow::{bail, Result};
use gloo_console::log;
use graphql_client::GraphQLQuery;
use lldap_auth::{opaque, registration};
use validator_derive::Validate;
use yew::prelude::*;
use yew::services::ConsoleService;
use yew_form_derive::Model;
use yew_router::{
agent::{RouteAgentDispatcher, RouteRequest},
route::Route,
};
use yew_router::{prelude::History, scope_ext::RouterScopeExt};
#[derive(GraphQLQuery)]
#[graphql(
@@ -28,17 +25,15 @@ pub struct CreateUser;
pub struct CreateUserForm {
common: CommonComponentParts<Self>,
route_dispatcher: RouteAgentDispatcher,
form: yew_form::Form<CreateUserModel>,
}
#[derive(Model, Validate, PartialEq, Clone, Default)]
#[derive(Model, Validate, PartialEq, Eq, Clone, Default)]
pub struct CreateUserModel {
#[validate(length(min = 1, message = "Username is required"))]
username: String,
#[validate(email(message = "A valid email is required"))]
email: String,
#[validate(length(min = 1, message = "Display name is required"))]
display_name: String,
first_name: String,
last_name: String,
@@ -74,7 +69,11 @@ pub enum Msg {
}
impl CommonComponent<CreateUserForm> for CreateUserForm {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg {
Msg::Update => Ok(true),
Msg::SubmitForm => {
@@ -90,9 +89,12 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
displayName: to_option(model.display_name),
firstName: to_option(model.first_name),
lastName: to_option(model.last_name),
avatar: None,
attributes: None,
},
};
self.common.call_graphql::<CreateUser, _>(
ctx,
req,
Msg::CreateUserResponse,
"Error trying to create user",
@@ -102,7 +104,7 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
Msg::CreateUserResponse(r) => {
match r {
Err(e) => return Err(e),
Ok(r) => ConsoleService::log(&format!(
Ok(r) => log!(&format!(
"Created user '{}' at '{}'",
&r.create_user.id, &r.create_user.creation_date
)),
@@ -116,18 +118,20 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
let opaque::client::registration::ClientRegistrationStartResult {
state,
message,
} = opaque::client::registration::start_registration(&password, &mut rng)?;
} = opaque::client::registration::start_registration(
password.as_bytes(),
&mut rng,
)?;
let req = registration::ClientRegistrationStartRequest {
username: user_id,
username: user_id.into(),
registration_start_request: message,
};
self.common
.call_backend(HostService::register_start, req, move |r| {
.call_backend(ctx, HostService::register_start(req), move |r| {
Msg::RegistrationStartResponse((state, r))
})
.context("Error trying to create user")?;
});
} else {
self.update(Msg::SuccessfulCreation);
self.update(ctx, Msg::SuccessfulCreation);
}
Ok(false)
}
@@ -143,22 +147,19 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
server_data: response.server_data,
registration_upload: registration_upload.message,
};
self.common
.call_backend(
HostService::register_finish,
req,
Msg::RegistrationFinishResponse,
)
.context("Error trying to register user")?;
self.common.call_backend(
ctx,
HostService::register_finish(req),
Msg::RegistrationFinishResponse,
);
Ok(false)
}
Msg::RegistrationFinishResponse(response) => {
response?;
self.handle_msg(Msg::SuccessfulCreation)
self.handle_msg(ctx, Msg::SuccessfulCreation)
}
Msg::SuccessfulCreation => {
self.route_dispatcher
.send(RouteRequest::ChangeRoute(Route::from(AppRoute::ListUsers)));
ctx.link().history().unwrap().push(AppRoute::ListUsers);
Ok(true)
}
}
@@ -173,44 +174,42 @@ impl Component for CreateUserForm {
type Message = Msg;
type Properties = ();
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
fn create(_: &Context<Self>) -> Self {
Self {
common: CommonComponentParts::<Self>::create(props, link),
route_dispatcher: RouteAgentDispatcher::new(),
common: CommonComponentParts::<Self>::create(),
form: yew_form::Form::<CreateUserModel>::new(CreateUserModel::default()),
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
CommonComponentParts::<Self>::update(self, msg)
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, ctx, msg)
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.common.change(props)
}
fn view(&self) -> Html {
fn view(&self, ctx: &Context<Self>) -> Html {
let link = &ctx.link();
type Field = yew_form::Field<CreateUserModel>;
html! {
<div class="row justify-content-center">
<form class="form shadow-sm py-3" style="max-width: 636px">
<form class="form py-3" style="max-width: 636px">
<div class="row mb-3">
<h5 class="fw-bold">{"Create a user"}</h5>
</div>
<div class="form-group row mb-3">
<label for="username"
class="form-label col-4 col-form-label">
{"User name*:"}
{"User name"}
<span class="text-danger">{"*"}</span>
{":"}
</label>
<div class="col-8">
<Field
form=&self.form
form={&self.form}
field_name="username"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="username"
oninput=self.common.callback(|_| Msg::Update) />
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("username")}
</div>
@@ -219,75 +218,77 @@ impl Component for CreateUserForm {
<div class="form-group row mb-3">
<label for="email"
class="form-label col-4 col-form-label">
{"Email*:"}
{"Email"}
<span class="text-danger">{"*"}</span>
{":"}
</label>
<div class="col-8">
<Field
form=&self.form
form={&self.form}
input_type="email"
field_name="email"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="email"
oninput=self.common.callback(|_| Msg::Update) />
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("email")}
</div>
</div>
</div>
<div class="form-group row mb-3">
<label for="display-name"
<label for="display_name"
class="form-label col-4 col-form-label">
{"Display name*:"}
{"Display name:"}
</label>
<div class="col-8">
<Field
form=&self.form
form={&self.form}
autocomplete="name"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
field_name="display_name"
oninput=self.common.callback(|_| Msg::Update) />
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("display_name")}
</div>
</div>
</div>
<div class="form-group row mb-3">
<label for="first-name"
<label for="first_name"
class="form-label col-4 col-form-label">
{"First name:"}
</label>
<div class="col-8">
<Field
form=&self.form
form={&self.form}
autocomplete="given-name"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
field_name="first_name"
oninput=self.common.callback(|_| Msg::Update) />
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("first_name")}
</div>
</div>
</div>
<div class="form-group row mb-3">
<label for="last-name"
<label for="last_name"
class="form-label col-4 col-form-label">
{"Last name:"}
</label>
<div class="col-8">
<Field
form=&self.form
form={&self.form}
autocomplete="family-name"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
field_name="last_name"
oninput=self.common.callback(|_| Msg::Update) />
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("last_name")}
</div>
@@ -300,14 +301,14 @@ impl Component for CreateUserForm {
</label>
<div class="col-8">
<Field
form=&self.form
form={&self.form}
input_type="password"
field_name="password"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="new-password"
oninput=self.common.callback(|_| Msg::Update) />
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("password")}
</div>
@@ -320,14 +321,14 @@ impl Component for CreateUserForm {
</label>
<div class="col-8">
<Field
form=&self.form
form={&self.form}
input_type="password"
field_name="confirm_password"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="new-password"
oninput=self.common.callback(|_| Msg::Update) />
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("confirm_password")}
</div>
@@ -336,14 +337,16 @@ impl Component for CreateUserForm {
<div class="form-group row justify-content-center">
<button
class="btn btn-primary col-auto col-form-label mt-4"
disabled=self.common.is_task_running()
disabled={self.common.is_task_running()}
type="submit"
onclick=self.common.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})>
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})}>
<i class="bi-save me-2"></i>
{"Submit"}
</button>
</div>
</form>
{ if let Some(e) = &self.common.error {
{
if let Some(e) = &self.common.error {
html! {
<div class="alert alert-danger">
{e.to_string() }

View File

@@ -39,16 +39,21 @@ pub enum Msg {
}
impl CommonComponent<DeleteGroup> for DeleteGroup {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg {
Msg::ClickedDeleteGroup => {
self.modal.as_ref().expect("modal not initialized").show();
}
Msg::ConfirmDeleteGroup => {
self.update(Msg::DismissModal);
self.update(ctx, Msg::DismissModal);
self.common.call_graphql::<DeleteGroupQuery, _>(
ctx,
delete_group_query::Variables {
group_id: self.common.group.id,
group_id: ctx.props().group.id,
},
Msg::DeleteGroupResponse,
"Error trying to delete group",
@@ -58,12 +63,8 @@ impl CommonComponent<DeleteGroup> for DeleteGroup {
self.modal.as_ref().expect("modal not initialized").hide();
}
Msg::DeleteGroupResponse(response) => {
self.common.cancel_task();
response?;
self.common
.props
.on_group_deleted
.emit(self.common.group.id);
ctx.props().on_group_deleted.emit(ctx.props().group.id);
}
}
Ok(true)
@@ -78,15 +79,15 @@ impl Component for DeleteGroup {
type Message = Msg;
type Properties = DeleteGroupProps;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
fn create(_: &Context<Self>) -> Self {
Self {
common: CommonComponentParts::<Self>::create(props, link),
common: CommonComponentParts::<Self>::create(),
node_ref: NodeRef::default(),
modal: None,
}
}
fn rendered(&mut self, first_render: bool) {
fn rendered(&mut self, _: &Context<Self>, first_render: bool) {
if first_render {
self.modal = Some(Modal::new(
self.node_ref
@@ -96,43 +97,42 @@ impl Component for DeleteGroup {
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update_and_report_error(
self,
ctx,
msg,
self.common.on_error.clone(),
ctx.props().on_error.clone(),
)
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.common.change(props)
}
fn view(&self) -> Html {
fn view(&self, ctx: &Context<Self>) -> Html {
let link = &ctx.link();
html! {
<>
<button
class="btn btn-danger"
disabled=self.common.is_task_running()
onclick=self.common.callback(|_| Msg::ClickedDeleteGroup)>
disabled={self.common.is_task_running()}
onclick={link.callback(|_| Msg::ClickedDeleteGroup)}>
<i class="bi-x-circle-fill" aria-label="Delete group" />
</button>
{self.show_modal()}
{self.show_modal(ctx)}
</>
}
}
}
impl DeleteGroup {
fn show_modal(&self) -> Html {
fn show_modal(&self, ctx: &Context<Self>) -> Html {
let link = &ctx.link();
html! {
<div
class="modal fade"
id="deleteGroupModal".to_string() + &self.common.group.id.to_string()
id={"deleteGroupModal".to_string() + &ctx.props().group.id.to_string()}
tabindex="-1"
aria-labelledby="deleteGroupModalLabel"
aria-hidden="true"
ref=self.node_ref.clone()>
ref={self.node_ref.clone()}>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@@ -141,25 +141,29 @@ impl DeleteGroup {
type="button"
class="btn-close"
aria-label="Close"
onclick=self.common.callback(|_| Msg::DismissModal) />
onclick={link.callback(|_| Msg::DismissModal)} />
</div>
<div class="modal-body">
<span>
{"Are you sure you want to delete group "}
<b>{&self.common.group.display_name}</b>{"?"}
<b>{&ctx.props().group.display_name}</b>{"?"}
</span>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
onclick=self.common.callback(|_| Msg::DismissModal)>
onclick={link.callback(|_| Msg::DismissModal)}>
<i class="bi-x-circle me-2"></i>
{"Cancel"}
</button>
<button
type="button"
onclick=self.common.callback(|_| Msg::ConfirmDeleteGroup)
class="btn btn-danger">{"Yes, I'm sure"}</button>
onclick={link.callback(|_| Msg::ConfirmDeleteGroup)}
class="btn btn-danger">
<i class="bi-check-circle me-2"></i>
{"Yes, I'm sure"}
</button>
</div>
</div>
</div>

View File

@@ -36,16 +36,21 @@ pub enum Msg {
}
impl CommonComponent<DeleteUser> for DeleteUser {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg {
Msg::ClickedDeleteUser => {
self.modal.as_ref().expect("modal not initialized").show();
}
Msg::ConfirmDeleteUser => {
self.update(Msg::DismissModal);
self.update(ctx, Msg::DismissModal);
self.common.call_graphql::<DeleteUserQuery, _>(
ctx,
delete_user_query::Variables {
user: self.common.username.clone(),
user: ctx.props().username.clone(),
},
Msg::DeleteUserResponse,
"Error trying to delete user",
@@ -55,12 +60,10 @@ impl CommonComponent<DeleteUser> for DeleteUser {
self.modal.as_ref().expect("modal not initialized").hide();
}
Msg::DeleteUserResponse(response) => {
self.common.cancel_task();
response?;
self.common
.props
ctx.props()
.on_user_deleted
.emit(self.common.username.clone());
.emit(ctx.props().username.clone());
}
}
Ok(true)
@@ -75,15 +78,15 @@ impl Component for DeleteUser {
type Message = Msg;
type Properties = DeleteUserProps;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
fn create(_: &Context<Self>) -> Self {
Self {
common: CommonComponentParts::<Self>::create(props, link),
common: CommonComponentParts::<Self>::create(),
node_ref: NodeRef::default(),
modal: None,
}
}
fn rendered(&mut self, first_render: bool) {
fn rendered(&mut self, _: &Context<Self>, first_render: bool) {
if first_render {
self.modal = Some(Modal::new(
self.node_ref
@@ -93,44 +96,43 @@ impl Component for DeleteUser {
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update_and_report_error(
self,
ctx,
msg,
self.common.on_error.clone(),
ctx.props().on_error.clone(),
)
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.common.change(props)
}
fn view(&self) -> Html {
fn view(&self, ctx: &Context<Self>) -> Html {
let link = &ctx.link();
html! {
<>
<button
class="btn btn-danger"
disabled=self.common.is_task_running()
onclick=self.common.callback(|_| Msg::ClickedDeleteUser)>
disabled={self.common.is_task_running()}
onclick={link.callback(|_| Msg::ClickedDeleteUser)}>
<i class="bi-x-circle-fill" aria-label="Delete user" />
</button>
{self.show_modal()}
{self.show_modal(ctx)}
</>
}
}
}
impl DeleteUser {
fn show_modal(&self) -> Html {
fn show_modal(&self, ctx: &Context<Self>) -> Html {
let link = &ctx.link();
html! {
<div
class="modal fade"
id="deleteUserModal".to_string() + &self.common.username
id={"deleteUserModal".to_string() + &ctx.props().username}
tabindex="-1"
//role="dialog"
aria-labelledby="deleteUserModalLabel"
aria-hidden="true"
ref=self.node_ref.clone()>
ref={self.node_ref.clone()}>
<div class="modal-dialog" /*role="document"*/>
<div class="modal-content">
<div class="modal-header">
@@ -139,25 +141,29 @@ impl DeleteUser {
type="button"
class="btn-close"
aria-label="Close"
onclick=self.common.callback(|_| Msg::DismissModal) />
onclick={link.callback(|_| Msg::DismissModal)} />
</div>
<div class="modal-body">
<span>
{"Are you sure you want to delete user "}
<b>{&self.common.username}</b>{"?"}
<b>{&ctx.props().username}</b>{"?"}
</span>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
onclick=self.common.callback(|_| Msg::DismissModal)>
{"Cancel"}
onclick={link.callback(|_| Msg::DismissModal)}>
<i class="bi-x-circle me-2"></i>
{"Cancel"}
</button>
<button
type="button"
onclick=self.common.callback(|_| Msg::ConfirmDeleteUser)
class="btn btn-danger">{"Yes, I'm sure"}</button>
onclick={link.callback(|_| Msg::ConfirmDeleteUser)}
class="btn btn-danger">
<i class="bi-check-circle me-2"></i>
{"Yes, I'm sure"}
</button>
</div>
</div>
</div>

View File

@@ -40,16 +40,17 @@ pub enum Msg {
OnUserRemovedFromGroup((String, i64)),
}
#[derive(yew::Properties, Clone, PartialEq)]
#[derive(yew::Properties, Clone, PartialEq, Eq)]
pub struct Props {
pub group_id: i64,
}
impl GroupDetails {
fn get_group_details(&mut self) {
fn get_group_details(&mut self, ctx: &Context<Self>) {
self.common.call_graphql::<GetGroupDetails, _>(
ctx,
get_group_details::Variables {
id: self.common.group_id,
id: ctx.props().group_id,
},
Msg::GroupDetailsResponse,
"Error trying to fetch group details",
@@ -68,34 +69,73 @@ impl GroupDetails {
}
}
fn view_user_list(&self, g: &Group) -> Html {
fn view_details(&self, g: &Group) -> Html {
html! {
<>
<h3>{g.display_name.to_string()}</h3>
<div class="py-3">
<form class="form">
<div class="form-group row mb-3">
<label for="displayName"
class="form-label col-4 col-form-label">
{"Group: "}
</label>
<div class="col-8">
<span id="groupId" class="form-constrol-static">{g.display_name.to_string()}</span>
</div>
</div>
<div class="form-group row mb-3">
<label for="creationDate"
class="form-label col-4 col-form-label">
{"Creation date: "}
</label>
<div class="col-8">
<span id="creationDate" class="form-constrol-static">{g.creation_date.naive_local().date()}</span>
</div>
</div>
<div class="form-group row mb-3">
<label for="uuid"
class="form-label col-4 col-form-label">
{"UUID: "}
</label>
<div class="col-8">
<span id="uuid" class="form-constrol-static">{g.uuid.to_string()}</span>
</div>
</div>
</form>
</div>
</>
}
}
fn view_user_list(&self, ctx: &Context<Self>, g: &Group) -> Html {
let link = ctx.link();
let make_user_row = |user: &User| {
let user_id = user.id.clone();
let display_name = user.display_name.clone();
html! {
<tr>
<td>
<Link route=AppRoute::UserDetails(user_id.clone())>
<Link to={AppRoute::UserDetails{user_id: user_id.clone()}}>
{user_id.clone()}
</Link>
</td>
<td>{display_name}</td>
<td>
<RemoveUserFromGroupComponent
username=user_id
group_id=g.id
on_user_removed_from_group=self.common.callback(Msg::OnUserRemovedFromGroup)
on_error=self.common.callback(Msg::OnError)/>
username={user_id}
group_id={g.id}
on_user_removed_from_group={link.callback(Msg::OnUserRemovedFromGroup)}
on_error={link.callback(Msg::OnError)}/>
</td>
</tr>
}
};
html! {
<>
<h3>{g.display_name.to_string()}</h3>
<h5 class="fw-bold">{"Members"}</h5>
<div class="table-responsive">
<table class="table table-striped">
<table class="table table-hover">
<thead>
<tr key="headerRow">
<th>{"User Id"}</th>
@@ -107,7 +147,7 @@ impl GroupDetails {
{if g.users.is_empty() {
html! {
<tr key="EmptyRow">
<td>{"No members"}</td>
<td>{"There are no users in this group."}</td>
<td/>
</tr>
}
@@ -121,7 +161,8 @@ impl GroupDetails {
}
}
fn view_add_user_button(&self, g: &Group) -> Html {
fn view_add_user_button(&self, ctx: &Context<Self>, g: &Group) -> Html {
let link = ctx.link();
let users: Vec<_> = g
.users
.iter()
@@ -132,16 +173,16 @@ impl GroupDetails {
.collect();
html! {
<AddGroupMemberComponent
group_id=g.id
users=users
on_error=self.common.callback(Msg::OnError)
on_user_added_to_group=self.common.callback(Msg::OnUserAddedToGroup)/>
group_id={g.id}
users={users}
on_error={link.callback(Msg::OnError)}
on_user_added_to_group={link.callback(Msg::OnUserAddedToGroup)}/>
}
}
}
impl CommonComponent<GroupDetails> for GroupDetails {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
fn handle_msg(&mut self, _: &Context<Self>, msg: <Self as Component>::Message) -> Result<bool> {
match msg {
Msg::GroupDetailsResponse(response) => match response {
Ok(group) => self.group = Some(group.group),
@@ -177,32 +218,29 @@ impl Component for GroupDetails {
type Message = Msg;
type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
fn create(ctx: &Context<Self>) -> Self {
let mut table = Self {
common: CommonComponentParts::<Self>::create(props, link),
common: CommonComponentParts::<Self>::create(),
group: None,
};
table.get_group_details();
table.get_group_details(ctx);
table
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
CommonComponentParts::<Self>::update(self, msg)
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, ctx, msg)
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.common.change(props)
}
fn view(&self) -> Html {
fn view(&self, ctx: &Context<Self>) -> Html {
match (&self.group, &self.common.error) {
(None, None) => html! {{"Loading..."}},
(None, Some(e)) => html! {<div>{"Error: "}{e.to_string()}</div>},
(Some(u), error) => {
html! {
<div>
{self.view_user_list(u)}
{self.view_add_user_button(u)}
{self.view_details(u)}
{self.view_user_list(ctx, u)}
{self.view_add_user_button(ctx, u)}
{self.view_messages(error)}
</div>
}

View File

@@ -13,7 +13,7 @@ use yew::prelude::*;
#[graphql(
schema_path = "../schema.graphql",
query_path = "queries/get_group_list.graphql",
response_derives = "Debug,Clone,PartialEq",
response_derives = "Debug,Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql"
)]
pub struct GetGroupList;
@@ -34,7 +34,7 @@ pub enum Msg {
}
impl CommonComponent<GroupTable> for GroupTable {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
fn handle_msg(&mut self, _: &Context<Self>, msg: <Self as Component>::Message) -> Result<bool> {
match msg {
Msg::ListGroupsResponse(groups) => {
self.groups = Some(groups?.groups.into_iter().collect());
@@ -58,12 +58,13 @@ impl Component for GroupTable {
type Message = Msg;
type Properties = ();
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
fn create(ctx: &Context<Self>) -> Self {
let mut table = GroupTable {
common: CommonComponentParts::<Self>::create(props, link),
common: CommonComponentParts::<Self>::create(),
groups: None,
};
table.common.call_graphql::<GetGroupList, _>(
ctx,
get_group_list::Variables {},
Msg::ListGroupsResponse,
"Error trying to fetch groups",
@@ -71,18 +72,14 @@ impl Component for GroupTable {
table
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
CommonComponentParts::<Self>::update(self, msg)
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, ctx, msg)
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.common.change(props)
}
fn view(&self) -> Html {
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<div>
{self.view_groups()}
{self.view_groups(ctx)}
{self.view_errors()}
</div>
}
@@ -90,19 +87,20 @@ impl Component for GroupTable {
}
impl GroupTable {
fn view_groups(&self) -> Html {
fn view_groups(&self, ctx: &Context<Self>) -> Html {
let make_table = |groups: &Vec<Group>| {
html! {
<div class="table-responsive">
<table class="table table-striped">
<table class="table table-hover">
<thead>
<tr>
<th>{"Groups"}</th>
<th>{"Group name"}</th>
<th>{"Creation date"}</th>
<th>{"Delete"}</th>
</tr>
</thead>
<tbody>
{groups.iter().map(|u| self.view_group(u)).collect::<Vec<_>>()}
{groups.iter().map(|u| self.view_group(ctx, u)).collect::<Vec<_>>()}
</tbody>
</table>
</div>
@@ -114,19 +112,23 @@ impl GroupTable {
}
}
fn view_group(&self, group: &Group) -> Html {
fn view_group(&self, ctx: &Context<Self>, group: &Group) -> Html {
let link = ctx.link();
html! {
<tr key=group.id>
<tr key={group.id}>
<td>
<Link route=AppRoute::GroupDetails(group.id)>
<Link to={AppRoute::GroupDetails{group_id: group.id}}>
{&group.display_name}
</Link>
</td>
<td>
{&group.creation_date.naive_local().date()}
</td>
<td>
<DeleteGroup
group=group.clone()
on_group_deleted=self.common.callback(Msg::OnGroupDeleted)
on_error=self.common.callback(Msg::OnError)/>
group={group.clone()}
on_group_deleted={link.callback(Msg::OnGroupDeleted)}
on_error={link.callback(Msg::OnError)}/>
</td>
</tr>
}

View File

@@ -1,14 +1,15 @@
use crate::{
components::router::{AppRoute, NavButton},
components::router::{AppRoute, Link},
infra::{
api::HostService,
common_component::{CommonComponent, CommonComponentParts},
},
};
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{anyhow, bail, Result};
use gloo_console::error;
use lldap_auth::*;
use validator_derive::Validate;
use yew::{prelude::*, services::ConsoleService};
use yew::prelude::*;
use yew_form::Form;
use yew_form_derive::Model;
@@ -19,7 +20,7 @@ pub struct LoginForm {
}
/// The fields of the form, with the constraints.
#[derive(Model, Validate, PartialEq, Clone, Default)]
#[derive(Model, Validate, PartialEq, Eq, Clone, Default)]
pub struct FormModel {
#[validate(length(min = 1, message = "Missing username"))]
username: String,
@@ -30,6 +31,7 @@ pub struct FormModel {
#[derive(Clone, PartialEq, Properties)]
pub struct Props {
pub on_logged_in: Callback<(String, bool)>,
pub password_reset_enabled: bool,
}
pub enum Msg {
@@ -46,7 +48,12 @@ pub enum Msg {
}
impl CommonComponent<LoginForm> for LoginForm {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
use anyhow::Context;
match msg {
Msg::Update => Ok(true),
Msg::Submit => {
@@ -59,13 +66,13 @@ impl CommonComponent<LoginForm> for LoginForm {
opaque::client::login::start_login(&password, &mut rng)
.context("Could not initialize login")?;
let req = login::ClientLoginStartRequest {
username,
username: username.into(),
login_start_request: message,
};
self.common
.call_backend(HostService::login_start, req, move |r| {
.call_backend(ctx, HostService::login_start(req), move |r| {
Msg::AuthenticationStartResponse((state, r))
})?;
});
Ok(true)
}
Msg::AuthenticationStartResponse((login_start, res)) => {
@@ -76,9 +83,8 @@ impl CommonComponent<LoginForm> for LoginForm {
Err(e) => {
// Common error, we want to print a full error to the console but only a
// simple one to the user.
ConsoleService::error(&format!("Invalid username or password: {}", e));
error!(&format!("Invalid username or password: {}", e));
self.common.error = Some(anyhow!("Invalid username or password"));
self.common.cancel_task();
return Ok(true);
}
Ok(l) => l,
@@ -88,24 +94,22 @@ impl CommonComponent<LoginForm> for LoginForm {
credential_finalization: login_finish.message,
};
self.common.call_backend(
HostService::login_finish,
req,
ctx,
HostService::login_finish(req),
Msg::AuthenticationFinishResponse,
)?;
);
Ok(false)
}
Msg::AuthenticationFinishResponse(user_info) => {
self.common.cancel_task();
self.common
ctx.props()
.on_logged_in
.emit(user_info.context("Could not log in")?);
Ok(true)
}
Msg::AuthenticationRefreshResponse(user_info) => {
self.refreshing = false;
self.common.cancel_task();
if let Ok(user_info) = user_info {
self.common.on_logged_in.emit(user_info);
ctx.props().on_logged_in.emit(user_info);
}
Ok(true)
}
@@ -121,32 +125,28 @@ impl Component for LoginForm {
type Message = Msg;
type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
fn create(ctx: &Context<Self>) -> Self {
let mut app = LoginForm {
common: CommonComponentParts::<Self>::create(props, link),
common: CommonComponentParts::<Self>::create(),
form: Form::<FormModel>::new(FormModel::default()),
refreshing: true,
};
if let Err(e) =
app.common
.call_backend(HostService::refresh, (), Msg::AuthenticationRefreshResponse)
{
ConsoleService::debug(&format!("Could not refresh auth: {}", e));
app.refreshing = false;
}
app.common.call_backend(
ctx,
HostService::refresh(),
Msg::AuthenticationRefreshResponse,
);
app
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
CommonComponentParts::<Self>::update(self, msg)
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, ctx, msg)
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.common.change(props)
}
fn view(&self) -> Html {
fn view(&self, ctx: &Context<Self>) -> Html {
type Field = yew_form::Field<FormModel>;
let password_reset_enabled = ctx.props().password_reset_enabled;
let link = &ctx.link();
if self.refreshing {
html! {
<div>
@@ -167,11 +167,11 @@ impl Component for LoginForm {
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
form=&self.form
form={&self.form}
field_name="username"
placeholder="Username"
autocomplete="username"
oninput=self.common.callback(|_| Msg::Update) />
oninput={link.callback(|_| Msg::Update)} />
</div>
<div class="input-group">
<div class="input-group-prepend">
@@ -183,7 +183,7 @@ impl Component for LoginForm {
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
form=&self.form
form={&self.form}
field_name="password"
input_type="password"
placeholder="Password"
@@ -193,16 +193,23 @@ impl Component for LoginForm {
<button
type="submit"
class="btn btn-primary"
disabled=self.common.is_task_running()
onclick=self.common.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})>
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})}>
<i class="bi-box-arrow-in-right me-2"/>
{"Login"}
</button>
<NavButton
classes="btn-link btn"
disabled=self.common.is_task_running()
route=AppRoute::StartResetPassword>
{"Forgot your password?"}
</NavButton>
{ if password_reset_enabled {
html! {
<Link
classes="btn-link btn"
disabled={self.common.is_task_running()}
to={AppRoute::StartResetPassword}>
{"Forgot your password?"}
</Link>
}
} else {
html!{}
}}
</div>
<div class="form-group">
{ if let Some(e) = &self.common.error {

View File

@@ -21,16 +21,20 @@ pub enum Msg {
}
impl CommonComponent<LogoutButton> for LogoutButton {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg {
Msg::LogoutRequested => {
self.common
.call_backend(HostService::logout, (), Msg::LogoutCompleted)?;
.call_backend(ctx, HostService::logout(), Msg::LogoutCompleted);
}
Msg::LogoutCompleted(res) => {
res?;
delete_cookie("user_id")?;
self.common.on_logged_out.emit(());
ctx.props().on_logged_out.emit(());
}
}
Ok(false)
@@ -45,25 +49,22 @@ impl Component for LogoutButton {
type Message = Msg;
type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
fn create(_: &Context<Self>) -> Self {
LogoutButton {
common: CommonComponentParts::<Self>::create(props, link),
common: CommonComponentParts::<Self>::create(),
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
CommonComponentParts::<Self>::update(self, msg)
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, ctx, msg)
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.common.change(props)
}
fn view(&self) -> Html {
fn view(&self, ctx: &Context<Self>) -> Html {
let link = &ctx.link();
html! {
<button
class="dropdown-item"
onclick=self.common.callback(|_| Msg::LogoutRequested)>
onclick={link.callback(|_| Msg::LogoutRequested)}>
{"Logout"}
</button>
}

View File

@@ -31,15 +31,18 @@ pub enum Msg {
}
impl CommonComponent<RemoveUserFromGroupComponent> for RemoveUserFromGroupComponent {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg {
Msg::SubmitRemoveGroup => self.submit_remove_group(),
Msg::SubmitRemoveGroup => self.submit_remove_group(ctx),
Msg::RemoveGroupResponse(response) => {
response?;
self.common.cancel_task();
self.common
ctx.props()
.on_user_removed_from_group
.emit((self.common.username.clone(), self.common.group_id));
.emit((ctx.props().username.clone(), ctx.props().group_id));
}
}
Ok(true)
@@ -51,11 +54,12 @@ impl CommonComponent<RemoveUserFromGroupComponent> for RemoveUserFromGroupCompon
}
impl RemoveUserFromGroupComponent {
fn submit_remove_group(&mut self) {
fn submit_remove_group(&mut self, ctx: &Context<Self>) {
self.common.call_graphql::<RemoveUserFromGroup, _>(
ctx,
remove_user_from_group::Variables {
user: self.common.username.clone(),
group: self.common.group_id,
user: ctx.props().username.clone(),
group: ctx.props().group_id,
},
Msg::RemoveGroupResponse,
"Error trying to initiate removing the user from a group",
@@ -67,30 +71,28 @@ impl Component for RemoveUserFromGroupComponent {
type Message = Msg;
type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
fn create(_: &Context<Self>) -> Self {
Self {
common: CommonComponentParts::<Self>::create(props, link),
common: CommonComponentParts::<Self>::create(),
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update_and_report_error(
self,
ctx,
msg,
self.common.on_error.clone(),
ctx.props().on_error.clone(),
)
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.common.change(props)
}
fn view(&self) -> Html {
fn view(&self, ctx: &Context<Self>) -> Html {
let link = &ctx.link();
html! {
<button
class="btn btn-danger"
disabled=self.common.is_task_running()
onclick=self.common.callback(|_| Msg::SubmitRemoveGroup)>
disabled={self.common.is_task_running()}
onclick={link.callback(|_| Msg::SubmitRemoveGroup)}>
<i class="bi-x-circle-fill" aria-label="Remove user from group" />
</button>
}

View File

@@ -1,5 +1,5 @@
use crate::{
components::router::{AppRoute, NavButton},
components::router::{AppRoute, Link},
infra::{
api::HostService,
common_component::{CommonComponent, CommonComponentParts},
@@ -18,7 +18,7 @@ pub struct ResetPasswordStep1Form {
}
/// The fields of the form, with the constraints.
#[derive(Model, Validate, PartialEq, Clone, Default)]
#[derive(Model, Validate, PartialEq, Eq, Clone, Default)]
pub struct FormModel {
#[validate(length(min = 1, message = "Missing username"))]
username: String,
@@ -31,7 +31,11 @@ pub enum Msg {
}
impl CommonComponent<ResetPasswordStep1Form> for ResetPasswordStep1Form {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg {
Msg::Update => Ok(true),
Msg::Submit => {
@@ -40,10 +44,10 @@ impl CommonComponent<ResetPasswordStep1Form> for ResetPasswordStep1Form {
}
let FormModel { username } = self.form.model();
self.common.call_backend(
HostService::reset_password_step1,
&username,
ctx,
HostService::reset_password_step1(username),
Msg::PasswordResetResponse,
)?;
);
Ok(true)
}
Msg::PasswordResetResponse(response) => {
@@ -63,25 +67,22 @@ impl Component for ResetPasswordStep1Form {
type Message = Msg;
type Properties = ();
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
fn create(_: &Context<Self>) -> Self {
ResetPasswordStep1Form {
common: CommonComponentParts::<Self>::create(props, link),
common: CommonComponentParts::<Self>::create(),
form: Form::<FormModel>::new(FormModel::default()),
just_succeeded: false,
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
self.just_succeeded = false;
CommonComponentParts::<Self>::update(self, msg)
CommonComponentParts::<Self>::update(self, ctx, msg)
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.common.change(props)
}
fn view(&self) -> Html {
fn view(&self, ctx: &Context<Self>) -> Html {
type Field = yew_form::Field<FormModel>;
let link = &ctx.link();
html! {
<form
class="form center-block col-sm-4 col-offset-4">
@@ -95,11 +96,11 @@ impl Component for ResetPasswordStep1Form {
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
form=&self.form
form={&self.form}
field_name="username"
placeholder="Username"
placeholder="Username or email"
autocomplete="username"
oninput=self.common.callback(|_| Msg::Update) />
oninput={link.callback(|_| Msg::Update)} />
</div>
{ if self.just_succeeded {
html! {
@@ -111,23 +112,24 @@ impl Component for ResetPasswordStep1Form {
<button
type="submit"
class="btn btn-primary"
disabled=self.common.is_task_running()
onclick=self.common.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})>
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})}>
<i class="bi-check-circle me-2"/>
{"Reset password"}
</button>
<NavButton
<Link
classes="btn-link btn"
disabled=self.common.is_task_running()
route=AppRoute::Login>
disabled={self.common.is_task_running()}
to={AppRoute::Login}>
{"Back"}
</NavButton>
</Link>
</div>
}
}}
<div class="form-group">
{ if let Some(e) = &self.common.error {
html! {
<div class="alert alert-danger">
<div class="alert alert-danger mb-2">
{e.to_string() }
</div>
}

View File

@@ -1,11 +1,11 @@
use crate::{
components::router::AppRoute,
components::router::{AppRoute, Link},
infra::{
api::HostService,
common_component::{CommonComponent, CommonComponentParts},
},
};
use anyhow::{bail, Context, Result};
use anyhow::{bail, Result};
use lldap_auth::{
opaque::client::registration as opaque_registration,
password_reset::ServerPasswordResetResponse, registration,
@@ -14,13 +14,10 @@ use validator_derive::Validate;
use yew::prelude::*;
use yew_form::Form;
use yew_form_derive::Model;
use yew_router::{
agent::{RouteAgentDispatcher, RouteRequest},
route::Route,
};
use yew_router::{prelude::History, scope_ext::RouterScopeExt};
/// The fields of the form, with the constraints.
#[derive(Model, Validate, PartialEq, Clone, Default)]
#[derive(Model, Validate, PartialEq, Eq, Clone, Default)]
pub struct FormModel {
#[validate(length(min = 8, message = "Invalid password. Min length: 8"))]
password: String,
@@ -33,10 +30,9 @@ pub struct ResetPasswordStep2Form {
form: Form<FormModel>,
username: Option<String>,
opaque_data: Option<opaque_registration::ClientRegistration>,
route_dispatcher: RouteAgentDispatcher,
}
#[derive(Clone, PartialEq, Properties)]
#[derive(Clone, PartialEq, Eq, Properties)]
pub struct Props {
pub token: String,
}
@@ -50,11 +46,15 @@ pub enum Msg {
}
impl CommonComponent<ResetPasswordStep2Form> for ResetPasswordStep2Form {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
use anyhow::Context;
match msg {
Msg::ValidateTokenResponse(response) => {
self.username = Some(response?.user_id);
self.common.cancel_task();
Ok(true)
}
Msg::FormUpdate => Ok(true),
@@ -65,18 +65,18 @@ impl CommonComponent<ResetPasswordStep2Form> for ResetPasswordStep2Form {
let mut rng = rand::rngs::OsRng;
let new_password = self.form.model().password;
let registration_start_request =
opaque_registration::start_registration(&new_password, &mut rng)
opaque_registration::start_registration(new_password.as_bytes(), &mut rng)
.context("Could not initiate password change")?;
let req = registration::ClientRegistrationStartRequest {
username: self.username.clone().unwrap(),
username: self.username.as_ref().unwrap().into(),
registration_start_request: registration_start_request.message,
};
self.opaque_data = Some(registration_start_request.state);
self.common.call_backend(
HostService::register_start,
req,
ctx,
HostService::register_start(req),
Msg::RegistrationStartResponse,
)?;
);
Ok(true)
}
Msg::RegistrationStartResponse(res) => {
@@ -94,17 +94,15 @@ impl CommonComponent<ResetPasswordStep2Form> for ResetPasswordStep2Form {
registration_upload: registration_finish.message,
};
self.common.call_backend(
HostService::register_finish,
req,
ctx,
HostService::register_finish(req),
Msg::RegistrationFinishResponse,
)?;
);
Ok(false)
}
Msg::RegistrationFinishResponse(response) => {
self.common.cancel_task();
if response.is_ok() {
self.route_dispatcher
.send(RouteRequest::ChangeRoute(Route::from(AppRoute::Login)));
ctx.link().history().unwrap().push(AppRoute::Login);
}
response?;
Ok(true)
@@ -121,35 +119,28 @@ impl Component for ResetPasswordStep2Form {
type Message = Msg;
type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
fn create(ctx: &Context<Self>) -> Self {
let mut component = ResetPasswordStep2Form {
common: CommonComponentParts::<Self>::create(props, link),
common: CommonComponentParts::<Self>::create(),
form: yew_form::Form::<FormModel>::new(FormModel::default()),
opaque_data: None,
route_dispatcher: RouteAgentDispatcher::new(),
username: None,
};
let token = component.common.token.clone();
component
.common
.call_backend(
HostService::reset_password_step2,
&token,
Msg::ValidateTokenResponse,
)
.unwrap();
let token = ctx.props().token.clone();
component.common.call_backend(
ctx,
HostService::reset_password_step2(token),
Msg::ValidateTokenResponse,
);
component
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
CommonComponentParts::<Self>::update(self, msg)
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, ctx, msg)
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.common.change(props)
}
fn view(&self) -> Html {
fn view(&self, ctx: &Context<Self>) -> Html {
let link = &ctx.link();
match (&self.username, &self.common.error) {
(None, None) => {
return html! {
@@ -158,9 +149,17 @@ impl Component for ResetPasswordStep2Form {
}
(None, Some(e)) => {
return html! {
<div class="alert alert-danger">
{e.to_string() }
</div>
<>
<div class="alert alert-danger">
{e.to_string() }
</div>
<Link
classes="btn-link btn"
disabled={self.common.is_task_running()}
to={AppRoute::Login}>
{"Back"}
</Link>
</>
}
}
_ => (),
@@ -178,14 +177,14 @@ impl Component for ResetPasswordStep2Form {
</label>
<div class="col-sm-10">
<Field
form=&self.form
form={&self.form}
field_name="password"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="new-password"
input_type="password"
oninput=self.common.callback(|_| Msg::FormUpdate) />
oninput={link.callback(|_| Msg::FormUpdate)} />
<div class="invalid-feedback">
{&self.form.field_message("password")}
</div>
@@ -198,14 +197,14 @@ impl Component for ResetPasswordStep2Form {
</label>
<div class="col-sm-10">
<Field
form=&self.form
form={&self.form}
field_name="confirm_password"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="new-password"
input_type="password"
oninput=self.common.callback(|_| Msg::FormUpdate) />
oninput={link.callback(|_| Msg::FormUpdate)} />
<div class="invalid-feedback">
{&self.form.field_message("confirm_password")}
</div>
@@ -215,8 +214,8 @@ impl Component for ResetPasswordStep2Form {
<button
class="btn btn-primary col-sm-1 col-form-label"
type="submit"
disabled=self.common.is_task_running()
onclick=self.common.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})>
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})}>
{"Submit"}
</button>
</div>

View File

@@ -1,34 +1,30 @@
use yew_router::{
components::{RouterAnchor, RouterButton},
Switch,
};
use yew_router::Routable;
#[derive(Switch, Debug, Clone)]
#[derive(Routable, Debug, Clone, PartialEq)]
pub enum AppRoute {
#[to = "/login"]
#[at("/login")]
Login,
#[to = "/reset-password/step1"]
#[at("/reset-password/step1")]
StartResetPassword,
#[to = "/reset-password/step2/{token}"]
FinishResetPassword(String),
#[to = "/users/create"]
#[at("/reset-password/step2/:token")]
FinishResetPassword { token: String },
#[at("/users/create")]
CreateUser,
#[to = "/users"]
#[at("/users")]
ListUsers,
#[to = "/user/{user_id}/password"]
ChangePassword(String),
#[to = "/user/{user_id}"]
UserDetails(String),
#[to = "/groups/create"]
#[at("/user/:user_id/password")]
ChangePassword { user_id: String },
#[at("/user/:user_id")]
UserDetails { user_id: String },
#[at("/groups/create")]
CreateGroup,
#[to = "/groups"]
#[at("/groups")]
ListGroups,
#[to = "/group/{group_id}"]
GroupDetails(i64),
#[to = "/"]
#[at("/group/:group_id")]
GroupDetails { group_id: i64 },
#[at("/")]
Index,
}
pub type Link = RouterAnchor<AppRoute>;
pub type NavButton = RouterButton<AppRoute>;
pub type Link = yew_router::components::Link<AppRoute>;
pub type Redirect = yew_router::components::Redirect<AppRoute>;

View File

@@ -1,9 +1,6 @@
use yew::{html::ChangeData, prelude::*};
use yewtil::NeqAssign;
use yew::prelude::*;
pub struct Select {
link: ComponentLink<Self>,
props: SelectProps,
node_ref: NodeRef,
}
@@ -14,100 +11,70 @@ pub struct SelectProps {
}
pub enum SelectMsg {
OnSelectChange(ChangeData),
OnSelectChange,
}
impl Select {
fn get_nth_child_props(&self, nth: i32) -> Option<SelectOptionProps> {
fn get_nth_child_props(&self, ctx: &Context<Self>, nth: i32) -> Option<SelectOptionProps> {
if nth == -1 {
return None;
}
self.props
ctx.props()
.children
.iter()
.nth(nth as usize)
.map(|child| child.props)
.map(|child| (*child.props).clone())
}
fn send_selection_update(&self) {
fn send_selection_update(&self, ctx: &Context<Self>) {
let select_node = self.node_ref.cast::<web_sys::HtmlSelectElement>().unwrap();
self.props
ctx.props()
.on_selection_change
.emit(self.get_nth_child_props(select_node.selected_index()))
.emit(self.get_nth_child_props(ctx, select_node.selected_index()))
}
}
impl Component for Select {
type Message = SelectMsg;
type Properties = SelectProps;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
fn create(_: &Context<Self>) -> Self {
Self {
link,
props,
node_ref: NodeRef::default(),
}
}
fn rendered(&mut self, _first_render: bool) {
self.send_selection_update();
fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
self.send_selection_update(ctx);
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
let SelectMsg::OnSelectChange(data) = msg;
match data {
ChangeData::Select(_) => self.send_selection_update(),
_ => unreachable!(),
}
fn update(&mut self, ctx: &Context<Self>, _: Self::Message) -> bool {
self.send_selection_update(ctx);
false
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.props.children.neq_assign(props.children)
}
fn view(&self) -> Html {
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<select
ref=self.node_ref.clone()
disabled=self.props.children.is_empty()
onchange=self.link.callback(SelectMsg::OnSelectChange)>
{ self.props.children.clone() }
<select class="form-select"
ref={self.node_ref.clone()}
disabled={ctx.props().children.is_empty()}
onchange={ctx.link().callback(|_| SelectMsg::OnSelectChange)}>
{ ctx.props().children.clone() }
</select>
}
}
}
pub struct SelectOption {
props: SelectOptionProps,
}
#[derive(yew::Properties, Clone, PartialEq, Debug)]
#[derive(yew::Properties, Clone, PartialEq, Eq, Debug)]
pub struct SelectOptionProps {
pub value: String,
pub text: String,
}
impl Component for SelectOption {
type Message = ();
type Properties = SelectOptionProps;
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
Self { props }
}
fn update(&mut self, _: Self::Message) -> ShouldRender {
false
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.props.neq_assign(props)
}
fn view(&self) -> Html {
html! {
<option value=self.props.value.clone()>
{&self.props.text}
</option>
}
#[function_component(SelectOption)]
pub fn select_option(props: &SelectOptionProps) -> Html {
html! {
<option value={props.value.clone()}>
{&props.text}
</option>
}
}

View File

@@ -2,7 +2,7 @@ use crate::{
components::{
add_user_to_group::AddUserToGroupComponent,
remove_user_from_group::RemoveUserFromGroupComponent,
router::{AppRoute, Link, NavButton},
router::{AppRoute, Link},
user_details_form::UserDetailsForm,
},
infra::common_component::{CommonComponent, CommonComponentParts},
@@ -40,14 +40,14 @@ pub enum Msg {
OnUserRemovedFromGroup((String, i64)),
}
#[derive(yew::Properties, Clone, PartialEq)]
#[derive(yew::Properties, Clone, PartialEq, Eq)]
pub struct Props {
pub username: String,
pub is_admin: bool,
}
impl CommonComponent<UserDetails> for UserDetails {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
fn handle_msg(&mut self, _: &Context<Self>, msg: <Self as Component>::Message) -> Result<bool> {
match msg {
Msg::UserDetailsResponse(response) => match response {
Ok(user) => self.user = Some(user.user),
@@ -77,10 +77,11 @@ impl CommonComponent<UserDetails> for UserDetails {
}
impl UserDetails {
fn get_user_details(&mut self) {
fn get_user_details(&mut self, ctx: &Context<Self>) {
self.common.call_graphql::<GetUserDetails, _>(
ctx,
get_user_details::Variables {
id: self.common.username.clone(),
id: ctx.props().username.clone(),
},
Msg::UserDetailsResponse,
"Error trying to fetch user details",
@@ -99,24 +100,25 @@ impl UserDetails {
}
}
fn view_group_memberships(&self, u: &User) -> Html {
fn view_group_memberships(&self, ctx: &Context<Self>, u: &User) -> Html {
let link = &ctx.link();
let make_group_row = |group: &Group| {
let display_name = group.display_name.clone();
html! {
<tr key="groupRow_".to_string() + &display_name>
{if self.common.is_admin { html! {
<tr key={"groupRow_".to_string() + &display_name}>
{if ctx.props().is_admin { html! {
<>
<td>
<Link route=AppRoute::GroupDetails(group.id)>
<Link to={AppRoute::GroupDetails{group_id: group.id}}>
{&group.display_name}
</Link>
</td>
<td>
<RemoveUserFromGroupComponent
username=u.id.clone()
group_id=group.id
on_user_removed_from_group=self.common.callback(Msg::OnUserRemovedFromGroup)
on_error=self.common.callback(Msg::OnError)/>
username={u.id.clone()}
group_id={group.id}
on_user_removed_from_group={link.callback(Msg::OnUserRemovedFromGroup)}
on_error={link.callback(Msg::OnError)}/>
</td>
</>
} } else { html! {
@@ -129,18 +131,18 @@ impl UserDetails {
<>
<h5 class="row m-3 fw-bold">{"Group memberships"}</h5>
<div class="table-responsive">
<table class="table table-striped">
<table class="table table-hover">
<thead>
<tr key="headerRow">
<th>{"Group"}</th>
{ if self.common.is_admin { html!{ <th></th> }} else { html!{} }}
{ if ctx.props().is_admin { html!{ <th></th> }} else { html!{} }}
</tr>
</thead>
<tbody>
{if u.groups.is_empty() {
html! {
<tr key="EmptyRow">
<td>{"Not member of any group"}</td>
<td>{"This user is not a member of any groups."}</td>
</tr>
}
} else {
@@ -153,14 +155,15 @@ impl UserDetails {
}
}
fn view_add_group_button(&self, u: &User) -> Html {
if self.common.is_admin {
fn view_add_group_button(&self, ctx: &Context<Self>, u: &User) -> Html {
let link = &ctx.link();
if ctx.props().is_admin {
html! {
<AddUserToGroupComponent
username=u.id.clone()
groups=u.groups.clone()
on_error=self.common.callback(Msg::OnError)
on_user_added_to_group=self.common.callback(Msg::OnUserAddedToGroup)/>
username={u.id.clone()}
groups={u.groups.clone()}
on_error={link.callback(Msg::OnError)}
on_user_added_to_group={link.callback(Msg::OnUserAddedToGroup)}/>
}
} else {
html! {}
@@ -172,24 +175,20 @@ impl Component for UserDetails {
type Message = Msg;
type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
fn create(ctx: &Context<Self>) -> Self {
let mut table = Self {
common: CommonComponentParts::<Self>::create(props, link),
common: CommonComponentParts::<Self>::create(),
user: None,
};
table.get_user_details();
table.get_user_details(ctx);
table
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
CommonComponentParts::<Self>::update(self, msg)
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, ctx, msg)
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.common.change(props)
}
fn view(&self) -> Html {
fn view(&self, ctx: &Context<Self>) -> Html {
match (&self.user, &self.common.error) {
(None, None) => html! {{"Loading..."}},
(None, Some(e)) => html! {<div>{"Error: "}{e.to_string()}</div>},
@@ -197,18 +196,20 @@ impl Component for UserDetails {
html! {
<>
<h3>{u.id.to_string()}</h3>
<UserDetailsForm
user=u.clone()
on_error=self.common.callback(Msg::OnError)/>
<div class="row justify-content-center">
<NavButton
route=AppRoute::ChangePassword(u.id.clone())
classes="btn btn-primary col-auto">
{"Change password"}
</NavButton>
<div class="d-flex flex-row-reverse">
<Link
to={AppRoute::ChangePassword{user_id: u.id.clone()}}
classes="btn btn-secondary">
<i class="bi-key me-2"></i>
{"Modify password"}
</Link>
</div>
{self.view_group_memberships(u)}
{self.view_add_group_button(u)}
<div>
<h5 class="row m-3 fw-bold">{"User details"}</h5>
</div>
<UserDetailsForm user={u.clone()} />
{self.view_group_memberships(ctx, u)}
{self.view_add_group_button(ctx, u)}
{self.view_messages(error)}
</>
}

View File

@@ -1,19 +1,49 @@
use std::str::FromStr;
use crate::{
components::user_details::User,
infra::common_component::{CommonComponent, CommonComponentParts},
};
use anyhow::{bail, Error, Result};
use gloo_file::{
callbacks::{read_as_bytes, FileReader},
File,
};
use graphql_client::GraphQLQuery;
use validator_derive::Validate;
use web_sys::{FileList, HtmlInputElement, InputEvent};
use yew::prelude::*;
use yew_form_derive::Model;
#[derive(Default)]
struct JsFile {
file: Option<File>,
contents: Option<Vec<u8>>,
}
impl ToString for JsFile {
fn to_string(&self) -> String {
self.file.as_ref().map(File::name).unwrap_or_default()
}
}
impl FromStr for JsFile {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
if s.is_empty() {
Ok(JsFile::default())
} else {
bail!("Building file from non-empty string")
}
}
}
/// The fields of the form, with the editable details and the constraints.
#[derive(Model, Validate, PartialEq, Clone)]
#[derive(Model, Validate, PartialEq, Eq, Clone)]
pub struct UserModel {
#[validate(email)]
email: String,
#[validate(length(min = 1, message = "Display name is required"))]
display_name: String,
first_name: String,
last_name: String,
@@ -25,7 +55,7 @@ pub struct UserModel {
schema_path = "../schema.graphql",
query_path = "queries/update_user.graphql",
response_derives = "Debug",
variables_derives = "Clone,PartialEq",
variables_derives = "Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql"
)]
pub struct UpdateUser;
@@ -34,33 +64,90 @@ pub struct UpdateUser;
pub struct UserDetailsForm {
common: CommonComponentParts<Self>,
form: yew_form::Form<UserModel>,
// None means that the avatar hasn't changed.
avatar: Option<JsFile>,
reader: Option<FileReader>,
/// True if we just successfully updated the user, to display a success message.
just_updated: bool,
user: User,
}
pub enum Msg {
/// A form field changed.
Update,
/// A new file was selected.
FileSelected(File),
/// The "Submit" button was clicked.
SubmitClicked,
/// The "Clear" button for the avatar was clicked.
ClearAvatarClicked,
/// A picked file finished loading.
FileLoaded(String, Result<Vec<u8>>),
/// We got the response from the server about our update message.
UserUpdated(Result<update_user::ResponseData>),
}
#[derive(yew::Properties, Clone, PartialEq)]
#[derive(yew::Properties, Clone, PartialEq, Eq)]
pub struct Props {
/// The current user details.
pub user: User,
/// Callback to report errors (e.g. server error).
pub on_error: Callback<Error>,
}
impl CommonComponent<UserDetailsForm> for UserDetailsForm {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg {
Msg::Update => Ok(true),
Msg::SubmitClicked => self.submit_user_update_form(),
Msg::FileSelected(new_avatar) => {
if self
.avatar
.as_ref()
.and_then(|f| f.file.as_ref().map(|f| f.name()))
!= Some(new_avatar.name())
{
let file_name = new_avatar.name();
let link = ctx.link().clone();
self.reader = Some(read_as_bytes(&new_avatar, move |res| {
link.send_message(Msg::FileLoaded(
file_name,
res.map_err(|e| anyhow::anyhow!("{:#}", e)),
))
}));
self.avatar = Some(JsFile {
file: Some(new_avatar),
contents: None,
});
}
Ok(true)
}
Msg::SubmitClicked => self.submit_user_update_form(ctx),
Msg::ClearAvatarClicked => {
self.avatar = Some(JsFile::default());
Ok(true)
}
Msg::UserUpdated(response) => self.user_update_finished(response),
Msg::FileLoaded(file_name, data) => {
if let Some(avatar) = &mut self.avatar {
if let Some(file) = &avatar.file {
if file.name() == file_name {
let data = data?;
if !is_valid_jpeg(data.as_slice()) {
// Clear the selection.
self.avatar = None;
bail!("Chosen image is not a valid JPEG");
} else {
avatar.contents = Some(data);
return Ok(true);
}
}
}
}
self.reader = None;
Ok(false)
}
}
}
@@ -73,35 +160,39 @@ impl Component for UserDetailsForm {
type Message = Msg;
type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
fn create(ctx: &Context<Self>) -> Self {
let model = UserModel {
email: props.user.email.clone(),
display_name: props.user.display_name.clone(),
first_name: props.user.first_name.clone(),
last_name: props.user.last_name.clone(),
email: ctx.props().user.email.clone(),
display_name: ctx.props().user.display_name.clone(),
first_name: ctx.props().user.first_name.clone(),
last_name: ctx.props().user.last_name.clone(),
};
Self {
common: CommonComponentParts::<Self>::create(props, link),
common: CommonComponentParts::<Self>::create(),
form: yew_form::Form::new(model),
avatar: None,
just_updated: false,
reader: None,
user: ctx.props().user.clone(),
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
self.just_updated = false;
CommonComponentParts::<Self>::update_and_report_error(
self,
msg,
self.common.on_error.clone(),
)
CommonComponentParts::<Self>::update(self, ctx, msg)
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.common.change(props)
}
fn view(&self) -> Html {
fn view(&self, ctx: &Context<Self>) -> Html {
type Field = yew_form::Field<UserModel>;
let link = &ctx.link();
let avatar_string = match &self.avatar {
Some(avatar) => {
let avatar_base64 = to_base64(avatar);
avatar_base64.as_deref().unwrap_or("").to_owned()
}
None => self.user.avatar.as_deref().unwrap_or("").to_owned(),
};
html! {
<div class="py-3">
<form class="form">
@@ -111,23 +202,43 @@ impl Component for UserDetailsForm {
{"User ID: "}
</label>
<div class="col-8">
<span id="userId" class="form-constrol-static">{&self.common.user.id}</span>
<span id="userId" class="form-control-static"><i>{&self.user.id}</i></span>
</div>
</div>
<div class="form-group row mb-3">
<label for="creationDate"
class="form-label col-4 col-form-label">
{"Creation date: "}
</label>
<div class="col-8">
<span id="creationDate" class="form-control-static">{&self.user.creation_date.naive_local().date()}</span>
</div>
</div>
<div class="form-group row mb-3">
<label for="uuid"
class="form-label col-4 col-form-label">
{"UUID: "}
</label>
<div class="col-8">
<span id="creationDate" class="form-control-static">{&self.user.uuid}</span>
</div>
</div>
<div class="form-group row mb-3">
<label for="email"
class="form-label col-4 col-form-label">
{"Email*: "}
{"Email"}
<span class="text-danger">{"*"}</span>
{":"}
</label>
<div class="col-8">
<Field
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
form=&self.form
form={&self.form}
field_name="email"
autocomplete="email"
oninput=self.common.callback(|_| Msg::Update) />
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("email")}
</div>
@@ -136,17 +247,17 @@ impl Component for UserDetailsForm {
<div class="form-group row mb-3">
<label for="display_name"
class="form-label col-4 col-form-label">
{"Display Name*: "}
{"Display Name: "}
</label>
<div class="col-8">
<Field
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
form=&self.form
form={&self.form}
field_name="display_name"
autocomplete="name"
oninput=self.common.callback(|_| Msg::Update) />
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("display_name")}
</div>
@@ -160,10 +271,10 @@ impl Component for UserDetailsForm {
<div class="col-8">
<Field
class="form-control"
form=&self.form
form={&self.form}
field_name="first_name"
autocomplete="given-name"
oninput=self.common.callback(|_| Msg::Update) />
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("first_name")}
</div>
@@ -177,36 +288,80 @@ impl Component for UserDetailsForm {
<div class="col-8">
<Field
class="form-control"
form=&self.form
form={&self.form}
field_name="last_name"
autocomplete="family-name"
oninput=self.common.callback(|_| Msg::Update) />
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("last_name")}
</div>
</div>
</div>
<div class="form-group row mb-3">
<label for="creationDate"
class="form-label col-4 col-form-label">
{"Creation date: "}
<div class="form-group row align-items-center mb-3">
<label for="avatar"
class="form-label col-4 col-form-label">
{"Avatar: "}
</label>
<div class="col-8">
<span id="creationDate" class="form-constrol-static">{&self.common.user.creation_date.date().naive_local()}</span>
<div class="row align-items-center">
<div class="col-5">
<input
class="form-control"
id="avatarInput"
type="file"
accept="image/jpeg"
oninput={link.callback(|e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
Self::upload_files(input.files())
})} />
</div>
<div class="col-3">
<button
class="btn btn-secondary col-auto"
id="avatarClear"
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::ClearAvatarClicked})}>
{"Clear"}
</button>
</div>
<div class="col-4">
{
if !avatar_string.is_empty() {
html!{
<img
id="avatarDisplay"
src={format!("data:image/jpeg;base64, {}", avatar_string)}
style="max-height:128px;max-width:128px;height:auto;width:auto;"
alt="Avatar" />
}
} else { html! {} }
}
</div>
</div>
</div>
</div>
<div class="form-group row justify-content-center">
<div class="form-group row justify-content-center mt-3">
<button
type="submit"
class="btn btn-primary col-auto col-form-label"
disabled=self.common.is_task_running()
onclick=self.common.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitClicked})>
{"Update"}
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitClicked})}>
<i class="bi-save me-2"></i>
{"Save changes"}
</button>
</div>
</form>
<div hidden=!self.just_updated>
<span>{"User successfully updated!"}</span>
{
if let Some(e) = &self.common.error {
html! {
<div class="alert alert-danger">
{e.to_string() }
</div>
}
} else { html! {} }
}
<div hidden={!self.just_updated}>
<div class="alert alert-success mt-4">{"User successfully updated!"}</div>
</div>
</div>
}
@@ -214,17 +369,27 @@ impl Component for UserDetailsForm {
}
impl UserDetailsForm {
fn submit_user_update_form(&mut self) -> Result<bool> {
fn submit_user_update_form(&mut self, ctx: &Context<Self>) -> Result<bool> {
if !self.form.validate() {
bail!("Invalid inputs");
}
let base_user = &self.common.user;
if let Some(JsFile {
file: Some(_),
contents: None,
}) = &self.avatar
{
bail!("Image file hasn't finished loading, try again");
}
let base_user = &self.user;
let mut user_input = update_user::UpdateUserInput {
id: self.common.user.id.clone(),
id: self.user.id.clone(),
email: None,
displayName: None,
firstName: None,
lastName: None,
avatar: None,
removeAttributes: None,
insertAttributes: None,
};
let default_user_input = user_input.clone();
let model = self.form.model();
@@ -241,12 +406,16 @@ impl UserDetailsForm {
if base_user.last_name != model.last_name {
user_input.lastName = Some(model.last_name);
}
if let Some(avatar) = &self.avatar {
user_input.avatar = Some(to_base64(avatar)?);
}
// Nothing changed.
if user_input == default_user_input {
return Ok(false);
}
let req = update_user::Variables { user: user_input };
self.common.call_graphql::<UpdateUser, _>(
ctx,
req,
Msg::UserUpdated,
"Error trying to update user",
@@ -255,23 +424,56 @@ impl UserDetailsForm {
}
fn user_update_finished(&mut self, r: Result<update_user::ResponseData>) -> Result<bool> {
self.common.cancel_task();
match r {
Err(e) => return Err(e),
Ok(_) => {
let model = self.form.model();
self.common.user = User {
id: self.common.user.id.clone(),
email: model.email,
display_name: model.display_name,
first_name: model.first_name,
last_name: model.last_name,
creation_date: self.common.user.creation_date,
groups: self.common.user.groups.clone(),
};
self.just_updated = true;
}
};
r?;
let model = self.form.model();
self.user.email = model.email;
self.user.display_name = model.display_name;
self.user.first_name = model.first_name;
self.user.last_name = model.last_name;
if let Some(avatar) = &self.avatar {
self.user.avatar = Some(to_base64(avatar)?);
}
self.just_updated = true;
Ok(true)
}
fn upload_files(files: Option<FileList>) -> Msg {
if let Some(files) = files {
if files.length() > 0 {
Msg::FileSelected(File::from(files.item(0).unwrap()))
} else {
Msg::Update
}
} else {
Msg::Update
}
}
}
fn is_valid_jpeg(bytes: &[u8]) -> bool {
image::io::Reader::with_format(std::io::Cursor::new(bytes), image::ImageFormat::Jpeg)
.decode()
.is_ok()
}
fn to_base64(file: &JsFile) -> Result<String> {
match file {
JsFile {
file: None,
contents: _,
} => Ok(String::new()),
JsFile {
file: Some(_),
contents: None,
} => bail!("Image file hasn't finished loading, try again"),
JsFile {
file: Some(_),
contents: Some(data),
} => {
if !is_valid_jpeg(data.as_slice()) {
bail!("Chosen image is not a valid JPEG");
}
Ok(base64::encode(data))
}
}
}

View File

@@ -34,7 +34,7 @@ pub enum Msg {
}
impl CommonComponent<UserTable> for UserTable {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
fn handle_msg(&mut self, _: &Context<Self>, msg: <Self as Component>::Message) -> Result<bool> {
match msg {
Msg::ListUsersResponse(users) => {
self.users = Some(users?.users.into_iter().collect());
@@ -55,8 +55,9 @@ impl CommonComponent<UserTable> for UserTable {
}
impl UserTable {
fn get_users(&mut self, req: Option<RequestFilter>) {
fn get_users(&mut self, ctx: &Context<Self>, req: Option<RequestFilter>) {
self.common.call_graphql::<ListUsersQuery, _>(
ctx,
list_users_query::Variables { filters: req },
Msg::ListUsersResponse,
"Error trying to fetch users",
@@ -68,27 +69,23 @@ impl Component for UserTable {
type Message = Msg;
type Properties = ();
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
fn create(ctx: &Context<Self>) -> Self {
let mut table = UserTable {
common: CommonComponentParts::<Self>::create(props, link),
common: CommonComponentParts::<Self>::create(),
users: None,
};
table.get_users(None);
table.get_users(ctx, None);
table
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
CommonComponentParts::<Self>::update(self, msg)
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, ctx, msg)
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.common.change(props)
}
fn view(&self) -> Html {
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<div>
{self.view_users()}
{self.view_users(ctx)}
{self.view_errors()}
</div>
}
@@ -96,11 +93,11 @@ impl Component for UserTable {
}
impl UserTable {
fn view_users(&self) -> Html {
fn view_users(&self, ctx: &Context<Self>) -> Html {
let make_table = |users: &Vec<User>| {
html! {
<div class="table-responsive">
<table class="table table-striped">
<table class="table table-hover">
<thead>
<tr>
<th>{"User ID"}</th>
@@ -113,7 +110,7 @@ impl UserTable {
</tr>
</thead>
<tbody>
{users.iter().map(|u| self.view_user(u)).collect::<Vec<_>>()}
{users.iter().map(|u| self.view_user(ctx, u)).collect::<Vec<_>>()}
</tbody>
</table>
</div>
@@ -125,20 +122,21 @@ impl UserTable {
}
}
fn view_user(&self, user: &User) -> Html {
fn view_user(&self, ctx: &Context<Self>, user: &User) -> Html {
let link = &ctx.link();
html! {
<tr key=user.id.clone()>
<td><Link route=AppRoute::UserDetails(user.id.clone())>{&user.id}</Link></td>
<tr key={user.id.clone()}>
<td><Link to={AppRoute::UserDetails{user_id: user.id.clone()}}>{&user.id}</Link></td>
<td>{&user.email}</td>
<td>{&user.display_name}</td>
<td>{&user.first_name}</td>
<td>{&user.last_name}</td>
<td>{&user.creation_date.date().naive_local()}</td>
<td>{&user.creation_date.naive_local().date()}</td>
<td>
<DeleteUser
username=user.id.clone()
on_user_deleted=self.common.callback(Msg::OnUserDeleted)
on_error=self.common.callback(Msg::OnError)/>
username={user.id.clone()}
on_user_deleted={link.callback(Msg::OnUserDeleted)}
on_error={link.callback(Msg::OnError)}/>
</td>
</tr>
}

View File

@@ -1,136 +1,88 @@
use super::cookies::set_cookie;
use anyhow::{anyhow, Context, Result};
use gloo_net::http::{Method, Request};
use graphql_client::GraphQLQuery;
use lldap_auth::{login, registration, JWTClaims};
use yew::callback::Callback;
use yew::format::Json;
use yew::services::fetch::{Credentials, FetchOptions, FetchService, FetchTask, Request, Response};
use serde::{de::DeserializeOwned, Serialize};
use web_sys::RequestCredentials;
#[derive(Default)]
pub struct HostService {}
fn get_default_options() -> FetchOptions {
FetchOptions {
credentials: Some(Credentials::SameOrigin),
..FetchOptions::default()
}
}
fn get_claims_from_jwt(jwt: &str) -> Result<JWTClaims> {
use jwt::*;
let token = Token::<header::Header, JWTClaims, token::Unverified>::parse_unverified(jwt)?;
Ok(token.claims().clone())
}
fn create_handler<Resp, CallbackResult, F>(
callback: Callback<Result<CallbackResult>>,
handler: F,
) -> Callback<Response<Result<Resp>>>
where
F: Fn(http::StatusCode, Resp) -> Result<CallbackResult> + 'static,
CallbackResult: 'static,
{
Callback::once(move |response: Response<Result<Resp>>| {
let (meta, maybe_data) = response.into_parts();
let message = maybe_data
.context("Could not reach server")
.and_then(|data| handler(meta.status, data));
callback.emit(message)
})
const NO_BODY: Option<()> = None;
fn base_url() -> String {
yew_router::utils::base_url().unwrap_or_default()
}
struct RequestBody<T>(T);
impl<'a, R> From<&'a R> for RequestBody<Json<&'a R>>
where
R: serde::ser::Serialize,
{
fn from(request: &'a R) -> Self {
Self(Json(request))
async fn call_server(
url: &str,
body: Option<impl Serialize>,
error_message: &'static str,
) -> Result<String> {
let mut request = Request::new(url)
.header("Content-Type", "application/json")
.credentials(RequestCredentials::SameOrigin);
if let Some(b) = body {
request = request
.body(serde_json::to_string(&b)?)
.method(Method::POST);
}
let response = request.send().await?;
if response.ok() {
Ok(response.text().await?)
} else {
Err(anyhow!(
"{}[{} {}]: {}",
error_message,
response.status(),
response.status_text(),
response.text().await?
))
}
}
impl From<yew::format::Nothing> for RequestBody<yew::format::Nothing> {
fn from(request: yew::format::Nothing) -> Self {
Self(request)
}
async fn call_server_json_with_error_message<CallbackResult, Body: Serialize>(
url: &str,
request: Option<Body>,
error_message: &'static str,
) -> Result<CallbackResult>
where
CallbackResult: DeserializeOwned + 'static,
{
let data = call_server(url, request, error_message).await?;
serde_json::from_str(&data).context("Could not parse response")
}
fn call_server<Req, CallbackResult, F, RB>(
async fn call_server_empty_response_with_error_message<Body: Serialize>(
url: &str,
request: RB,
callback: Callback<Result<CallbackResult>>,
request: Option<Body>,
error_message: &'static str,
parse_response: F,
) -> Result<FetchTask>
where
F: Fn(String) -> Result<CallbackResult> + 'static,
CallbackResult: 'static,
RB: Into<RequestBody<Req>>,
Req: Into<yew::format::Text>,
{
let request = {
// If the request type is empty (if the size is 0), it's a get.
if std::mem::size_of::<RB>() == 0 {
Request::get(url)
} else {
Request::post(url)
}
}
.header("Content-Type", "application/json")
.body(request.into().0)?;
let handler = create_handler(callback, move |status: http::StatusCode, data: String| {
if status.is_success() {
parse_response(data)
} else {
Err(anyhow!("{}[{}]: {}", error_message, status, data))
}
});
FetchService::fetch_with_options(request, get_default_options(), handler)
) -> Result<()> {
call_server(url, request, error_message).await.map(|_| ())
}
fn call_server_json_with_error_message<CallbackResult, RB, Req>(
url: &str,
request: RB,
callback: Callback<Result<CallbackResult>>,
error_message: &'static str,
) -> Result<FetchTask>
where
CallbackResult: serde::de::DeserializeOwned + 'static,
RB: Into<RequestBody<Req>>,
Req: Into<yew::format::Text>,
{
call_server(url, request, callback, error_message, |data: String| {
serde_json::from_str(&data).context("Could not parse response")
})
}
fn call_server_empty_response_with_error_message<RB, Req>(
url: &str,
request: RB,
callback: Callback<Result<()>>,
error_message: &'static str,
) -> Result<FetchTask>
where
RB: Into<RequestBody<Req>>,
Req: Into<yew::format::Text>,
{
call_server(
url,
request,
callback,
error_message,
|_data: String| Ok(()),
)
fn set_cookies_from_jwt(response: login::ServerLoginResponse) -> Result<(String, bool)> {
let jwt_claims = get_claims_from_jwt(response.token.as_str()).context("Could not parse JWT")?;
let is_admin = jwt_claims.groups.contains("lldap_admin");
set_cookie("user_id", &jwt_claims.user, &jwt_claims.exp)
.map(|_| set_cookie("is_admin", &is_admin.to_string(), &jwt_claims.exp))
.map(|_| (jwt_claims.user.clone(), is_admin))
.context("Error setting cookie")
}
impl HostService {
pub fn graphql_query<QueryType>(
pub async fn graphql_query<QueryType>(
variables: QueryType::Variables,
callback: Callback<Result<QueryType::ResponseData>>,
error_message: &'static str,
) -> Result<FetchTask>
) -> Result<QueryType::ResponseData>
where
QueryType: GraphQLQuery + 'static,
{
@@ -147,143 +99,111 @@ impl HostService {
)
})
};
let parse_graphql_response = move |data: String| {
serde_json::from_str(&data)
.context("Could not parse response")
.and_then(unwrap_graphql_response)
};
let request_body = QueryType::build_query(variables);
call_server(
"/api/graphql",
&request_body,
callback,
call_server_json_with_error_message::<graphql_client::Response<_>, _>(
&(base_url() + "/api/graphql"),
Some(request_body),
error_message,
parse_graphql_response,
)
.await
.and_then(unwrap_graphql_response)
}
pub fn login_start(
pub async fn login_start(
request: login::ClientLoginStartRequest,
callback: Callback<Result<Box<login::ServerLoginStartResponse>>>,
) -> Result<FetchTask> {
) -> Result<Box<login::ServerLoginStartResponse>> {
call_server_json_with_error_message(
"/auth/opaque/login/start",
&request,
callback,
&(base_url() + "/auth/opaque/login/start"),
Some(request),
"Could not start authentication: ",
)
.await
}
pub fn login_finish(
request: login::ClientLoginFinishRequest,
callback: Callback<Result<(String, bool)>>,
) -> Result<FetchTask> {
let set_cookies = |jwt_claims: JWTClaims| {
let is_admin = jwt_claims.groups.contains("lldap_admin");
set_cookie("user_id", &jwt_claims.user, &jwt_claims.exp)
.map(|_| set_cookie("is_admin", &is_admin.to_string(), &jwt_claims.exp))
.map(|_| (jwt_claims.user.clone(), is_admin))
.context("Error clearing cookie")
};
let parse_token = move |data: String| {
serde_json::from_str::<login::ServerLoginResponse>(&data)
.context("Could not parse response")
.and_then(|r| {
get_claims_from_jwt(r.token.as_str())
.context("Could not parse response")
.and_then(set_cookies)
})
};
call_server(
"/auth/opaque/login/finish",
&request,
callback,
pub async fn login_finish(request: login::ClientLoginFinishRequest) -> Result<(String, bool)> {
call_server_json_with_error_message::<login::ServerLoginResponse, _>(
&(base_url() + "/auth/opaque/login/finish"),
Some(request),
"Could not finish authentication",
parse_token,
)
.await
.and_then(set_cookies_from_jwt)
}
pub fn register_start(
pub async fn register_start(
request: registration::ClientRegistrationStartRequest,
callback: Callback<Result<Box<registration::ServerRegistrationStartResponse>>>,
) -> Result<FetchTask> {
) -> Result<Box<registration::ServerRegistrationStartResponse>> {
call_server_json_with_error_message(
"/auth/opaque/register/start",
&request,
callback,
&(base_url() + "/auth/opaque/register/start"),
Some(request),
"Could not start registration: ",
)
.await
}
pub fn register_finish(
pub async fn register_finish(
request: registration::ClientRegistrationFinishRequest,
callback: Callback<Result<()>>,
) -> Result<FetchTask> {
) -> Result<()> {
call_server_empty_response_with_error_message(
"/auth/opaque/register/finish",
&request,
callback,
&(base_url() + "/auth/opaque/register/finish"),
Some(request),
"Could not finish registration",
)
.await
}
pub fn refresh(_request: (), callback: Callback<Result<(String, bool)>>) -> Result<FetchTask> {
let set_cookies = |jwt_claims: JWTClaims| {
let is_admin = jwt_claims.groups.contains("lldap_admin");
set_cookie("user_id", &jwt_claims.user, &jwt_claims.exp)
.map(|_| set_cookie("is_admin", &is_admin.to_string(), &jwt_claims.exp))
.map(|_| (jwt_claims.user.clone(), is_admin))
.context("Error clearing cookie")
};
let parse_token = move |data: String| {
serde_json::from_str::<login::ServerLoginResponse>(&data)
.context("Could not parse response")
.and_then(|r| {
get_claims_from_jwt(r.token.as_str())
.context("Could not parse response")
.and_then(set_cookies)
})
};
call_server(
"/auth/refresh",
yew::format::Nothing,
callback,
pub async fn refresh() -> Result<(String, bool)> {
call_server_json_with_error_message::<login::ServerLoginResponse, _>(
&(base_url() + "/auth/refresh"),
NO_BODY,
"Could not start authentication: ",
parse_token,
)
.await
.and_then(set_cookies_from_jwt)
}
// The `_request` parameter is to make it the same shape as the other functions.
pub fn logout(_request: (), callback: Callback<Result<()>>) -> Result<FetchTask> {
pub async fn logout() -> Result<()> {
call_server_empty_response_with_error_message(
"/auth/logout",
yew::format::Nothing,
callback,
&(base_url() + "/auth/logout"),
NO_BODY,
"Could not logout",
)
.await
}
pub fn reset_password_step1(
username: &str,
callback: Callback<Result<()>>,
) -> Result<FetchTask> {
pub async fn reset_password_step1(username: String) -> Result<()> {
call_server_empty_response_with_error_message(
&format!("/auth/reset/step1/{}", username),
yew::format::Nothing,
callback,
&format!(
"{}/auth/reset/step1/{}",
base_url(),
url_escape::encode_query(&username)
),
NO_BODY,
"Could not initiate password reset",
)
.await
}
pub fn reset_password_step2(
token: &str,
callback: Callback<Result<lldap_auth::password_reset::ServerPasswordResetResponse>>,
) -> Result<FetchTask> {
pub async fn reset_password_step2(
token: String,
) -> Result<lldap_auth::password_reset::ServerPasswordResetResponse> {
call_server_json_with_error_message(
&format!("/auth/reset/step2/{}", token),
yew::format::Nothing,
callback,
&format!("{}/auth/reset/step2/{}", base_url(), token),
NO_BODY,
"Could not validate token",
)
.await
}
pub async fn probe_password_reset() -> Result<bool> {
Ok(gloo_net::http::Request::get(
&(base_url() + "/auth/reset/step1/lldap_unlikely_very_long_user_name"),
)
.header("Content-Type", "application/json")
.send()
.await?
.status()
!= http::StatusCode::NOT_FOUND)
}
}

View File

@@ -21,21 +21,28 @@
//! [`CommonComponentParts::update`]. This will in turn call [`CommonComponent::handle_msg`] and
//! take care of error and task handling.
use std::{
future::Future,
marker::PhantomData,
sync::{Arc, Mutex},
};
use crate::infra::api::HostService;
use anyhow::{Error, Result};
use gloo_console::error;
use graphql_client::GraphQLQuery;
use yew::{
prelude::*,
services::{fetch::FetchTask, ConsoleService},
};
use yewtil::NeqAssign;
use yew::prelude::*;
/// Trait required for common components.
pub trait CommonComponent<C: Component + CommonComponent<C>>: Component {
/// Handle the incoming message. If an error is returned here, any running task will be
/// cancelled, the error will be written to the [`CommonComponentParts::error`] and the
/// component will be refreshed.
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool>;
fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool>;
/// Get a mutable reference to the inner component parts, necessary for the CRTP.
fn mut_common(&mut self) -> &mut CommonComponentParts<C>;
}
@@ -43,41 +50,33 @@ pub trait CommonComponent<C: Component + CommonComponent<C>>: Component {
/// Structure that contains the common parts needed by most components.
/// The fields of [`props`] are directly accessible through a `Deref` implementation.
pub struct CommonComponentParts<C: CommonComponent<C>> {
link: ComponentLink<C>,
pub props: <C as Component>::Properties,
pub error: Option<Error>,
task: Option<FetchTask>,
is_task_running: Arc<Mutex<bool>>,
_phantom: PhantomData<C>,
}
impl<C: CommonComponent<C>> CommonComponentParts<C> {
pub fn create() -> Self {
CommonComponentParts {
error: None,
is_task_running: Arc::new(Mutex::new(false)),
_phantom: PhantomData::<C>,
}
}
/// Whether there is a currently running task in the background.
pub fn is_task_running(&self) -> bool {
self.task.is_some()
}
/// Cancel any background task.
pub fn cancel_task(&mut self) {
self.task = None;
}
pub fn create(props: <C as Component>::Properties, link: ComponentLink<C>) -> Self {
Self {
link,
props,
error: None,
task: None,
}
*self.is_task_running.lock().unwrap()
}
/// This should be called from the [`yew::prelude::Component::update`]: it will in turn call
/// [`CommonComponent::handle_msg`] and handle any resulting error.
pub fn update(com: &mut C, msg: <C as Component>::Message) -> ShouldRender {
pub fn update(com: &mut C, ctx: &Context<C>, msg: <C as Component>::Message) -> bool {
com.mut_common().error = None;
match com.handle_msg(msg) {
match com.handle_msg(ctx, msg) {
Err(e) => {
ConsoleService::error(&e.to_string());
error!(&e.to_string());
com.mut_common().error = Some(e);
com.mut_common().cancel_task();
assert!(!*com.mut_common().is_task_running.lock().unwrap());
true
}
Ok(b) => b,
@@ -87,10 +86,11 @@ impl<C: CommonComponent<C>> CommonComponentParts<C> {
/// Same as above, but the resulting error is instead passed to the reporting function.
pub fn update_and_report_error(
com: &mut C,
ctx: &Context<C>,
msg: <C as Component>::Message,
report_fn: Callback<Error>,
) -> ShouldRender {
let should_render = Self::update(com, msg);
) -> bool {
let should_render = Self::update(com, ctx, msg);
com.mut_common()
.error
.take()
@@ -101,38 +101,24 @@ impl<C: CommonComponent<C>> CommonComponentParts<C> {
.unwrap_or(should_render)
}
/// This can be called from [`yew::prelude::Component::update`]: it will check if the
/// properties have changed and return whether the component should update.
pub fn change(&mut self, props: <C as Component>::Properties) -> ShouldRender
where
<C as yew::Component>::Properties: std::cmp::PartialEq,
{
self.props.neq_assign(props)
}
/// Create a callback from the link.
pub fn callback<F, IN, M>(&self, function: F) -> Callback<IN>
where
M: Into<C::Message>,
F: Fn(IN) -> M + 'static,
{
self.link.callback(function)
}
/// Call `method` from the backend with the given `request`, and pass the `callback` for the
/// result. Returns whether _starting the call_ failed.
pub fn call_backend<M, Req, Cb, Resp>(
&mut self,
method: M,
req: Req,
callback: Cb,
) -> Result<()>
/// result.
pub fn call_backend<Fut, Cb, Resp>(&mut self, ctx: &Context<C>, fut: Fut, callback: Cb)
where
M: Fn(Req, Callback<Resp>) -> Result<FetchTask>,
Fut: Future<Output = Resp> + 'static,
Cb: FnOnce(Resp) -> <C as Component>::Message + 'static,
{
self.task = Some(method(req, self.link.callback_once(callback))?);
Ok(())
{
let mut running = self.is_task_running.lock().unwrap();
assert!(!*running);
*running = true;
}
let is_task_running = self.is_task_running.clone();
ctx.link().send_future(async move {
let res = fut.await;
*is_task_running.lock().unwrap() = false;
callback(res)
});
}
/// Call the backend with a GraphQL query.
@@ -140,6 +126,7 @@ impl<C: CommonComponent<C>> CommonComponentParts<C> {
/// `EnumCallback` should usually be left as `_`.
pub fn call_graphql<QueryType, EnumCallback>(
&mut self,
ctx: &Context<C>,
variables: QueryType::Variables,
enum_callback: EnumCallback,
error_message: &'static str,
@@ -147,29 +134,10 @@ impl<C: CommonComponent<C>> CommonComponentParts<C> {
QueryType: GraphQLQuery + 'static,
EnumCallback: Fn(Result<QueryType::ResponseData>) -> <C as Component>::Message + 'static,
{
self.task = HostService::graphql_query::<QueryType>(
variables,
self.link.callback(enum_callback),
error_message,
)
.map_err::<(), _>(|e| {
ConsoleService::log(&e.to_string());
self.error = Some(e);
})
.ok();
}
}
impl<C: Component + CommonComponent<C>> std::ops::Deref for CommonComponentParts<C> {
type Target = <C as Component>::Properties;
fn deref(&self) -> &<Self as std::ops::Deref>::Target {
&self.props
}
}
impl<C: Component + CommonComponent<C>> std::ops::DerefMut for CommonComponentParts<C> {
fn deref_mut(&mut self) -> &mut <Self as std::ops::Deref>::Target {
&mut self.props
self.call_backend(
ctx,
HostService::graphql_query::<QueryType>(variables, error_message),
enum_callback,
);
}
}

View File

@@ -22,10 +22,11 @@ pub fn set_cookie(cookie_name: &str, value: &str, expiration: &DateTime<Utc>) ->
.map_err(|_| anyhow!("Document is not an HTMLDocument"))
})?;
let cookie_string = format!(
"{}={}; expires={}; sameSite=Strict; path=/",
"{}={}; expires={}; sameSite=Strict; path={}/",
cookie_name,
value,
expiration.to_rfc2822()
expiration.to_rfc2822(),
yew_router::utils::base_url().unwrap_or_default()
);
doc.set_cookie(&cookie_string)
.map_err(|_| anyhow!("Could not set cookie"))
@@ -53,7 +54,11 @@ pub fn get_cookie(cookie_name: &str) -> Result<Option<String>> {
pub fn delete_cookie(cookie_name: &str) -> Result<()> {
if get_cookie(cookie_name)?.is_some() {
set_cookie(cookie_name, "", &Utc.ymd(1970, 1, 1).and_hms(0, 0, 0))
set_cookie(
cookie_name,
"",
&Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap(),
)
} else {
Ok(())
}

View File

@@ -1,16 +1,16 @@
use wasm_bindgen::prelude::*;
#[wasm_bindgen(module = "bootstrap")]
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen]
#[wasm_bindgen(js_namespace = bootstrap)]
pub type Modal;
#[wasm_bindgen(constructor)]
#[wasm_bindgen(constructor, js_namespace = bootstrap)]
pub fn new(e: web_sys::Element) -> Modal;
#[wasm_bindgen(method)]
#[wasm_bindgen(method, js_namespace = bootstrap)]
pub fn show(this: &Modal);
#[wasm_bindgen(method)]
#[wasm_bindgen(method, js_namespace = bootstrap)]
pub fn hide(this: &Modal);
}

View File

@@ -1,6 +1,8 @@
#![recursion_limit = "256"]
#![forbid(non_ascii_idents)]
#![allow(clippy::nonstandard_macro_braces)]
#![allow(clippy::uninlined_format_args)]
#![allow(clippy::let_unit_value)]
pub mod components;
pub mod infra;
@@ -8,7 +10,7 @@ use wasm_bindgen::prelude::{wasm_bindgen, JsValue};
#[wasm_bindgen]
pub fn run_app() -> Result<(), JsValue> {
yew::start_app::<components::app::App>();
yew::start_app::<components::app::AppContainer>();
Ok(())
}

View File

@@ -1,4 +1,5 @@
https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css
https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-nightshade.min.css
https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/js/darkmode.min.js
https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.bundle.min.js
https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css
https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css

10
app/static/main.js Normal file
View File

@@ -0,0 +1,10 @@
import init, { run_app } from '/pkg/lldap_app.js';
async function main() {
if(navigator.userAgent.indexOf('AppleWebKit') != -1) {
await init('/pkg/lldap_app_bg.wasm');
} else {
await init('/pkg/lldap_app_bg.wasm.gz');
}
run_app();
}
main()

View File

@@ -1,4 +1,4 @@
header h1 {
header h2 {
font-family: 'Bebas Neue', cursive;
}
@@ -10,3 +10,23 @@ header h1 {
font-weight: 700;
text-decoration: none;
}
html.dark .bg-light {
background-color: rgba(59,59,59,1) !important;
}
html.dark a {
color: #e1e1e1
}
a {
color: #212529
}
html.dark .nav-link {
color: #e1e1e1
}
.nav-link {
color: #212529
}

View File

@@ -1,20 +1,25 @@
[package]
name = "lldap_auth"
version = "0.3.0-alpha.1"
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
description = "Authentication protocol for LLDAP"
edition = "2021"
homepage = "https://github.com/lldap/lldap"
license = "GPL-3.0-only"
name = "lldap_auth"
repository = "https://github.com/lldap/lldap"
version = "0.4.0"
[features]
default = ["opaque_server", "opaque_client"]
opaque_server = []
opaque_client = []
js = []
sea_orm = ["dep:sea-orm"]
[dependencies]
rust-argon2 = "0.8"
curve25519-dalek = "3"
digest = "0.9"
generic-array = "*"
generic-array = "0.14"
rand = "0.8"
serde = "*"
sha2 = "0.9"
@@ -27,10 +32,16 @@ version = "0.6"
version = "*"
features = [ "serde" ]
[dependencies.sea-orm]
version= "0.12"
default-features = false
features = ["macros"]
optional = true
# For WASM targets, use the JS getrandom.
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.getrandom]
version = "0.2"
features = ["js"]
[target.'cfg(target_arch = "wasm32")'.dependencies.getrandom]
version = "0.2"
features = ["js"]

View File

@@ -9,17 +9,17 @@ pub mod opaque;
/// The messages for the 3-step OPAQUE and simple login process.
pub mod login {
use super::*;
use super::{types::UserId, *};
#[derive(Serialize, Deserialize, Clone)]
pub struct ServerData {
pub username: String,
pub username: UserId,
pub server_login: opaque::server::login::ServerLogin,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct ClientLoginStartRequest {
pub username: String,
pub username: UserId,
pub login_start_request: opaque::server::login::CredentialRequest,
}
@@ -39,14 +39,14 @@ pub mod login {
#[derive(Serialize, Deserialize, Clone)]
pub struct ClientSimpleLoginRequest {
pub username: String,
pub username: UserId,
pub password: String,
}
impl fmt::Debug for ClientSimpleLoginRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ClientSimpleLoginRequest")
.field("username", &self.username)
.field("username", &self.username.as_str())
.field("password", &"***********")
.finish()
}
@@ -63,16 +63,16 @@ pub mod login {
/// The messages for the 3-step OPAQUE registration process.
/// It is used to reset a user's password.
pub mod registration {
use super::*;
use super::{types::UserId, *};
#[derive(Serialize, Deserialize, Clone)]
pub struct ServerData {
pub username: String,
pub username: UserId,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct ClientRegistrationStartRequest {
pub username: String,
pub username: UserId,
pub registration_start_request: opaque::server::registration::RegistrationRequest,
}
@@ -104,6 +104,100 @@ pub mod password_reset {
}
}
pub mod types {
use serde::{Deserialize, Serialize};
#[cfg(feature = "sea_orm")]
use sea_orm::{DbErr, DeriveValueType, QueryResult, TryFromU64, Value};
#[derive(
PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Default, Hash, Serialize, Deserialize,
)]
#[cfg_attr(feature = "sea_orm", derive(DeriveValueType))]
#[serde(from = "String")]
pub struct CaseInsensitiveString(String);
impl CaseInsensitiveString {
pub fn new(s: &str) -> Self {
Self(s.to_ascii_lowercase())
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn into_string(self) -> String {
self.0
}
}
impl From<String> for CaseInsensitiveString {
fn from(mut s: String) -> Self {
s.make_ascii_lowercase();
Self(s)
}
}
impl From<&String> for CaseInsensitiveString {
fn from(s: &String) -> Self {
Self::new(s.as_str())
}
}
impl From<&str> for CaseInsensitiveString {
fn from(s: &str) -> Self {
Self::new(s)
}
}
#[derive(
PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Default, Hash, Serialize, Deserialize,
)]
#[cfg_attr(feature = "sea_orm", derive(DeriveValueType))]
#[serde(from = "CaseInsensitiveString")]
pub struct UserId(CaseInsensitiveString);
impl UserId {
pub fn new(s: &str) -> Self {
s.into()
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn into_string(self) -> String {
self.0.into_string()
}
}
impl<T> From<T> for UserId
where
T: Into<CaseInsensitiveString>,
{
fn from(s: T) -> Self {
Self(s.into())
}
}
impl std::fmt::Display for UserId {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0.as_str())
}
}
#[cfg(feature = "sea_orm")]
impl From<&UserId> for Value {
fn from(user_id: &UserId) -> Self {
user_id.as_str().into()
}
}
#[cfg(feature = "sea_orm")]
impl TryFromU64 for UserId {
fn try_from_u64(_n: u64) -> Result<Self, DbErr> {
Err(DbErr::ConvertFromU64(
"UserId cannot be constructed from u64",
))
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct JWTClaims {
pub exp: DateTime<Utc>,

View File

@@ -1,3 +1,4 @@
use crate::types::UserId;
use opaque_ke::ciphersuite::CipherSuite;
use rand::{CryptoRng, RngCore};
@@ -77,10 +78,10 @@ pub mod client {
pub use opaque_ke::ClientRegistrationFinishParameters;
/// Initiate the registration negotiation.
pub fn start_registration<R: RngCore + CryptoRng>(
password: &str,
password: &[u8],
rng: &mut R,
) -> AuthenticationResult<ClientRegistrationStartResult> {
Ok(ClientRegistration::start(rng, password.as_bytes())?)
Ok(ClientRegistration::start(rng, password)?)
}
/// Finalize the registration negotiation.
@@ -145,12 +146,12 @@ pub mod server {
pub fn start_registration(
server_setup: &ServerSetup,
registration_request: RegistrationRequest,
username: &str,
username: &UserId,
) -> AuthenticationResult<ServerRegistrationStartResult> {
Ok(ServerRegistration::start(
server_setup,
registration_request,
username.as_bytes(),
username.as_str().as_bytes(),
)?)
}
@@ -178,14 +179,14 @@ pub mod server {
server_setup: &ServerSetup,
password_file: Option<ServerRegistration>,
credential_request: CredentialRequest,
username: &str,
username: &UserId,
) -> AuthenticationResult<ServerLoginStartResult> {
Ok(ServerLogin::start(
rng,
server_setup,
password_file,
credential_request,
username.as_bytes(),
username.as_str().as_bytes(),
ServerLoginStartParameters::default(),
)?)
}

20
docker-entrypoint-rootless.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
CONFIG_FILE=/data/lldap_config.toml
if [ ! -f "$CONFIG_FILE" ]; then
echo "[entrypoint] Copying the default config to $CONFIG_FILE"
echo "[entrypoint] Edit this $CONFIG_FILE to configure LLDAP."
if cp /app/lldap_config.docker_template.toml $CONFIG_FILE; then
echo "Configuration copied successfully."
else
echo "Fail to copy configuration, check permission on /data or manually create one by copying from LLDAP repository"
exit 1
fi
fi
echo "> Starting lldap.."
echo ""
exec /app/lldap "$@"
exec "$@"

View File

@@ -1,20 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
for SECRET in LLDAP_JWT_SECRET LLDAP_LDAP_USER_PASS; do
FILE_VAR="${SECRET}_FILE"
SECRET_FILE="${!FILE_VAR:-}"
if [[ -n "$SECRET_FILE" ]]; then
if [[ -f "$SECRET_FILE" ]]; then
declare "$SECRET=$(cat $SECRET_FILE)"
export "$SECRET"
echo "[entrypoint] Set $SECRET from $SECRET_FILE"
else
echo "[entrypoint] Could not read contents of $SECRET_FILE (specified in $FILE_VAR)" >&2
fi
fi
done
CONFIG_FILE=/data/lldap_config.toml
if [[ ( ! -w "/data" ) ]] || [[ ( ! -d "/data" ) ]]; then
@@ -35,4 +21,13 @@ if [[ ! -r "$CONFIG_FILE" ]]; then
exit 1;
fi
exec /app/lldap "$@"
echo "> Setup permissions.."
find /app \! -user "$UID" -exec chown "$UID:$GID" '{}' +
find /data \! -user "$UID" -exec chown "$UID:$GID" '{}' +
echo "> Starting lldap.."
echo ""
exec gosu "$UID:$GID" /app/lldap "$@"
exec "$@"

View File

@@ -26,9 +26,9 @@ Frontend:
Data storage:
* The data (users, groups, memberships, active JWTs, ...) is stored in SQL.
* Currently only SQLite is supported (see
https://github.com/launchbadge/sqlx/issues/1225 for what blocks us from
supporting more SQL backends).
* The main SQL DBs are supported: SQLite by default, MySQL, MariaDB, PostgreSQL
(see [DB Migration](/database_migration.md) for how to migrate off of
SQLite).
### Code organization

BIN
docs/cookie.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

111
docs/database_migration.md Normal file
View File

@@ -0,0 +1,111 @@
# Migration
Existing servers can migrate from one database backend to another. This page includes guidance for migrating from SQLite - similar concepts apply when migrating from databases of other types.
NOTE: [pgloader](https://github.com/dimitri/pgloader) is a tool that can easily migrate to PostgreSQL from other databases. Consider it if your target database is PostgreSQL
The process is as follows:
1. Create empty schema on target database
2. Stop/pause LLDAP and dump existing values
3. Sanitize for target DB (not always required)
4. Insert data into target
5. Change LLDAP config to new target and restart
The steps below assume you already have PostgreSQL or MySQL set up with an empty database for LLDAP to use.
## Create schema on target
LLDAP has a command that will connect to a target database and initialize the
schema. If running with docker, run the following command to use your active
instance (this has the benefit of ensuring your container has access):
```sh
docker exec -it <LLDAP container name> /app/lldap create_schema -d <Target database url>
```
If it succeeds, you can proceed to the next step.
## Create a dump of existing data
We want to dump (almost) all existing values to some file - the exception being the `metadata` table (and sometimes
the `sqlite_sequence` table, when it exists). Be sure to stop/pause LLDAP during this step, as some
databases (SQLite in this example) will give an error if LLDAP is in the middle of a write. The dump should consist just INSERT
statements. There are various ways to do this, but a simple enough way is filtering a
whole database dump. This repo contains [a script](/scripts/sqlite_dump_commands.sh) to generate SQLite commands for creating an appropriate dump:
```sh
./sqlite_dump_commands.sh | sqlite3 /path/to/lldap/config/users.db > /path/to/dump.sql
```
## Sanitize data
Some databases might use different formats for some data - for example, PostgreSQL uses
a different syntax for hex strings than SQLite. We also want to make sure inserts are done in
a transaction in case one of the statements fail.
### To PostgreSQL
PostgreSQL uses a different hex string format. The command below should switch SQLite
format to PostgreSQL format, and wrap it all in a transaction:
```sh
sed -i -r -e "s/X'([[:xdigit:]]+'[^'])/'\\\x\\1/g" \
-e ":a; s/(INSERT INTO (user_attribute_schema|jwt_storage)\(.*\) VALUES\(.*),1([^']*\);)$/\1,true\3/; s/(INSERT INTO (user_attribute_schema|jwt_storage)\(.*\) VALUES\(.*),0([^']*\);)$/\1,false\3/; ta" \
-e '1s/^/BEGIN;\n/' \
-e '$aSELECT setval(pg_get_serial_sequence('\''groups'\'', '\''group_id'\''), COALESCE((SELECT MAX(group_id) FROM groups), 1));' \
-e '$aCOMMIT;' /path/to/dump.sql
```
### To MySQL
MySQL mostly cooperates, but it gets some errors if you don't escape the `groups` table. It also uses
backticks to escape table name instead of quotes. Run the
following command to wrap all table names in backticks for good measure, and wrap the inserts in
a transaction:
```sh
sed -i -r -e 's/^INSERT INTO "?([a-zA-Z0-9_]+)"?/INSERT INTO `\1`/' \
-e '1s/^/START TRANSACTION;\n/' \
-e '$aCOMMIT;' \
-e '1 i\SET FOREIGN_KEY_CHECKS = 0;' /path/to/dump.sql
```
### To MariaDB
While MariaDB is supposed to be identical to MySQL, it doesn't support timezone offsets on DATETIME
strings. Use the following command to remove those and perform the additional MySQL sanitization:
```sh
sed -i -r -e "s/([^']'[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{9})\+00:00'([^'])/\1'\2/g" \
-e 's/^INSERT INTO "?([a-zA-Z0-9_]+)"?/INSERT INTO `\1`/' \
-e '1s/^/START TRANSACTION;\n/' \
-e '$aCOMMIT;' \
-e '1 i\SET FOREIGN_KEY_CHECKS = 0;' /path/to/dump.sql
```
## Insert data
Insert the data generated from the previous step into the target database. If you encounter errors,
you may need to manually tweak your dump, or make changed in LLDAP and recreate the dump.
### PostgreSQL
`psql -d <database> -U <username> -W < /path/to/dump.sql`
or
`psql -d <database> -U <username> -W -f /path/to/dump.sql`
### MySQL
`mysql -u <username> -p <database> < /path/to/dump.sql`
## Switch to new database
Modify your `database_url` in `lldap_config.toml` (or `LLDAP_DATABASE_URL` in the env)
to point to your new database (the same value used when generating schema). Restart
LLDAP and check the logs to ensure there were no errors.
#### More details/examples can be seen in the CI process [here](https://raw.githubusercontent.com/lldap/lldap/main/.github/workflows/docker-build-static.yml), look for the job `lldap-database-migration-test`

View File

@@ -0,0 +1,58 @@
# Migration from 0.4 to 0.5
Welcome! If you're here, it's probably that the migration from 0.4.x to 0.5
didn't go smoothly for you. Don't worry, we can fix that.
## Multiple users with the same email
This is the most common case. You can see in the LLDAP logs that there are
several users with the same email, and they are listed.
This is not allowed anymore in v0.5, to prevent a user from setting their email
to someone else's email and gaining access to systems that identify by email.
The problem is that you currently have several users with the same email, so the
constraint cannot be enforced.
### Step 1: Take a note of the users with duplicate emails
In the LLDAP logs when you tried to start v0.5+, you'll see some warnings with
the list of users with the same emails. Take note of them.
### Step 2: Downgrade to v0.4.3
If using docker, switch to the `lldap/lldap:v0.4.3` image. Alternatively, grab
the binaries at https://github.com/lldap/lldap/releases/tag/v0.4.3.
This downgrade is safe and supported.
### Step 3: Remove duplicate emails
Restart LLDAP with the v0.4.3 version, and using your notes from step 1, change
the email of users with duplicate emails to make sure that each email is unique.
### Step 4: Upgrade again
You can now revert to the initial version.
## Multiple users/groups with the same UUID
This should be extremely rare. In this case, you'll need to find which users
have the same UUID, revert to v0.4.3 to be able to apply the changes, and delete
one of the duplicates.
## FAQ
### What if I want several users to be controlled by the same email?
You can use plus codes to set "the same" email to several users, while ensuring
that they can't identify as each other. For instance:
- Admin: `admin@example.com`
- Read-only admin: `admin+readonly@example.com`
- Jellyfin admin: `admin+jellyfin@example.com`
### I'm upgrading to a higher version than v0.5.
This guide is still relevant: you can use whatever later version in place of
v0.5. You'll still need to revert to v0.4.3 to apply the changes.

99
docs/scripting.md Normal file
View File

@@ -0,0 +1,99 @@
# Scripting
Programmatically accessing LLDAP can be done either through the LDAP protocol,
or via the GraphQL API.
## LDAP
Most _read-only_ queries about users and groups are supported. Anything not
supported would be considered a missing feature or a bug.
Most _modification_ queries are not supported, except for creating users and
changing the password (through the extended password operation). Those could be
added in the future, on a case-by-case basis.
Most _meta_-queries about the LDAP server itself are not supported and are out
of scope. That includes anything that touches the schema, for instance. LLDAP
still supports basic RootDSE queries.
Anonymous bind is not supported.
## `lldap-cli`
There is a community-built CLI frontend,
[Zepmann/lldap-cli](https://github.com/Zepmann/lldap-cli), that supports all
(as of this writing) the operations possible. Getting information from the
server, creating users, adding them to groups, creating new custom attributes
and populating them, all of that is supported. It is currently the easiest way
to script the interaction with LLDAP.
## GraphQL
The best way to interact with LLDAP programmatically is via the GraphQL
interface. You can use any language that has a GraphQL library (most of them
do), and use the [GraphQL Schema](../schema.graphql) to guide your queries.
### Getting a token
You'll need a JWT (authentication token) to issue GraphQL queries. Your view of
the system will be limited by the rights of your user. In particular, regular
users can only see themselves and the groups they belong to (but not other
members of these groups, for instance).
#### Manually
Log in to the web front-end of LLDAP. Then open the developer tools (F12), find
the "Storage > Cookies", and you'll find the "token" cookie with your JWT.
![Cookies menu with a JWT](cookie.png)
#### Automatically
The easiest way is to send a json POST request to `/auth/simple/login` with
`{"username": "john", "password": "1234"}` in the body.
Then you'll receive a JSON response with:
```
{
"token": "eYbat...",
"refreshToken": "3bCka...",
}
```
### Using the token
You can use the token directly, either as a cookie, or as a bearer auth token
(add an "Authorization" header with contents `"Bearer <token>"`).
The JWT is valid for 1 day (unless you log out explicitly).
You can use the refresh token to query `/auth/refresh` and get another JWT. The
refresh token is valid for 30 days.
### Testing your GraphQL queries
You can go to `/api/graphql/playground` to test your queries and explore the
data in the playground. You'll need to provide the JWT in the headers:
```
{ "Authorization": "Bearer abcdef123..." }
```
Then you can enter your query, for instance:
```graphql
{
user(userId:"admin") {
displayName
}
groups {
id
displayName
users {
id
email
}
}
}
```
The schema is on the right, along with some basic docs.

View File

@@ -0,0 +1,26 @@
# Configuration for Airsonic Advanced
Replace `dc=example,dc=com` with your LLDAP configured domain.
### LDAP URL
```
ldap://lldap:3890/ou=people,dc=example,dc=com
```
### LDAP search filter
```
(&(uid={0})(memberof=cn=airsonic,ou=groups,dc=example,dc=com))
```
### LDAP manager DN
```
cn=admin,ou=people,dc=example,dc=com
```
### Password
```
admin-password
```
Make sure the box `Automatically create users in Airsonic` is checked.
Restart airsonic-advanced

View File

@@ -0,0 +1,18 @@
# Configuration for Apereo CAS Server
Replace `dc=example,dc=com` with your LLDAP configured domain, and hostname for your LLDAP server.
The `search-filter` provided here requires users to be members of the `cas_auth` group in LLDAP.
Configuration to use LDAP in e.g. `/etc/cas/config/standalone.yml`
```
cas:
authn:
ldap:
- base-dn: dc=example,dc=com
bind-credential: password
bind-dn: uid=admin,ou=people,dc=example,dc=com
ldap-url: ldap://ldap.example.com:3890
search-filter: (&(objectClass=person)(memberOf=uid=cas_auth,ou=groups,dc=example,dc=com))
```

View File

@@ -30,11 +30,11 @@ authentication_backend:
additional_users_dn: ou=people
# To allow sign in both with username and email, one can use a filter like
# (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person))
users_filter: (&({username_attribute}={input})(objectClass=person))
users_filter: "(&({username_attribute}={input})(objectClass=person))"
# Set this to ou=groups, because all groups are stored in this ou
additional_groups_dn: ou=groups
# Only this filter is supported right now
groups_filter: (member={dn})
# The groups are not displayed in the UI, but this filter works.
groups_filter: "(member={dn})"
# The attribute holding the name of the group.
group_name_attribute: cn
# Email attribute

View File

@@ -0,0 +1,105 @@
# Name
```
lldap
```
# Slug
```
lldap
```
- [x] Enabled
- [x] Sync Users
- [x] User password writeback
- [x] Sync groups
# Connection settings
## Server URI
```
ldap://lldap:3890
```
- [ ] Enable StartTLS
## TLS Verification Certificate
```
---------
```
## Bind CN
```
uid=admin,ou=people,dc=example,dc=com
```
## Bind Password
```
ADMIN_PASSWORD
```
## Base DN
```
dc=example,dc=com
```
# LDAP Attribute mapping
## User Property Mappings
- [x] authentik default LDAP Mapping: mail
- [x] authentik default LDAP Mapping: Name
- [x] authentik default Active Directory Mapping: givenName
- [ ] authentik default Active Directory Mapping: sAMAccountName
- [x] authentik default Active Directory Mapping: sn
- [ ] authentik default Active Directory Mapping: userPrincipalName
- [x] authentik default OpenLDAP Mapping: cn
- [x] authentik default OpenLDAP Mapping: uid
## Group Property Mappings
- [ ] authentik default LDAP Mapping: mail
- [ ] authentik default LDAP Mapping: Name
- [ ] authentik default Active Directory Mapping: givenName
- [ ] authentik default Active Directory Mapping: sAMAccountName
- [ ] authentik default Active Directory Mapping: sn
- [ ] authentik default Active Directory Mapping: userPrincipalName
- [x] authentik default OpenLDAP Mapping: cn
- [ ] authentik default OpenLDAP Mapping: uid
# Additional settings
## Group
```
---------
```
## User path
```
LDAP/users
```
## Addition User DN
```
ou=people
```
## Addition Group DN
```
ou=groups
```
## User object filter
```
(objectClass=person)
```
## Group object filter
```
(objectClass=groupOfUniqueNames)
```
## Group membership field
```
member
```
## Object uniqueness field
```
uid
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -0,0 +1,254 @@
# Bootstrapping lldap using [bootstrap.sh](bootstrap.sh) script
bootstrap.sh allows managing your lldap in a git-ops, declarative way using JSON config files.
The script can:
* create, update users
* set/update all lldap built-in user attributes
* add/remove users to/from corresponding groups
* set/update user avatar from file, link or from gravatar by user email
* set/update user password
* create groups
* delete redundant users and groups (when `DO_CLEANUP` env var is true)
* maintain the desired state described in JSON config files
![](bootstrap-example-log-1.jpeg)
## Required packages
> The script will automatically install the required packages for alpine and debian-based distributions
> when run by root, or you can install them by yourself.
- curl
- [jq](https://github.com/jqlang/jq)
- [jo](https://github.com/jpmens/jo)
## Environment variables
- `LLDAP_URL` or `LLDAP_URL_FILE` - URL to your lldap instance or path to file that contains URL (**MANDATORY**)
- `LLDAP_ADMIN_USERNAME` or `LLDAP_ADMIN_USERNAME_FILE` - admin username or path to file that contains username (**MANDATORY**)
- `LLDAP_ADMIN_PASSWORD` or `LLDAP_ADMIN_PASSWORD_FILE` - admin password or path to file that contains password (**MANDATORY**)
- `USER_CONFIGS_DIR` (default value: `/user-configs`) - directory where the user JSON configs could be found
- `GROUP_CONFIGS_DIR` (default value: `/group-configs`) - directory where the group JSON configs could be found
- `LLDAP_SET_PASSWORD_PATH` - path to the `lldap_set_password` utility (default value: `/app/lldap_set_password`)
- `DO_CLEANUP` (default value: `false`) - delete groups and users not specified in config files, also remove users from groups that they do not belong to
## Config files
There are two types of config files: [group](#group-config-file-example) and [user](#user-config-file-example) configs.
Each config file can be as one JSON file with nested JSON top-level values as several JSON files.
### Group config file example
Group configs are used to define groups that will be created by the script
Fields description:
* `name`: name of the group (**MANDATORY**)
```json
{
"name": "group-1"
}
{
"name": "group-2"
}
```
### User config file example
User config defines all the lldap user structures,
if the non-mandatory field is omitted, the script will clean this field in lldap as well.
Fields description:
* `id`: it's just username (**MANDATORY**)
* `email`: self-explanatory (**MANDATORY**)
* `password`: would be used to set the password using `lldap_set_password` utility
* `displayName`: self-explanatory
* `firstName`: self-explanatory
* `lastName`: self-explanatory
* `avatar_file`: must be a valid path to jpeg file (ignored if `avatar_url` specified)
* `avatar_url`: must be a valid URL to jpeg file (ignored if `gravatar_avatar` specified)
* `gravatar_avatar` (`false` by default): the script will try to get an avatar from [gravatar](https://gravatar.com/) by previously specified `email` (has the highest priority)
* `weserv_avatar` (`false` by default): avatar file from `avatar_url` or `gravatar_avatar` would be converted to jpeg using [wsrv.nl](https://wsrv.nl) (useful when your avatar is png)
* `groups`: an array of groups the user would be a member of (all the groups must be specified in group config files)
```json
{
"id": "username",
"email": "username@example.com",
"password": "changeme",
"displayName": "Display Name",
"firstName": "First",
"lastName": "Last",
"avatar_file": "/path/to/avatar.jpg",
"avatar_url": "https://i.imgur.com/nbCxk3z.jpg",
"gravatar_avatar": "false",
"weserv_avatar": "false",
"groups": [
"group-1",
"group-2"
]
}
```
## Usage example
### Manually
The script can be run manually in the terminal for initial bootstrapping of your lldap instance.
You should make sure that the [required packages](#required-packages) are installed
and the [environment variables](#environment-variables) are configured properly.
```bash
export LLDAP_URL=http://localhost:8080
export LLDAP_ADMIN_USERNAME=admin
export LLDAP_ADMIN_PASSWORD=changeme
export USER_CONFIGS_DIR="$(realpath ./configs/user)"
export GROUP_CONFIGS_DIR="$(realpath ./configs/group)"
export LLDAP_SET_PASSWORD_PATH="$(realpath ./lldap_set_password)"
export DO_CLEANUP=false
./bootstrap.sh
```
### Docker compose
Let's suppose you have the next file structure:
```text
./
├─ docker-compose.yaml
└─ bootstrap
├─ bootstrap.sh
└─ user-configs
│ ├─ user-1.json
│ ├─ ...
│ └─ user-n.json
└─ group-configs
├─ group-1.json
├─ ...
└─ group-n.json
```
You should mount `bootstrap` dir to lldap container and set the corresponding `env` variables:
```yaml
version: "3"
services:
lldap:
image: lldap/lldap:v0.5.0
volumes:
- ./bootstrap:/bootstrap
ports:
- "3890:3890" # For LDAP
- "17170:17170" # For the web front-end
environment:
# envs required for lldap
- LLDAP_LDAP_USER_EMAIL=admin@example.com
- LLDAP_LDAP_USER_PASS=changeme
- LLDAP_LDAP_BASE_DN=dc=example,dc=com
# envs required for bootstrap.sh
- LLDAP_URL=http://localhost:17170
- LLDAP_ADMIN_USERNAME=admin
- LLDAP_ADMIN_PASSWORD=changeme # same as LLDAP_LDAP_USER_PASS
- USER_CONFIGS_DIR=/bootstrap/user-configs
- GROUP_CONFIGS_DIR=/bootstrap/group-configs
- DO_CLEANUP=false
```
Then, to bootstrap your lldap just run `docker compose exec lldap /bootstrap/bootstrap.sh`.
If config files were changed, re-run the `bootstrap.sh` with the same command.
### Kubernetes job
```yaml
apiVersion: batch/v1
kind: Job
metadata:
name: lldap-bootstrap
# Next annotations are required if the job managed by Argo CD,
# so Argo CD can relaunch the job on every app sync action
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: lldap-bootstrap
image: lldap/lldap:v0.5.0
command:
- /bootstrap/bootstrap.sh
env:
- name: LLDAP_URL
value: "http://lldap:8080"
- name: LLDAP_ADMIN_USERNAME
valueFrom: { secretKeyRef: { name: lldap-admin-user, key: username } }
- name: LLDAP_ADMIN_PASSWORD
valueFrom: { secretKeyRef: { name: lldap-admin-user, key: password } }
- name: DO_CLEANUP
value: "true"
volumeMounts:
- name: bootstrap
mountPath: /bootstrap/bootstrap.sh
subPath: bootstrap.sh
- name: user-configs
mountPath: /user-configs
readOnly: true
- name: group-configs
mountPath: /group-configs
readOnly: true
volumes:
- name: bootstrap
configMap:
name: bootstrap
defaultMode: 0555
items:
- key: bootstrap.sh
path: bootstrap.sh
- name: user-configs
projected:
sources:
- secret:
name: lldap-admin-user
items:
- key: user-config.json
path: admin-config.json
- secret:
name: lldap-password-manager-user
items:
- key: user-config.json
path: password-manager-config.json
- secret:
name: lldap-bootstrap-configs
items:
- key: user-configs.json
path: user-configs.json
- name: group-configs
projected:
sources:
- secret:
name: lldap-bootstrap-configs
items:
- key: group-configs.json
path: group-configs.json
```

View File

@@ -0,0 +1,490 @@
#!/usr/bin/env bash
set -e
set -o pipefail
LLDAP_URL="${LLDAP_URL}"
LLDAP_ADMIN_USERNAME="${LLDAP_ADMIN_USERNAME}"
LLDAP_ADMIN_PASSWORD="${LLDAP_ADMIN_PASSWORD}"
USER_CONFIGS_DIR="${USER_CONFIGS_DIR:-/user-configs}"
GROUP_CONFIGS_DIR="${GROUP_CONFIGS_DIR:-/group-configs}"
LLDAP_SET_PASSWORD_PATH="${LLDAP_SET_PASSWORD_PATH:-/app/lldap_set_password}"
DO_CLEANUP="${DO_CLEANUP:-false}"
check_install_dependencies() {
local commands=('curl' 'jq' 'jo')
local commands_not_found='false'
if ! hash "${commands[@]}" 2>/dev/null; then
if hash 'apk' 2>/dev/null && [[ $EUID -eq 0 ]]; then
apk add "${commands[@]}"
elif hash 'apt' 2>/dev/null && [[ $EUID -eq 0 ]]; then
apt update -yqq
apt install -yqq "${commands[@]}"
else
local command=''
for command in "${commands[@]}"; do
if ! hash "$command" 2>/dev/null; then
printf 'Command not found "%s"\n' "$command"
fi
done
commands_not_found='true'
fi
fi
if [[ "$commands_not_found" == 'true' ]]; then
return 1
fi
}
check_required_env_vars() {
local env_var_not_specified='false'
local dual_env_vars_list=(
'LLDAP_URL'
'LLDAP_ADMIN_USERNAME'
'LLDAP_ADMIN_PASSWORD'
)
local dual_env_var_name=''
for dual_env_var_name in "${dual_env_vars_list[@]}"; do
local dual_env_var_file_name="${dual_env_var_name}_FILE"
if [[ -z "${!dual_env_var_name}" ]] && [[ -z "${!dual_env_var_file_name}" ]]; then
printf 'Please specify "%s" or "%s" variable!\n' "$dual_env_var_name" "$dual_env_var_file_name" >&2
env_var_not_specified='true'
else
if [[ -n "${!dual_env_var_file_name}" ]]; then
declare -g "$dual_env_var_name"="$(cat "${!dual_env_var_file_name}")"
fi
fi
done
if [[ "$env_var_not_specified" == 'true' ]]; then
return 1
fi
}
check_configs_validity() {
local config_file='' config_invalid='false'
for config_file in "$@"; do
local error=''
if ! error="$(jq '.' -- "$config_file" 2>&1 >/dev/null)"; then
printf '%s: %s\n' "$config_file" "$error"
config_invalid='true'
fi
done
if [[ "$config_invalid" == 'true' ]]; then
return 1
fi
}
auth() {
local url="$1" admin_username="$2" admin_password="$3"
local response
response="$(curl --silent --request POST \
--url "$url/auth/simple/login" \
--header 'Content-Type: application/json' \
--data "$(jo -- username="$admin_username" password="$admin_password")")"
TOKEN="$(printf '%s' "$response" | jq --raw-output .token)"
}
make_query() {
local query_file="$1" variables_file="$2"
curl --silent --request POST \
--url "$LLDAP_URL/api/graphql" \
--header "Authorization: Bearer $TOKEN" \
--header 'Content-Type: application/json' \
--data @<(jq --slurpfile variables "$variables_file" '. + {"variables": $variables[0]}' "$query_file")
}
get_group_list() {
local query='{"query":"query GetGroupList {groups {id displayName}}","operationName":"GetGroupList"}'
make_query <(printf '%s' "$query") <(printf '{}')
}
get_group_array() {
get_group_list | jq --raw-output '.data.groups[].displayName'
}
group_exists() {
if [[ "$(get_group_list | jq --raw-output --arg displayName "$1" '.data.groups | any(.[]; select(.displayName == $displayName))')" == 'true' ]]; then
return 0
else
return 1
fi
}
get_group_id() {
get_group_list | jq --raw-output --arg displayName "$1" '.data.groups[] | if .displayName == $displayName then .id else empty end'
}
create_group() {
local group_name="$1"
if group_exists "$group_name"; then
printf 'Group "%s" (%s) already exists\n' "$group_name" "$(get_group_id "$group_name")"
return
fi
# shellcheck disable=SC2016
local query='{"query":"mutation CreateGroup($name: String!) {createGroup(name: $name) {id displayName}}","operationName":"CreateGroup"}'
local response='' error=''
response="$(make_query <(printf '%s' "$query") <(jo -- name="$group_name"))"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'Group "%s" (%s) successfully created\n' "$group_name" "$(printf '%s' "$response" | jq --raw-output '.data.createGroup.id')"
fi
}
delete_group() {
local group_name="$1" id=''
if ! group_exists "$group_name"; then
printf '[WARNING] Group "%s" does not exist\n' "$group_name"
return
fi
id="$(get_group_id "$group_name")"
# shellcheck disable=SC2016
local query='{"query":"mutation DeleteGroupQuery($groupId: Int!) {deleteGroup(groupId: $groupId) {ok}}","operationName":"DeleteGroupQuery"}'
local response='' error=''
response="$(make_query <(printf '%s' "$query") <(jo -- groupId="$id"))"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'Group "%s" (%s) successfully deleted\n' "$group_name" "$id"
fi
}
get_user_details() {
local id="$1"
# shellcheck disable=SC2016
local query='{"query":"query GetUserDetails($id: String!) {user(userId: $id) {id email displayName firstName lastName creationDate uuid groups {id displayName}}}","operationName":"GetUserDetails"}'
make_query <(printf '%s' "$query") <(jo -- id="$id")
}
user_in_group() {
local user_id="$1" group_name="$2"
if ! group_exists "$group_name"; then
printf '[WARNING] Group "%s" does not exist\n' "$group_name"
return
fi
if ! user_exists "$user_id"; then
printf 'User "%s" is not exists\n' "$user_id"
return
fi
if [[ "$(get_user_details "$user_id" | jq --raw-output --arg displayName "$group_name" '.data.user.groups | any(.[]; select(.displayName == $displayName))')" == 'true' ]]; then
return 0
else
return 1
fi
}
add_user_to_group() {
local user_id="$1" group_name="$2" group_id=''
if ! group_exists "$group_name"; then
printf '[WARNING] Group "%s" does not exist\n' "$group_name"
return
fi
group_id="$(get_group_id "$group_name")"
if user_in_group "$user_id" "$group_name"; then
printf 'User "%s" already in group "%s" (%s)\n' "$user_id" "$group_name" "$group_id"
return
fi
# shellcheck disable=SC2016
local query='{"query":"mutation AddUserToGroup($user: String!, $group: Int!) {addUserToGroup(userId: $user, groupId: $group) {ok}}","operationName":"AddUserToGroup"}'
local response='' error=''
response="$(make_query <(printf '%s' "$query") <(jo -- user="$user_id" group="$group_id"))"
error="$(printf '%s' "$response" | jq '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'User "%s" successfully added to the group "%s" (%s)\n' "$user_id" "$group_name" "$group_id"
fi
}
remove_user_from_group() {
local user_id="$1" group_name="$2" group_id=''
if ! group_exists "$group_name"; then
printf '[WARNING] Group "%s" does not exist\n' "$group_name"
return
fi
group_id="$(get_group_id "$group_name")"
# shellcheck disable=SC2016
local query='{"operationName":"RemoveUserFromGroup","query":"mutation RemoveUserFromGroup($user: String!, $group: Int!) {removeUserFromGroup(userId: $user, groupId: $group) {ok}}"}'
local response='' error=''
response="$(make_query <(printf '%s' "$query") <(jo -- user="$user_id" group="$group_id"))"
error="$(printf '%s' "$response" | jq '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'User "%s" successfully removed from the group "%s" (%s)\n' "$user_id" "$group_name" "$group_id"
fi
}
get_users_list() {
# shellcheck disable=SC2016
local query='{"query": "query ListUsersQuery($filters: RequestFilter) {users(filters: $filters) {id email displayName firstName lastName creationDate}}","operationName": "ListUsersQuery"}'
make_query <(printf '%s' "$query") <(jo -- filters=null)
}
user_exists() {
if [[ "$(get_users_list | jq --raw-output --arg id "$1" '.data.users | any(.[]; contains({"id": $id}))')" == 'true' ]]; then
return 0
else
return 1
fi
}
delete_user() {
local id="$1"
if ! user_exists "$id"; then
printf 'User "%s" is not exists\n' "$id"
return
fi
# shellcheck disable=SC2016
local query='{"query": "mutation DeleteUserQuery($user: String!) {deleteUser(userId: $user) {ok}}","operationName": "DeleteUserQuery"}'
local response='' error=''
response="$(make_query <(printf '%s' "$query") <(jo -- user="$id"))"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'User "%s" successfully deleted\n' "$id"
fi
}
__common_user_mutation_query() {
local \
query="$1" \
id="${2:-null}" \
email="${3:-null}" \
displayName="${4:-null}" \
firstName="${5:-null}" \
lastName="${6:-null}" \
avatar_file="${7:-null}" \
avatar_url="${8:-null}" \
gravatar_avatar="${9:-false}" \
weserv_avatar="${10:-false}"
local variables_arr=(
'-s' "id=$id"
'-s' "email=$email"
'-s' "displayName=$displayName"
'-s' "firstName=$firstName"
'-s' "lastName=$lastName"
)
local temp_avatar_file=''
if [[ "$gravatar_avatar" == 'true' ]]; then
avatar_url="https://gravatar.com/avatar/$(printf '%s' "$email" | sha256sum | cut -d ' ' -f 1)?size=512"
fi
if [[ "$avatar_url" != 'null' ]]; then
temp_avatar_file="${TMP_AVATAR_DIR}/$(printf '%s' "$avatar_url" | md5sum | cut -d ' ' -f 1)"
if ! [[ -f "$temp_avatar_file" ]]; then
if [[ "$weserv_avatar" == 'true' ]]; then
avatar_url="https://wsrv.nl/?url=$avatar_url&output=jpg"
fi
curl --silent --location --output "$temp_avatar_file" "$avatar_url"
fi
avatar_file="$temp_avatar_file"
fi
if [[ "$avatar_file" == 'null' ]]; then
variables_arr+=('-s' 'avatar=null')
else
variables_arr+=("avatar=%$avatar_file")
fi
make_query <(printf '%s' "$query") <(jo -- user=:<(jo -- "${variables_arr[@]}"))
}
create_user() {
local id="$1"
if user_exists "$id"; then
printf 'User "%s" already exists\n' "$id"
return
fi
# shellcheck disable=SC2016
local query='{"query":"mutation CreateUser($user: CreateUserInput!) {createUser(user: $user) {id creationDate}}","operationName":"CreateUser"}'
local response='' error=''
response="$(__common_user_mutation_query "$query" "$@")"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'User "%s" successfully created\n' "$id"
fi
}
update_user() {
local id="$1"
if ! user_exists "$id"; then
printf 'User "%s" is not exists\n' "$id"
return
fi
# shellcheck disable=SC2016
local query='{"query":"mutation UpdateUser($user: UpdateUserInput!) {updateUser(user: $user) {ok}}","operationName":"UpdateUser"}'
local response='' error=''
response="$(__common_user_mutation_query "$query" "$@")"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'User "%s" successfully updated\n' "$id"
fi
}
create_update_user() {
local id="$1"
if user_exists "$id"; then
update_user "$@"
else
create_user "$@"
fi
}
main() {
check_install_dependencies
check_required_env_vars
local user_config_files=("${USER_CONFIGS_DIR}"/*.json)
local group_config_files=("${GROUP_CONFIGS_DIR}"/*.json)
if ! check_configs_validity "${group_config_files[@]}" "${user_config_files[@]}"; then
exit 1
fi
until curl --silent -o /dev/null "$LLDAP_URL"; do
printf 'Waiting lldap to start...\n'
sleep 10
done
auth "$LLDAP_URL" "$LLDAP_ADMIN_USERNAME" "$LLDAP_ADMIN_PASSWORD"
local redundant_groups=''
redundant_groups="$(get_group_list | jq '[ .data.groups[].displayName ]' | jq --compact-output '. - ["lldap_admin","lldap_password_manager","lldap_strict_readonly"]')"
printf -- '\n--- groups ---\n'
local group_config=''
while read -r group_config; do
local group_name=''
group_name="$(printf '%s' "$group_config" | jq --raw-output '.name')"
create_group "$group_name"
redundant_groups="$(printf '%s' "$redundant_groups" | jq --compact-output --arg name "$group_name" '. - [$name]')"
done < <(jq --compact-output '.' -- "${group_config_files[@]}")
printf -- '--- groups ---\n'
printf -- '\n--- redundant groups ---\n'
if [[ "$redundant_groups" == '[]' ]]; then
printf 'There are no redundant groups\n'
else
local group_name=''
while read -r group_name; do
if [[ "$DO_CLEANUP" == 'true' ]]; then
delete_group "$group_name"
else
printf '[WARNING] Group "%s" is not declared in config files\n' "$group_name"
fi
done < <(printf '%s' "$redundant_groups" | jq --raw-output '.[]')
fi
printf -- '--- redundant groups ---\n'
local redundant_users=''
redundant_users="$(get_users_list | jq '[ .data.users[].id ]' | jq --compact-output --arg admin_id "$LLDAP_ADMIN_USERNAME" '. - [$admin_id]')"
TMP_AVATAR_DIR="$(mktemp -d)"
local user_config=''
while read -r user_config; do
local field='' id='' email='' displayName='' firstName='' lastName='' avatar_file='' avatar_url='' gravatar_avatar='' weserv_avatar='' password=''
for field in 'id' 'email' 'displayName' 'firstName' 'lastName' 'avatar_file' 'avatar_url' 'gravatar_avatar' 'weserv_avatar' 'password'; do
declare "$field"="$(printf '%s' "$user_config" | jq --raw-output --arg field "$field" '.[$field]')"
done
printf -- '\n--- %s ---\n' "$id"
create_update_user "$id" "$email" "$displayName" "$firstName" "$lastName" "$avatar_file" "$avatar_url" "$gravatar_avatar" "$weserv_avatar"
redundant_users="$(printf '%s' "$redundant_users" | jq --compact-output --arg id "$id" '. - [$id]')"
if [[ "$password" != 'null' ]] && [[ "$password" != '""' ]]; then
"$LLDAP_SET_PASSWORD_PATH" --base-url "$LLDAP_URL" --token "$TOKEN" --username "$id" --password "$password"
fi
local redundant_user_groups=''
redundant_user_groups="$(get_user_details "$id" | jq '[ .data.user.groups[].displayName ]')"
local group=''
while read -r group; do
if [[ -n "$group" ]]; then
add_user_to_group "$id" "$group"
redundant_user_groups="$(printf '%s' "$redundant_user_groups" | jq --compact-output --arg group "$group" '. - [$group]')"
fi
done < <(printf '%s' "$user_config" | jq --raw-output '.groups | if . == null then "" else .[] end')
local user_group_name=''
while read -r user_group_name; do
if [[ "$DO_CLEANUP" == 'true' ]]; then
remove_user_from_group "$id" "$user_group_name"
else
printf '[WARNING] User "%s" is not declared as member of the "%s" group in the config files\n' "$id" "$user_group_name"
fi
done < <(printf '%s' "$redundant_user_groups" | jq --raw-output '.[]')
printf -- '--- %s ---\n' "$id"
done < <(jq --compact-output '.' -- "${user_config_files[@]}")
rm -r "$TMP_AVATAR_DIR"
printf -- '\n--- redundant users ---\n'
if [[ "$redundant_users" == '[]' ]]; then
printf 'There are no redundant users\n'
else
local id=''
while read -r id; do
if [[ "$DO_CLEANUP" == 'true' ]]; then
delete_user "$id"
else
printf '[WARNING] User "%s" is not declared in config files\n' "$id"
fi
done < <(printf '%s' "$redundant_users" | jq --raw-output '.[]')
fi
printf -- '--- redundant users ---\n'
}
main "$@"

View File

@@ -0,0 +1,57 @@
# Configuration for Dell iDRAC
## iDRAC 9
iDRAC 9 can only be connected to LDAPS, so make sure you have that enabled.
The settings then are as follows:
### Use Distinguished Name to Search Group Membership
```
Enabled
```
### LDAP Server Address
```
Your server address eg. localhost
```
### LDAP Server Port
```
Your LDAPS port, eg. 6360 or 636
```
### Bind DN
```
uid=admin,ou=people,dc=example,dc=com
```
### Bind Password
```
Enabled
```
### Bind Password
```
Your admin user password
```
### Attribute of User Login
```
uid
```
### Attribute of Group Membership
```
member
```
### Search Filter
```
(&(objectClass=person)(memberof=cn=idrac_users,ou=groups,dc=example,dc=com))
```
For the Group Role Mappings, you define groups by their full `Group DN`, eg.
```
cn=idrac_users,ou=groups,dc=example,dc=com
```

View File

@@ -0,0 +1,32 @@
# lldap configuration:
# LLDAP_LDAP_BASE_DN: dc=example,dc=com
# ##############################
# rest of the Dex options
# ##############################
connectors:
- type: ldap
id: ldap
name: LDAP
config:
host: lldap-host # make sure it does not start with `ldap://`
port: 3890 # or 6360 if you have ldaps enabled
insecureNoSSL: true # or false if you have ldaps enabled
insecureSkipVerify: true # or false if you have ldaps enabled
bindDN: uid=admin,ou=people,dc=example,dc=com # replace admin with your admin user
bindPW: very-secure-password # replace with your admin password
userSearch:
baseDN: ou=people,dc=example,dc=com
username: uid
idAttr: uid
emailAttr: mail
nameAttr: displayName
preferredUsernameAttr: uid
groupSearch:
baseDN: ou=groups,dc=example,dc=com
filter: "(objectClass=groupOfUniqueNames)"
userMatchers:
- userAttr: DN
groupAttr: member
nameAttr: cn

View File

@@ -0,0 +1,26 @@
# Configuration for dokuwiki
LDAP configuration is in ```/dokuwiki/conf/local.protected.php```:
```
<?php
$conf['useacl'] = 1; //enable ACL
$conf['authtype'] = 'authldap'; //enable this Auth plugin
$conf['superuser'] = 'admin';
$conf['plugin']['authldap']['server'] = 'ldap://lldap_server:3890'; #IP of your lldap
$conf['plugin']['authldap']['usertree'] = 'ou=people,dc=example,dc=com';
$conf['plugin']['authldap']['grouptree'] = 'ou=groups, dc=example, dc=com';
$conf['plugin']['authldap']['userfilter'] = '(&(uid=%{user})(objectClass=person))';
$conf['plugin']['authldap']['groupfilter'] = '(&(member=%{dn})(objectClass=groupOfUniqueNames))';
$conf['plugin']['authldap']['attributes'] = array('cn', 'displayname', 'mail', 'givenname', 'objectclass', 'sn', 'uid', 'memberof');
$conf['plugin']['authldap']['version'] = 3;
$conf['plugin']['authldap']['binddn'] = 'cn=admin,ou=people,dc=example,dc=com';
$conf['plugin']['authldap']['bindpw'] = 'ENTER_YOUR_LLDAP_PASSWORD';
```
DokuWiki by default, ships with an LDAP Authentication Plugin called ```authLDAP``` that allows authentication against an LDAP directory.
All you need to do is to activate the plugin. This can be done on the DokuWiki Extensions Manager.
Once the LDAP settings are defined, proceed to define the default authentication method.
Navigate to Table of Contents > DokuWiki > Authentication.
On the Authentication backend, select ```authldap``` and save the changes.

View File

@@ -0,0 +1,30 @@
# Basic LDAP auth for a Ejabberd XMPP server
[Main documentation here.](https://docs.ejabberd.im/admin/configuration/ldap/)
For simple user auth add this to main ejabberd.yml:
```
host_config:
xmpp.example.org:
auth_method: [ldap]
ldap_servers:
- 127.0.0.1 #IP or hostname of LLDAP server
ldap_port: 3890
ldap_uids:
- uid
ldap_rootdn: "uid=lldap_readonly,ou=people,dc=example,dc=org"
ldap_password: "secret"
ldap_base: "ou=people,dc=example,dc=org"
```
## vCard from LDAP
Theoretically possible, [see the documentation.](https://docs.ejabberd.im/admin/configuration/ldap/#vcard-in-ldap)
TODO
## Shared roster groups from LDAP
Theoretically possible, [see the documentation.](https://docs.ejabberd.im/admin/configuration/ldap/#shared-roster-in-ldap)
TODO

22
example_configs/ergo.md Normal file
View File

@@ -0,0 +1,22 @@
# Basic LDAP auth for an Ergo IRC server
[Main documentation here.](https://github.com/ergochat/ergo-ldap)
For simple user auth prepare a ldap-config.yaml with the following settings
```
host: "127.0.0.1"
port: 3890
timeout: 30s
# uncomment for TLS / LDAPS:
# use-ssl: true
bind-dn: "uid=%s,ou=people,dc=example,dc=org"
```
Then add the compiled ergo-ldap program to your Ergo folder and make sure it can be executed by the same user your Ergo IRCd runs as.
Follow the instructions in the main Ergo config file's accounts section on how to execute an external auth program.
Make sure SASL auth is enabled and then restart Ergo to enable LDAP linked SASL auth.

View File

@@ -1,4 +1,4 @@
# Configuration for Gitea
# Configuration for Gitea (& Forgejo)
In Gitea, go to `Site Administration > Authentication Sources` and click `Add Authentication Source`
Select `LDAP (via BindDN)`
@@ -14,9 +14,36 @@ To log in they can either use their email address or user name. If you only want
For more info on the user filter, see: https://docs.gitea.io/en-us/authentication/#ldap-via-binddn
* Admin Filter: Use `(memberof=cn=lldap_admin,ou=groups,dc=example,dc=com)` if you want lldap admins to become Gitea admins. Leave empty otherwise.
* Username Attribute: `uid`
* First Name Attribute: `givenName`
* Surname Attribute: `sn`
* Email Attribute: `mail`
* Avatar Attribute: `jpegPhoto`
* Check `Enable User Synchronization`
Replace every instance of `dc=example,dc=com` with your configured domain.
After applying the above settings, users should be able to log in with either their user name or email address.
After applying the above settings, users should be able to log in with either their user name or email address.
## Syncronizing LDAP groups with existing teams in organisations
Groups in LLDAP can be syncronized with teams in organisations. Organisations and teams must be created manually in Gitea.
It is possible to syncronize one LDAP group with multiple teams in a Gitea organization.
Check `Enable LDAP Groups`
* Group Search Base DN: `ou=groups,dc=example,dc=com`
* Group Attribute Containing List Of Users: `member`
* User Attribute Listed In Group: `dn`
* Map LDAP groups to Organization teams: `{"cn=Groupname1,ou=groups,dc=example,dc=com":{"Organization1": ["Teamname"]},"cn=Groupname2,ou=groups,dc=example,dc=com": {"Organization2": ["Teamname1", "Teamname2"]}}`
Check `Remove Users from syncronised teams...`
The `Map LDAP groups to Organization teams` config is JSON formatted and can be extended to as many groups as needed.
Replace every instance of `dc=example,dc=com` with your configured domain.
# Configuration for Gitea in `simple auth` mode
* The configuration method is the same as `BindDN` mode.
* `BindDN` and `password` are not required
* Gitea will not be able to pre-sync users, user account will be created at login time.

30
example_configs/gitlab.md Normal file
View File

@@ -0,0 +1,30 @@
# GitLab Configuration
Members of the group ``git_user`` will have access to GitLab.
Edit ``/etc/gitlab/gitlab.rb``:
```ruby
gitlab_rails['ldap_enabled'] = true
gitlab_rails['ldap_servers'] = {
'main' => {
'label' => 'LDAP',
'host' => 'ldap.example.com',
'port' => 3890,
'uid' => 'uid',
'base' => 'ou=people,dc=example,dc=com',
'encryption' => 'plain',
'bind_dn' => 'uid=bind_user,ou=people,dc=example,dc=com',
'password' => '<bind user password>',
'active_directory' => false,
'user_filter' => '(&(objectclass=person)(memberof=cn=git_user,ou=groups,dc=example,dc=com))',
'attributes' => {
'username' => 'uid',
'email' => 'mail',
'name' => 'displayName',
'first_name' => 'givenName',
'last_name' => 'sn'
}
}
}
```

View File

@@ -20,7 +20,7 @@ ssl_skip_verify = false
# client_key = "/path/to/client.key"
# Search user bind dn
bind_dn = "cn=<your grafana user>,ou=people,dc=example,dc=org"
bind_dn = "uid=<your grafana user>,ou=people,dc=example,dc=org"
# Search user bind password
# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
bind_password = "<grafana user password>"
@@ -37,13 +37,13 @@ search_base_dns = ["dc=example,dc=org"]
[servers.attributes]
member_of = "memberOf"
email = "mail"
name = "givenName"
name = "displayName"
surname = "sn"
username = "uid"
# If you want to map your ldap groups to grafana's groups, see: https://grafana.com/docs/grafana/latest/auth/ldap/#group-mappings
# As a quick example, here is how you would map lldap's admin group to grafana's admin
# [[servers.group_mappings]]
# group_dn = "cn=lldap_admin,ou=groups,c=example,dc=org"
# group_dn = "cn=lldap_admin,ou=groups,dc=example,dc=org"
# org_role = "Admin"
# grafana_admin = true

28
example_configs/grocy.md Normal file
View File

@@ -0,0 +1,28 @@
# Configuration for Grocy
Adjust the following values in the file `config/data/config.php` or add environment variables for them (prefixed with `GROCY_`).
NOTE: If the environment variables are not working (for example in the linuxserver.io Docker Image), you need to add `clear_env = no` under the `[www]` in `/config/php/www2.conf`.
Replace `dc=example,dc=com` with your LLDAP configured domain.
### AUTH_CLASS
Needs to be set to `Grocy\Middleware\LdapAuthMiddleware` in order to use LDAP
### LDAP_ADDRESS
The address of your ldap server, eg: `ldap://lldap.example.com:389`
### LDAP_BASE_DN
The base dn, usually points directly to the `people`, eg: `ou=people,dc=example,dc=com`
### LDAP_BIND_DN
The reader user for lldap, eg: `uid=ldap-reader,ou=people,dc=example,dc=com`
### LDAP_BIND_PW
The password for the reader user
### LDAP_USER_FILTER
The filter to use for the users, eg. for a separate group: `(&(objectClass=person)(memberof=cn=grocy_users,ou=groups,dc=example,dc=com))`
### LDAP_UID_ATTR
The user id attribute, should be `uid`

View File

@@ -0,0 +1,16 @@
# Configuration for hedgedoc
[Hedgedoc](https://hedgedoc.org/) is a platform to write and share markdown.
### Using docker variables
Any member of the group ```hedgedoc``` can log into hedgedoc.
```
- CMD_LDAP_URL=ldap://lldap:3890
- CMD_LDAP_BINDDN=uid=admin,ou=people,dc=example,dc=com
- CMD_LDAP_BINDCREDENTIALS=insert_your_password
- CMD_LDAP_SEARCHBASE=ou=people,dc=example,dc=com
- CMD_LDAP_SEARCHFILTER=(&(memberOf=cn=hedgedoc,ou=groups,dc=example,dc=com)(uid={{username}}))
- CMD_LDAP_USERIDFIELD=uid
```
Replace `dc=example,dc=com` with your LLDAP configured domain for all occurances

View File

@@ -0,0 +1,35 @@
# Home Assistant Configuration
Home Assistant configures ldap auth via the [Command Line Auth Provider](https://www.home-assistant.io/docs/authentication/providers/#command-line). The wiki mentions a script that can be used for LDAP authentication, but it doesn't work in the container version (it is lacking both `ldapsearch` and `curl` ldap protocol support). Thankfully LLDAP has a graphql API to save the day!
## Graphql-based Auth Script
The [auth script](lldap-ha-auth.sh) attempts to authenticate a user against an LLDAP server, using credentials provided via `username` and `password` environment variables. The first argument must be the URL of your LLDAP server, accessible from Home Assistant. You can provide an additional optional argument to confine allowed logins to a single group. The script will output the user's display name as the `name` variable, if not empty.
1. Copy the [auth script](lldap-ha-auth.sh) to your home assistant instance. In this example, we use `/config/lldap-ha-auth.sh`.
- Set the script as executable by running `chmod +x /config/lldap-ha-auth.sh`
2. Add the following to your configuration.yaml in Home assistant:
```yaml
homeassistant:
auth_providers:
# Ensure you have the homeassistant provider enabled if you want to continue using your existing accounts
- type: homeassistant
- type: command_line
command: /config/lldap-ha-auth.sh
# arguments: [<LDAP Host>, <regular user group>, <admin user group>, <local user group>]
# <regular user group>: Find users that has permission to access homeassistant, anyone inside
# this group will have the default 'system-users' permission in homeassistant.
#
# <admin user group>: Allow users in the <regular user group> to be assigned into 'system-admin' group.
# Anyone inside this group will not have the 'system-users' permission as only one permission group
# is allowed in homeassistant
#
# <local user group>: Users in the <local user group> (e.g., 'homeassistant_local') can only access
# homeassistant inside LAN network.
#
# Only the first argument is required. ["https://lldap.example.com"] allows all users to log in from
# anywhere and have 'system-users' permissions.
args: ["https://lldap.example.com", "homeassistant_user", "homeassistant_admin", "homeassistant_local"]
meta: true
```
3. Reload your config or restart Home Assistant

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

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