Compare commits

...

278 Commits

Author SHA1 Message Date
dependabot[bot] 155bda6bbf build(deps): bump actions/download-artifact from 7 to 8
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-27 22:01:45 +01:00
dependabot[bot] 7d1593e266 build(deps): bump actions/upload-artifact from 6 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-27 18:48:41 +01:00
Sertonix 8c8df11250 cargo: Update wasm-bindgen and lettre 2026-02-24 14:17:17 +01:00
Santi Gonzalez aa1384939b example_config: fix HA for blank displayName 2026-02-19 00:15:12 +01:00
lyzstrik 6f94134fdc refactor(server): migrate to rustls 0.23 and centralize TLS logic (#1389)
This commit upgrades the TLS stack to Rustls 0.23

Key changes:
- Dependencies: Updated 'rustls' (v0.23), 'tokio-rustls' (v0.26), and 'actix-web' (v4.12.1).
- Build Fix: Configured 'rustls' to use the 'ring' provider (disabling default 'aws-lc-rs') to ensure ARMv7 compatibility.
- Refactor: Created 'server/src/tls.rs' to handle certificate loading (DRY).
- LDAP: Updated 'ldap_server.rs' to use the new TLS module and Rustls APIs.
- Healthcheck: Updated 'healthcheck.rs' to use Rustls 0.23 types.
2026-01-31 09:47:11 +01:00
Valentin Tolmer d1904a2759 readme: Add a link to TrueNAS installation guide 2026-01-31 09:42:12 +01:00
Asher Densmore-Lynn 02d92c3261 example_configs: Add Apache WebDAV 2026-01-31 09:41:53 +01:00
Michael Reid 48058540ec example_configs: Installing and Configuring LLDAP on TrueNAS 2026-01-31 09:36:18 +01:00
Copilot 618e3f3062 Fix cn attribute case-insensitive matching in LDAP equality filters (#1363) 2026-01-31 09:34:10 +01:00
jakob42 cafd3732f0 example_configs: add Continuwuity 2026-01-23 13:51:26 +01:00
dependabot[bot] 8588d4b851 build(deps): bump actions/checkout from 6.0.1 to 6.0.2
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.1 to 6.0.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/v6.0.1...v6.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-22 21:28:51 +01:00
Hobbabobba 2f70e2e31f example_configs: add Semaphore 2026-01-13 19:09:36 +01:00
lwintermelon a9d04b6bdf example_config: Add gerrit 2026-01-09 08:02:06 +01:00
Osi Bluber c03f3b5498 docs(bootstrap): add password_file for user configs 2026-01-07 18:45:32 +01:00
Copilot ac55dfedc4 app: Remove password length validation from login form 2026-01-06 23:37:01 +01:00
josef 62ae1d73fa app: asterisk for mail attribute when creating a user 2025-12-24 22:53:17 +01:00
Valentin Tolmer 469f35c12c cargo: Update dependencies 2025-12-24 15:33:30 +01:00
dependabot[bot] ee9fec71a5 build(deps): bump actions/upload-artifact from 4 to 6
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-12 23:23:32 +01:00
dependabot[bot] 9cbb0c99e2 build(deps): bump actions/download-artifact from 6 to 7
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-12 23:00:34 +01:00
dependabot[bot] 81e985df48 build(deps): bump actions/cache from 4 to 5
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [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/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-12 23:00:12 +01:00
Robert Cambridge a136a68bf4 example_configs: add sample group query to Grafana guide 2025-12-04 18:09:08 +01:00
dependabot[bot] 8f0022a9f1 build(deps): bump actions/checkout from 6.0.0 to 6.0.1
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.0 to 6.0.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/v6.0.0...v6.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-02 22:28:44 +01:00
dependabot[bot] fc7b33e4b3 build(deps): bump actions/checkout from 5.0.1 to 6.0.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.1 to 6.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/v5.0.1...v6.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-21 00:42:49 +01:00
dependabot[bot] a9b5147a30 build(deps): bump actions/download-artifact from 4 to 6
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 23:42:22 +01:00
dependabot[bot] 4de069452f build(deps): bump actions/checkout from 5.0.0 to 5.0.1
Bumps [actions/checkout](https://github.com/actions/checkout) from 5.0.0 to 5.0.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/v5.0.0...v5.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 22:47:42 +01:00
copilot-swe-agent[bot] e5c28a61d9 ldap: Fix LDAP base scope search to return NoSuchObject for non-existent entries
Added logic to return LdapResultCode::NoSuchObject (error 32) when a base scope
search returns no results, instead of returning Success with zero entries. This
aligns with RFC 4511 LDAP specification.
2025-11-16 15:20:06 +01:00
Valentin Tolmer c5e0441cae clippy: remove unnecessary elided lifetimes 2025-11-16 15:03:52 +01:00
Shawn Wilsher a959a50e07 server: allow specifying the healthcheck addresses
This change adds two new optional configuration options:
- `ldap_healthcheck_host` to pair with `ldap_host`
- `http_healthcheck_host` to pair with `http_host`

These both default to `localhost` to preserve the existing behavior.

Fixes #700
2025-11-16 15:03:40 +01:00
Tobias Jungel ab4389fc5f fix(bootstrap): set shopt nullglob
Set the `nullglob` option in the bootstrap script to handle cases where
no files match a glob pattern.

This prevents the following error when the folder exists without json
files:

```
/bootstrap/group-configs/*.json: jq: error: Could not open file /bootstrap/group-configs/*.json: No such file or directory
```
2025-11-09 22:35:50 +01:00
Tobias Jungel ddcbe383ab docs: Rename 'mail_alias' to 'mail-alias' in example config (#1346)
The example included an invalid character `_` for the attribute `name`

This resulted in:

```
Cannot create attribute with invalid name. Valid characters: a-z, A-Z, 0-9, and dash (-). Invalid chars found: _
```

This fixes the example by using a `-`.
2025-11-09 12:07:44 +01:00
Sören eee42502f3 docs: fix example_configs path
from ./example_configs to ../example_configs
2025-10-21 15:42:06 +02:00
thchha 660301eb5f example_configs: add initial gogs.md documentation
Gogs is the origin for common git forges so we add a documentation which
may be beneficial for other use cases where lldap should be used with.
It appears to be in mantenance mode - the current example may have to be
extended in the future.

We adapt the official documentation example configuration to integrate
lldap with the more elaborated example.
The reader may also be interested in a more simple example at
[upstream](https://github.com/gogs/gogs/blob/main/conf/auth.d/ldap_simple_auth.conf.example).
2025-10-21 00:07:46 +02:00
Nassim Bounouas 73f071ce89 docs: lldap password in docker install corrected 2025-10-18 12:44:59 +02:00
Copilot 28ef6e0c56 example_configs: mailserver,
fix outdated roundcube mounts and filters
2025-10-18 12:20:29 +02:00
Shawn Wilsher a32c8baa25 misc: improve vscode devcontainer experience
This change enables a better IDE experience in vscode by doing two
things:
1) Enables the rust-analyzer, which enables a bunch of features in
   vscode
2) Installs the needed deps for `cargo fmt` to work.
2025-10-14 11:54:48 +02:00
Copilot bf5b76269f server: Refactor config_overrides to use Option::inspect
To reduce cyclomatic complexity.
2025-10-12 20:14:20 +02:00
Hendrik Sievers c09e5c451c example_configs: update SSSD guide 2025-10-11 08:39:25 +02:00
Valentin Tolmer 1382c67de9 server: Extract configuration utilities 2025-10-10 23:28:35 +02:00
Copilot 0f8f9e1244 server: split up update_user_with_transaction 2025-10-10 09:01:52 +02:00
Webysther Sperandio 9a83e68667 app: Set a key for user/group creation buttons
That prevents them from jumping around when changing pages.
2025-10-10 00:28:11 +02:00
Copilot 3f9880ec11 server: Move LDAP search tests to their respective implementation files
Move user and group tests to their respective implementation files

User tests → core/user.rs:
- test_search_regular_user
- test_search_readonly_user
- test_search_member_of
- test_search_user_as_scope
- test_search_users
- test_pwd_changed_time_format

Group tests → core/group.rs:
- test_search_groups
- test_search_groups_by_groupid
- test_search_groups_filter
- test_search_groups_filter_2
- test_search_groups_filter_3
- test_search_group_as_scope

Tests remain in search.rs:
- DSE/schema tests
- General search logic tests
- Filter tests
- Error handling tests
- OU search tests
- Mixed user/group tests
2025-10-10 00:21:32 +02:00
Valentin Tolmer 94007aee58 readme: Add a link to the configuration guide's readme 2025-10-04 23:24:46 +02:00
Copilot 9e9d8e2ab5 graphql: split query.rs and mutation.rs into modular structures (#1311) 2025-10-04 23:09:36 +02:00
Lucas Sylvester 18edd4eb7d example_configs: update portainer group membership and filter attributes
The current descriptions is wrong, and will make portainer try to assign "group" to be a member of "group" instead of the assign the "user" to be a part of "group"
2025-10-04 22:16:00 +02:00
Jonas Resch 3cdf2241ea example_configs: Improve bootstrap.sh and documentation for use with Kubernetes (#1245) 2025-09-28 14:02:06 +02:00
thchha 9021066507 example_configs: Add configuration example for Open WebUI
This documents a working (LDAPS) configuration for using lldap in Open WebUI.

Environment Variables where directly taken from the logs.
The names of the GUI variables are taken from the UI.
Version v0.6.26.

The two configuration options are then put in a table and a small
elaboration + example values are provided.

Other then additionally mounting the ca chain into the container (with appropriate rights) there were not additional steps required.
The ownership of the ca chain will get changed to `chown 501:`.
2025-09-28 13:55:29 +02:00
Copilot fe063272bf chore: add Nix flake-based development environment
Co-authored-by: Kumpelinus <kumpelinus@jat.de>

- Add Nix flake and lockfile for reproducible development environments
- Document Nix-based setup in `docs/nix-development.md`
- Add `.envrc` for direnv integration and update `.gitignore` for Nix/direnv artifacts
- Reference Nix setup in CONTRIBUTING.md
2025-09-28 13:51:41 +02:00
RealSpinelle 59dee0115d example_configs: add missing fields to authentik example 2025-09-24 16:03:56 +02:00
Valentin Tolmer 622274cb1a chore: fix codecov config 2025-09-22 09:34:37 +02:00
Valentin Tolmer 4bad3a9e69 chore: reduce codecov verbosity 2025-09-22 01:01:00 +02:00
Copilot 84fb9b0fd2 Fix pwdChangedTime format to use LDAP GeneralizedTime instead of RFC3339 (#1300)
When querying for pwdChangedTime, the timestamp is returned in RFC3339 format instead of the expected LDAP GeneralizedTime format (YYYYMMDDHHMMSSZ). This causes issues when LLDAP is used with systems like Keycloak that expect proper LDAP timestamp formatting.
2025-09-22 00:42:51 +02:00
Valentin Tolmer 8a803bfb11 ldap: normalize base DN in LdapInfo, reduce memory usage
By making it a &'static, we can have a single allocation for all the threads/async contexts.

This also normalizes the whitespace from the user input; a trailing \n can cause weird issues with clients
2025-09-17 01:03:19 +02:00
Valentin Tolmer f7fe0c6ea0 ldap: fix swapped filter conditions 2025-09-16 14:58:46 +02:00
Valentin Tolmer 8f04843466 ldap: Simplify boolean expressions derived from filters 2025-09-16 01:58:41 +02:00
Hobbabobba 400beafb29 example_config: Add pocket-id 2025-09-16 01:40:08 +02:00
dependabot[bot] 963e58bf1a build(deps): bump tracing-subscriber from 0.3.18 to 0.3.20
Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.18 to 0.3.20.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.18...tracing-subscriber-0.3.20)

---
updated-dependencies:
- dependency-name: tracing-subscriber
  dependency-version: 0.3.20
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-16 01:10:06 +02:00
Kumpelinus 176c49c78d chore: upgrade Rust toolchain to 1.89 and modernize code with let-chains 2025-09-16 00:48:16 +02:00
Copilot 3d5542996f chore: Add CodeRabbit configuration to reduce agent verbosity 2025-09-16 00:12:45 +02:00
psentee 4590463cdf auth: serialize exp and iat claims as NumericDate to comply with RFC7519 (#1289)
Add `jti` claim to the JWT to avoid hashing collisions
2025-09-15 17:24:59 +02:00
lordratner 85ce481e32 Update opnsense.md
Added instruction for using/not using Constraint Groups. This option is selected by default and the current instructions do not address it, but if it is left on and the Authentication Containers are not updated, the group sync will fail.
2025-09-14 15:53:05 +02:00
Valentin Tolmer f64f8625f1 Add username to password recovey emails 2025-09-14 15:44:37 +02:00
Alexandre Foley c68f9e7cab example_configs: fix the quadlet readme
Several "podman" command should have been "systemctl" from the start.
2025-09-04 22:23:12 +02:00
Copilot 775c5c716d server: gracefully shut down database connection pool 2025-09-04 09:19:03 +02:00
Kumpelinus 89cb59919b server: Add modifyTimestamp and pwdChangedTime attributes (#1265)
Add a modifyTimestamp attribute to LDAP entries for users and groups, and expose pwdChangedTime for users.
These attributes let clients track when an entry (or its password) was last changed.

 -  modifyTimestamp is a server-maintained attribute that updates on any write to user or group entries, including membership changes (on the group side).

 -  pwdChangedTime is set when a user’s password is created or changed.
2025-08-31 14:56:07 +02:00
Valentin Tolmer 267f08f479 github: Remove CODEOWNERS 2025-08-21 22:11:35 +02:00
copilot-swe-agent[bot] b370360130 Add memberOf attribute definition to LDAP schema 2025-08-21 22:07:02 +02:00
Valentin Tolmer 7438fe92cf github: pin the CI rust version to 1.85.0 2025-08-21 02:24:05 +02:00
copilot-swe-agent[bot] cd2694d7dc Add comprehensive GitHub Copilot instructions for LLDAP repository
Add copilot-setup-steps.yml for GitHub Copilot agent environment setup
2025-08-21 01:22:31 +02:00
Valentin Tolmer 5e83ed8eb0 release: v0.6.2 2025-08-18 00:06:44 +02:00
Kirill Zhuravlev c69957690e docs: avoid bad-sounding words in secrets example 2025-08-17 23:10:45 +02:00
Linus Astel 7ef2af8beb devcontainer: Bump Rust version 2025-08-14 22:38:45 +02:00
Toby 5c9897b156 ldap: Add missing subschema entries 2025-08-14 16:04:28 +02:00
ibizaman 0b720aa082 bootstrap: fine grained cleanup 2025-08-13 09:36:21 +02:00
dependabot[bot] 3e7277e77d build(deps): bump actions/checkout from 4.2.2 to 5.0.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.2 to 5.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/v4.2.2...v5.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-12 08:02:59 +02:00
ibizaman 5241626a3a bootstrap: make password_file a standard custom attribute
Otherwise the bootstrap script tries to create the password_file
as a custom attribute which fails since it's not in the schema.
And anyway, it shouldn't be in the schema.
2025-08-06 22:13:22 +02:00
Valentin Tolmer 363ef106e2 app: Fix attribute type parsing 2025-07-30 01:02:47 +02:00
ibizaman 3c7e4c3dec bootstrap: do not leak password in process list 2025-07-22 08:51:35 +02:00
Valentin Tolmer fa196a9fd9 docker: try several GPG server
Sometimes the docker build fails because the gpg server is intermittently unavailable
2025-07-22 01:10:25 +02:00
ibizaman f02b365478 bootstrap: do not fail if no user or group defined 2025-07-21 23:35:49 +02:00
Valentin Tolmer 0b0e6ae2cd github: Fix warnings about Dockerfile syntax 2025-07-21 23:23:37 +02:00
Valentin Tolmer da525fc99b app: simplify attribute_type handling, display creation time in user details
In the user table it's still only the date, but that makes sense for an overview
2025-07-21 23:15:46 +02:00
ibizaman 78337bce72 bootstrap: allow to give password from a file 2025-07-16 23:51:21 +02:00
selfhoster1312 87e9311a44 meta: Fix cargo clippy failures (format strings) 2025-07-16 23:23:08 +02:00
Hendrik Sievers 53e62ecf5a docs: move authelia configuration to markdown file (#1205) 2025-07-13 22:29:09 +02:00
core 10d33a7537 readme: fix broken Iink 2025-07-11 00:52:03 +02:00
copilot-swe-agent[bot] ada438398e set-password: load system certificates
Fixes #1206
2025-07-08 22:46:13 +02:00
selfhoster1312 8c65d8958a docs: Add FAQ about sustainability and professional support contracts (#1200) 2025-07-06 23:55:08 +02:00
Toby f8cd7ad023 server, ldap: add support for Subschema requests (#1071)
Add a subschema entry to the rootDSE, which shows all attributes and
objectclasses present on the LLDAP server, which is needed for some
applications that need to index the LDAP server. The current
implementation's goal is to have a bare minimum working subschema which
follows the LDAP RFC. It also updates the GraphQL interface to follow
the changes that have been made in actually separating out
objectclasses, instead of having them as an attribute.

Co-authored-by: nitnelave <valentin@tolmer.fr>
2025-07-06 23:42:53 +02:00
selfhoster1312 823adcefd0 docs: Document (lack of) vhosts support (#1201) 2025-07-06 23:32:28 +02:00
selfhoster1312 5b120a5958 docs: Split README into smaller files (#1198) 2025-07-06 23:12:48 +02:00
Alexandre Foley c658666b3f example_configs: Quadlet documentation and improvements 2025-06-30 19:47:32 +02:00
PHIDIAS 7a5a88384d example_configs: add Mailcow (#1188) 2025-06-06 19:13:26 +02:00
Jona Joachim 4eb4fae49c example_configs: wiki_js: Add missing closing curly brackets to filter 2025-06-02 12:55:48 +02:00
Bryan Alves 58b028ad5f example_configs: fix Authelia OU for helm installations
Authelia when installed via helm by default looks for users
in the `Users` OU.  It supports changing this configuration via the
`additional_users_dn` property.  Set this to match what lldap expects.
2025-06-01 09:11:21 +02:00
Josh Thorpe 612bce48ad example_configs: fix overly-permissive next loud config
Any LLDAP user, not just members of nextcloud-users, could log in and get an instance. However, they weren't synchronized to the nextcloud admin view and thus were nearly invisible.
2025-05-30 10:23:07 +02:00
aokblast 1b5f6bfa66 example_configs: correct the manual for pkg support in FreeBSD 2025-05-19 20:28:23 +02:00
Nick 5913d81a44 chore: upgrade top level docker alpine version 2025-05-19 08:13:01 +02:00
sdelnevo cb9fd38271 example_configs: Add UnifiOS Directory 2025-05-17 23:34:53 +02:00
ChibyX 97bcfd1a99 example_configs: Add Peertube 2025-05-16 16:11:38 +02:00
Christian Medel 7330496a77 example_configs: Add Snipe-IT 2025-05-14 23:50:17 +09:00
MikaelUrankar 0baee7a120 Readme: Fix typo
s#LLDPA#LLDAP#g
2025-05-09 22:03:37 +09:00
broemp 0a5b2d4c46 example_configs: Change Grocy example config Port 2025-05-04 09:04:45 +09:00
cogdavid 9978111bec example_configs: fix dovecot example
ACCOUNT_PROVISIONER=LDAP is supported and indeed necessary for ldap integration - however quotas must be explicitly disabled with ENABLE_QUOTAS=0
2025-04-27 06:03:16 +09:00
Valentin Tolmer 8e25e9b2a4 app: Add a create user/group button at the top 2025-04-25 15:51:16 +09:00
Valentin Tolmer 4d6402c838 app: Fix email validation for groups
Fixes #1092
2025-04-25 15:36:43 +09:00
Valentin Tolmer b4f636ded9 server: Introduce True/False for filters
This should help clean up the filter debug representations
2025-04-25 15:25:26 +09:00
cogdavid 4018a6933c exmaple_configs: fix account provisioner in dovecot config
Updated ACCOUNT_PROVISIONER variable in environment directive. Change from ACCOUNT_PROVISIONER=LDAP to ACCOUNT_PROVISIONER=FILE.
2025-04-23 22:33:58 +09:00
Antonio Vargas bd29c7282d bootstrap: Include custom attributes when bootstrapping user and group configs (#1155)
* Include custom attributes when bootstrapping user and group configs

* Fix logic to detect user/group config without custom attributes

* Increase readability of query definition using a heredoc

* Remove duplicate query variables and improve readability

* Revert "Increase readability of query definition using a heredoc"

This reverts commit 7a73dacc21.
2025-04-21 16:07:11 +09:00
v-mod 1f89059c84 example_config: Add SSSD
* example_config: moving nslcd old guide to NSLCD.md

* example_config: creating README for SSSD

* example_config: creating sssd.conf

* example_config: removing explicit links and adding a reference to the old NSLCD guide

* example_config: fixing images in pam README

* example_config: add how to enable automatic home directory creation

* example_config: fixing typo in command to edit ssh config

* example_config: using commments instead of line numbers for the example sssd.conf file

---------

Co-authored-by: nitnelave <valentin@tolmer.fr>
2025-04-09 10:15:03 +09:00
Valentin Tolmer 74dbba0bdc server: dependency cleanup 2025-04-09 09:30:39 +09:00
Valentin Tolmer 3556e41612 server: flatten remaining files 2025-04-09 09:30:39 +09:00
Valentin Tolmer d38a2cd08b server: extract graphql crate 2025-04-09 09:30:39 +09:00
Valentin Tolmer db77a0f023 server: rename sql_backend_server -> sql_tcp_backend_server 2025-04-09 09:30:39 +09:00
Valentin Tolmer 3d61c209d2 server: small dependency cleanup 2025-04-09 09:30:39 +09:00
Valentin Tolmer 55de3ac329 server: extract the sql backend handler to a separate crate 2025-04-09 09:30:39 +09:00
Valentin Tolmer ee21d83056 server: extract ldap operations to a crate 2025-04-09 09:30:39 +09:00
Valentin Tolmer a49ddeaa02 server: extract opaque_handler to a separate crate 2025-04-09 09:30:39 +09:00
Valentin Tolmer dbba4c4e26 server: extract access_control to a separate crate 2025-04-09 09:30:39 +09:00
Valentin Tolmer 0eef966c3e server: Move PublicSchema to the domain crate 2025-04-09 09:30:39 +09:00
Valentin Tolmer cdf43f2a69 server: cleanup extra mut in ldap handler 2025-04-09 09:30:39 +09:00
Valentin Tolmer 7450ff1028 server: Add support for deleting users and groups via LDAP 2025-04-09 09:30:39 +09:00
Valentin Tolmer c3ae149ae3 server: add tests for ldap modify 2025-04-09 09:30:39 +09:00
Valentin Tolmer 0a05a091d8 server: split off modify requests from ldap_handler 2025-04-09 09:30:39 +09:00
Valentin Tolmer 6a2a5fe7f5 server: split off create_* from ldap_handler 2025-04-09 09:30:39 +09:00
Valentin Tolmer 52f22c00c3 server: split off compare from ldap_handler 2025-04-09 09:30:39 +09:00
Valentin Tolmer 37a85b4c2e server: split off password handling from ldap_handler 2025-04-09 09:30:39 +09:00
Valentin Tolmer 63f8b51c88 server: split off do_bind from ldap_handler 2025-04-09 09:30:39 +09:00
Valentin Tolmer c4aca0dad7 server: split off ldap/search from ldap_handler 2025-04-09 09:30:39 +09:00
Valentin Tolmer b8f114bd43 ldap: add support for creating groups 2025-04-08 19:15:47 -05:00
Valentin Tolmer 31364da6d4 chore: add prepare-release to Makefile 2025-04-04 10:43:48 -05:00
Juntong Zhu 853c561314 example_config: fix kimai.yaml 2025-04-01 06:47:53 -05:00
Valentin Tolmer 0aa31a282a app: Remove max-width in main component 2025-03-31 22:07:43 -05:00
ivan-sirosh 41e38234ed example_config: Add penpot 2025-03-31 11:42:26 -05:00
Valentin Tolmer ba9bcb3894 chore: Migrate all the crates to edition 2024 2025-03-30 21:32:46 -05:00
Valentin Tolmer e18f2af54f cargo: Migrate metadata to workspace 2025-03-30 20:56:16 -05:00
Valentin Tolmer 5afcdbda65 app, server: Add an endpoint to fetch the frontend settings 2025-03-30 20:52:39 -05:00
Valentin Tolmer ba93533790 chore: update lldap/rust-dev to 1.85 2025-03-30 18:49:58 -05:00
Valentin Tolmer e4044b7415 dependencies: Upgrade sea-orm to 1.1.8 2025-03-30 23:00:50 +02:00
meetpatty 26b25e7776 example_configs: Fix nslcd group member mapping. 2025-03-29 11:44:17 +01:00
taiwan-king 20ade89633 example_configs: Add MFA with DuoAuthProxy 2025-03-13 23:28:16 +01:00
Yuki 928559890a example_configs: add example with podman quadlets for pgsql deployment w/ secrets 2025-03-13 17:46:57 +01:00
hendrik1120 049e882c35 docs(readme): clarify password change permission for admin users 2025-03-07 12:31:44 +01:00
MickMorley f5f3091313 example_configs: update Home Assistant
Found that only a restart of Home Assistant will read the new lines in configuration.yaml.  Also added a note to use `-k` when using the curl command if needed.
2025-02-27 17:30:51 +01:00
Simon Broeng Jensen 0a0f915ce6 chore: update rust-argon2 crate to v2 2025-02-25 15:18:06 +01:00
xeoneox 5f42d423e3 example_configs: fix typo in Stalwart config 2025-02-24 08:10:48 +01:00
Simon Broeng Jensen 2a226963ee auth: move Permission and ValidationResults to auth crate 2025-02-22 23:18:06 +01:00
Simon Broeng Jensen ca1c6ff645 domain-handlers: move backend handler traits to separate crate 2025-02-21 20:41:20 +01:00
xeoneox e22d17dca6 example_configs: reduce privileges for stalwart bind user
Update instructions for read_only bind
2025-02-21 16:02:08 +01:00
Simon Broeng Jensen f34fa1d701 cargo,server: update ldap3_proto to version 0.6.0 2025-02-21 11:27:13 +01:00
Simon Broeng Jensen d854ace89f domain-model: move domain::model module to separate crate 2025-02-21 10:25:07 +01:00
xeoneox 3c0359eb8a example_configs: Add Stalwart link to README 2025-02-20 21:16:32 +01:00
xeoneox b591539c8a example_configs: Add Stalwart
Failed at getting Docker Mailserver and Maddy working, so I wrote a config for Stalwart Mailserver instead
2025-02-20 20:50:52 +01:00
Simon Broeng Jensen 5d2f168554 domain + server: introduce new AttributeValue enum 2025-02-19 15:48:27 +01:00
JaidenW cf0e9a01f1 readme: add link to the discord bot 2025-02-19 08:03:35 +01:00
Giovanni Geraci 86d15e831e example_configs: Add Project Quay 2025-02-16 21:33:01 +01:00
Simon Broeng Jensen 8285e21ebb domain: rename AttributeValue to Attribute
Preparation for storing the actual types for each value, which
will repurpose the AttributeValue name.
2025-02-05 16:07:05 +01:00
Simon Broeng Jensen 4c6cfeee9e server: remove deprecated fields from CreateUserRequest
The fields first_name, last_name, and avatar have all been moved
to regular attributes in the database, and are available through
the GraphQL API as such as well. This commit removes the legacy
fields for each on the internal CreateUserRequest type, leaving
these to only be updateable through attributes.

The fields are still available in the GraphQL CreateUserInput
type, preserving backwards compatiblity, and if set, they will
be used for the corresponding attribute values. If both fields
and attributes are set, the values given through attributes will
superceed the fields, and be used. This change also fixes a bug,
where creation of a user would fail if either of these attributes
were set as both attribute and field, as it would attempt to
insert the attribute twice, violating a unique constraint in the
database.
2025-02-05 15:42:06 +01:00
Simon Broeng Jensen 37a683dcb2 validation: move validation crate to crates folder 2025-02-03 23:32:42 +01:00
Simon Broeng Jensen b5e87c7226 auth: move auth crate to crates folder 2025-02-03 23:32:42 +01:00
nitnelave dd0ba5975e server: Adds support for whoamiOID
Co-authored-by: eyjhb <eyjhbb@gmail.com>
2025-02-03 23:21:44 +01:00
Simon Broeng Jensen 1b26859141 server: move domain::types to separate domain crate (#1086)
Preparation for using basic type definitions in other upcoming
modules, in particular for plugins.
2025-02-03 23:00:27 +01:00
Fabian May 417abc54e4 server: Cleanup log messages for ignored attribute warnings
Reduce log messages by remove line break and remove visible \n\ sequence
2025-02-03 22:52:15 +01:00
Simon Broeng Jensen 5cc489aafe app: mute a clippy error about a wasm_bindgen directive 2025-01-29 13:32:05 +01:00
Simon Broeng Jensen c01c7744c7 server: fix a couple of clippy warnings 2025-01-29 13:32:05 +01:00
Simon Broeng Jensen 1b58ac61f4 server: fix serving of frontend after PR #1079 (#1090)
Had changed behaviour to serve the gz compressed wasm package
with the uncompressed handler.
2025-01-29 13:25:03 +01:00
tyami94 f46e5375df server: Allow custom path to front-end assets 2025-01-28 19:37:12 +01:00
Mathieu Bélanger 722464daf4 example_configs: Add pgAdmin 2025-01-22 22:12:54 +01:00
Simon Broeng Jensen 0799b6bc26 server: include preserved case in user attribute value search
Extends the generated UserRequestFilter with an OR'ed clause for
the attribute value in both it's original case and lowercased.
2025-01-22 10:37:04 +01:00
Simon Broeng Jensen f5fbb31e6e server, app: Add validation for attribute names (#1075)
This commit adds support for basic validation of attribute
names at creation, and also in the schema overview. Both
user and group attributes are validated with the same rules.

For now, attribute names will be considered valid, if they
only contain alphanumeric characters and dashes.

Validation has been added the following places:

- In graphql API, for creation of both user and group attributes.
  Request will be rejected, if attribute name is invalid.

- In frontend, before submitting a request to create a new user
  or group attribute. Rejection here will show an error message
  including a list of the invalid characters used.

As this change adds stricter validation to attributes, and, since
the rationale for this is partly compatibility with other LDAP
systems, this change also adds a warning in the schema overviews
to any attribute using invalid characters.
2025-01-22 09:57:47 +01:00
Simon Broeng Jensen 31a0cf5a4f app: Change default alias for User & Group schema attributes (#1082)
A number of the hardcoded attributes displayed in the User
and Group schema overviews were using aliases which contain
underscores, which is not always completely supported by
clients. Therefore, this commit changes the primary alias
for each attribute to be one without underscores.

To reduce confusion with this change, and also improve the
visibility of available aliases, this commit also includes
a list of each alias, for each hardcoded attribute. This
list will also include the old primary aliases.
2025-01-21 13:46:55 +01:00
Simon Broeng Jensen 33fb59f2f7 server: Add support for querying GroupId with LDAP filters 2025-01-20 17:07:53 +01:00
farshad fb43af1299 example_configs: update Authelia with LLDAP default settings 2025-01-19 07:02:05 +01:00
Valentin Tolmer f417427635 Prevent starting up if the JWT secret is not given
Similarly, don't create the admin if the password is not given
2024-12-24 19:40:26 +01:00
Dakota G 1f26262e13 example_configs: add Hashicorp 2024-12-10 07:34:50 +01:00
Zepmann 42fccf4713 readme: Updated Arch Linux install-from-repository section
Cleaned up the Arch Linux section. Added a link to the discussions support thread.
2024-12-07 18:49:58 +01:00
xeoneox 928faa4bcc example_configs: add search filter in onedev configuration 2024-12-07 07:17:52 +01:00
xeoneox 3895a5050d example_configs: Update OneDev example for latest release 2024-12-06 00:21:35 +01:00
Christian Medel f92035b6fd example_configs: Add Kimai 2024-11-25 22:20:09 +01:00
Valentin Tolmer 37a10c871f github: Fix release bot clearing the release body 2024-11-22 23:12:36 +01:00
Valentin Tolmer 8397d536d9 chore: bump version to 0.6.2-alpha 2024-11-22 22:55:53 +01:00
Valentin Tolmer acd39d20b1 release: 0.6.1 2024-11-22 22:47:49 +01:00
Valentin Tolmer 0ddeab8caa server: Fix schema migration from v8 for sqlite and postgres
Neither supports limits, but we can delete all the duplicate memberships and re-insert a single one
2024-11-21 23:34:37 +01:00
xeoneox 64514ddfc6 example_configs: expand url for OneDev config
fix capitalization and expound URL example
2024-11-21 10:01:24 +01:00
Valentin Tolmer c47be779a3 docs: update architecture.md 2024-11-19 22:07:02 +01:00
xeoneox fea2ed5b79 example_configs: Add onedev 2024-11-19 22:01:30 +01:00
Jan Düpmeier e982908768 cargo,auth,server: update opaque-ke => 0.7 2024-11-17 13:34:01 +01:00
Valentin Tolmer 713dbde4cb server: Fix the instructions to silence the key_seed warning 2024-11-14 22:27:32 +01:00
Ansgar Tasler 579dd5e1b6 readme: add reference to terraform provider (#1035) 2024-11-13 16:04:41 +01:00
traverseda 3828ec7624 example_configs: Update pam example for release 0.6..0 2024-11-13 12:38:45 +01:00
Valentin Tolmer b8c06ebd75 chore: bump version to 0.6.1-alpha 2024-11-09 22:25:13 +01:00
Valentin Tolmer 130d2552ac github: Remove release PR comment bot
It only runs for PRs that are mentioned in the release notes, but I only mention issues
2024-11-09 22:20:31 +01:00
Valentin Tolmer 098745ebc9 release: 0.6.0 2024-11-09 21:46:49 +01:00
Valentin Tolmer 95337e2cd8 server: Remove session-wide logging, add session_uuid to message logs 2024-11-04 21:47:26 +01:00
Valentin Tolmer 143eb70bee server: Only use a single connection with SQlite
Several writer connections can lock the DB and cause other inserts to fail.

A single connection should be enough given the usual workloads
2024-10-30 15:35:47 +01:00
Valentin Tolmer 35fe521cbe server: Correctly handle removal of the display_name attribute 2024-10-29 15:33:46 +01:00
Valentin Tolmer c8601b9169 server: Correctly handle attempts to probe for password resets 2024-10-28 20:09:46 +01:00
Hobbabobba 8f6c324de7 example_configs: add ldap_ssl to vaultwarden_ldap_sync:2.0.2 (#1011) 2024-10-28 16:43:49 +01:00
Valentin Tolmer f0fcc88f1d server: Fix env warning for nested keys 2024-10-28 16:23:25 +01:00
Valentin Tolmer c08ddecd32 server: Fix missing lowercasing when changing passwords through LDAP 2024-10-28 16:06:25 +01:00
Valentin Tolmer 4ebfd0525b app: Allow custom attributes in group creation 2024-10-28 15:59:08 +01:00
Valentin Tolmer a190fe7ddf server: return custom attributes when asked for all attributes 2024-10-26 19:07:08 +02:00
dependabot[bot] df188ee83f build(deps): bump actions/checkout from 4.2.1 to 4.2.2
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.1 to 4.2.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/v4.2.1...v4.2.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>
2024-10-24 07:46:46 +02:00
Valentin Tolmer 52c917d967 server: improve key_seed warning 2024-10-22 00:48:40 +02:00
Valentin Tolmer f01daae6a8 server: Fix env variable warning 2024-10-22 00:48:29 +02:00
Valentin Tolmer 62b2afa283 app: fix password reset probing
It was still using get, but should have used post
2024-10-22 00:38:09 +02:00
Valentin Tolmer 305b272cdf app: Add support for group attributes 2024-10-22 00:37:38 +02:00
Daniel S. Reichenbach a95ac38083 example_configs: keycloak typo for first name attribute (#1004)
It should be `givenName` instead of `givenname`. Using the later one, will result in Keycloak bugging out during the sync process, and henceforth displaying an empty user list.
2024-10-18 12:52:42 +02:00
Valentin Tolmer abfe2f3a17 cargo,app,auth: Update dependencies, fix breaks 2024-10-17 00:17:41 +02:00
Johannes Kastl 11d766b2ba Dockerfile: add jq/jo/curl, required by bootstrap.sh 2024-10-14 21:34:04 +02:00
Valentin Tolmer 56eee6908e server: Add a way to print raw logs
If the variable LLDAP_RAW_LOG is set, the logs will be both formatted with tracing_forest and printed raw
2024-10-10 21:27:36 +02:00
Grzegorz Godlewski dcb45d4f6b Add support for bootstrapping schemas (#991)
* Moved default bootstrap dirs into single /bootstrap parent dir in order to have single docker volume bind (with fallback to previous folder hierarchy)
* Added default values for LDAP user and credentials
* Added support for bootstrapping schema

Place schema files under /bootstrap/(user|group)-schemas/*.json

Sample content:
[
  {
    "name" : "test_attrib",
    "attributeType" : "STRING",
    "isEditable" : true,
    "isList" : false,
    "isVisible" : true
  }
]
2024-10-10 21:05:01 +02:00
dependabot[bot] a6eac55fc7 build(deps): bump actions/checkout from 4.1.7 to 4.2.1
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.7 to 4.2.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.1.7...v4.2.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>
2024-10-08 00:03:48 +02:00
dependabot[bot] 1c6646d8c5 build(deps): bump docker/build-push-action from 5 to 6
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
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>
2024-10-07 23:36:28 +02:00
Blueswen 362e968e00 example_configs: Update base DN in grafana_ldap_config.toml
Use `dc=com` as the same as the default DN.
2024-10-07 18:24:33 +02:00
Valentin Tolmer 17bcd7645b app: Clean up code, don't error on admin empty email 2024-10-05 23:10:40 +02:00
Austin Alvarado dcba3d17dc app: Add support for user-created attributes
Note: This PR doesn't handle errors around Jpeg files very well.

Co-authored-by: Bojidar Marinov <bojidar.marinov.bg@gmail.com>
Co-authored-by: Austin Alvarado <pixelrazor@gmail.com>
2024-09-30 23:53:14 +02:00
Valentin Tolmer 1f3f73585b server: Add logging for password resets, add name for successful opaque logins 2024-09-26 22:51:34 +02:00
Valentin Tolmer 0c6a92a8fa server: Clarify logging of login attempts and failures 2024-09-26 20:43:19 +02:00
PopeRigby 120ad34f92 example_configs: Update Radicale guide with configuration for Radicale 3.3.0 (#979) 2024-09-22 21:58:53 +02:00
Roman a2ba71ac19 example_configs: Update PAM integration
* Add more information for PAM integration:

* Add info that custom attributes only work on nightly
* Add sample lldap-cli command to set attribute

* Modify nslcd to use unix-uid/gid directly as it is now supported

* Add readme for PAM integration, removing the need for is-unix-user/group.
2024-09-17 00:19:03 +02:00
Valentin Tolmer 10a820f2a2 server: detect anonymous binds and return a correct error 2024-09-11 22:19:58 +02:00
Valentin Tolmer 01f97f5ed4 server: clean up the expected keys 2024-09-10 23:25:33 +02:00
Valentin Tolmer f14aa2284c server: Detect unknown env variables (e.g. due to typos) 2024-09-08 21:45:36 +02:00
Valentin Tolmer 65e2103365 server: Simplify the debug print of various structs
And use derive_more more liberally to simplify the impls
2024-09-08 00:43:58 +02:00
Valentin Tolmer 5db0072cfa server: clarify SMTP error message
SMTP docs for many email providers use SSL to mean SSL/TLS, and TLS to mean STARTTLS, causing endless confusion. This should hopefully help.
2024-09-07 23:50:43 +02:00
Valentin Tolmer 1d8d3eb73f server: Fix attribute name 2024-09-07 22:27:20 +02:00
Joshua M. Clulow 97e4d90eb7 dependencies: update whoami to fix illumos build 2024-09-02 21:11:58 +02:00
Valentin Tolmer 6cf0f6df06 server: map email and display_name from attributes into user fields 2024-08-28 00:25:23 +02:00
Valentin Tolmer b1384818d2 server: Add a is_readonly attribute to the schema 2024-08-27 23:04:24 +02:00
Valentin Tolmer 3ec44a58be server: Allow password reset every time the server starts 2024-08-26 12:53:25 +02:00
aokblast 6f7bfca682 Use sysrc in FreeBSD install instruction 2024-08-25 08:12:02 +02:00
Valentin Tolmer 2c79a40a73 server: Mask the details of SMTP errors, sleep when failing to send an email 2024-08-21 16:19:13 +02:00
Valentin Tolmer 25c6d6c962 README: fix anchor link 2024-08-19 22:42:18 +02:00
Valentin Tolmer 04b048dd47 example_configs: add PAM configuration guide 2024-08-19 22:38:58 +02:00
Valentin Tolmer dc26f97117 server: Fix compilation on Windows 2024-08-18 20:12:03 +02:00
Valentin Tolmer 09c5d9f925 server: Fix implementation of attribute present filter
Instead of just doing a schema check, this actually looks for users that have a value for this attribute.
2024-08-16 23:56:02 +02:00
Valentin Tolmer ee7f9c9f41 server: Update ldap3_proto dependency 2024-08-16 23:47:06 +02:00
Valentin Tolmer fa9c503de7 server: Add support for memberOf with plain user names, relax hard errors
This should help when the client sends some invalid-looking queries as part of a bigger filter
2024-08-16 23:21:20 +02:00
Masgalor 4138963bee readme: Improve the package repository section 2024-08-09 00:27:54 +02:00
Alyssa Ross 5a2a92bbda cargo: update time
Fixes building with Rust 1.80.0.

Closes: https://github.com/lldap/lldap/issues/945
2024-08-08 22:39:10 +02:00
Dakota G 6aa9303339 example_configs: Add configuration for Netbox 2024-08-06 15:06:16 +02:00
Bojidar Marinov 049a360506 server: Lookup first_name/last_name in the right list of attributes (#943)
Note the std::mem::take(&mut user.attributes) further up that zeroes out user.attributes
2024-07-31 23:55:07 +02:00
jakob42 b26de34e0d example_configs: add Prosody 2024-07-31 07:10:38 +02:00
Josh Thorpe 15c28110b5 example_configs: clean up jellyfin.md
Restructured to match the jellyfin plugin UI.
2024-07-24 14:46:40 +02:00
ChevySSinSD 83508a363c generate_secrets: improve portability
Updated print_random function definition to be compatible with multiple default shells
2024-07-23 16:36:11 +02:00
fengxsong 010eec22d3 example_configs: fix dex integration
Host and optional port of the LDAP server are in the form "host:port".
2024-07-22 07:38:22 +02:00
Binh-Nguyen Tran b33d56a459 bootstrap.sh: use exact match instead of substring when checking user id existence
Signed-off-by: Binh-Nguyen Tran <tbnguyen1407@gmail.com>
2024-07-20 11:07:51 +02:00
sean 6eb5b959bf example_config: adjusted addressand attributes for authelia 5.0.0 compliance 2024-07-10 22:00:35 +02:00
Valentin Tolmer 6f46ffd1e4 clippy: new fixes 2024-06-16 12:18:46 +02:00
Noah Snelson 73686224dd example_configs: Add Carpal (#916) 2024-06-15 22:39:42 +02:00
dependabot[bot] 56ed37ef8a build(deps): bump actions/checkout from 4.1.5 to 4.1.7
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.5 to 4.1.7.
- [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.1.5...v4.1.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-13 05:27:34 +02:00
thielj 39e1a02255 Update minio.md
The described configuration didn't work for me; I've added my working configuration at the bottom. Hope that helps someone!
2024-06-10 07:35:44 +02:00
dependabot[bot] 4f050cded5 build(deps): bump actions/checkout from 4.1.4 to 4.1.5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.4 to 4.1.5.
- [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.1.4...v4.1.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 22:40:29 +02:00
RobertL 254a168e78 example_configs: mailserver: Include protocol in server host definition
Without the protocol specified, Mailserver throws an error
2024-05-03 09:32:54 +02:00
Pierre Penninckx 85b83aff5f example_configs: add user_id mapping for nextcloud
This allows both LDAP and SSO backends to have consistent usernames
2024-05-02 09:19:33 +02:00
lvillis 199a80ca5b example_configs: Add Metabase and sonarqube (#906) 2024-04-30 12:17:25 +02:00
Torstein Eide f96868318a example_configs: pfsense.md, add warning about error about OU 2024-04-27 14:42:48 +02:00
dependabot[bot] 04b0fa0ae9 build(deps): bump actions/checkout from 4.1.3 to 4.1.4
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.3 to 4.1.4.
- [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.1.3...v4.1.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-24 23:26:16 +02:00
dependabot[bot] 2e08c6a7ec build(deps): bump actions/checkout from 4.1.2 to 4.1.3
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.2 to 4.1.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/v4.1.2...v4.1.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>
2024-04-23 07:22:01 +02:00
dependabot[bot] 892492815d build(deps): bump actions/checkout from 4.1.1 to 4.1.2
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.1 to 4.1.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/v4.1.1...v4.1.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>
2024-04-22 21:52:51 +02:00
Valentin Tolmer 2597a250f0 github: Update codecove action to v4 2024-04-22 21:37:23 +02:00
Valentin Tolmer f67f090bde migration_tool: fix clippy warning 2024-04-22 20:42:40 +02:00
n-connect a97881477f example_configs: add FreeBSD rc.d service script
Use:
extract (the future) FreeBSD release package into /usr/local/ -> so your files will be under /usr/local/lldap_server/
save/copy this rc.d script file into /usr/local/etc/rc.d/
finally cat lldap_enable=YES >> /etc/rc.conf
the service script set to run the lldap server as "www" user - make sure the whole lldap_server directory is accessible/runnable by "www". Simplest to run chown -R www:www /usr/local/lldap_server
2024-04-22 17:39:05 +02:00
nitnelave 8587fc38fd config: Fix the admin reset password option in template 2024-04-04 00:35:40 +02:00
Jonathan 6d65a2546c docker: Include bootstrap.sh in main image 2024-04-01 18:54:09 +02:00
kri164 7806ed34ff example_configs: Update nextcloud.md - add a example of group filter (#882) 2024-03-28 10:29:55 +01:00
Valentin Tolmer 22623bfab1 server: Fix user search for multiple memberOf 2024-03-18 22:02:12 +01:00
lvillis 2f20f63b41 example_configs: Fix typo in nexus.md 2024-03-15 12:02:56 +01:00
lordratner 87d825626c example_configs: fix role in authelia 2024-03-14 20:42:03 +01:00
Aziz 8cbad6d5bd example_configs: Add MegaRAC-SP-X-BMC 2024-03-14 09:36:12 +01:00
kevin7s-io 8db7d8a46f example_configs: Add Harbor 2024-03-12 21:42:37 +01:00
Valentin Tolmer 533d1bcfd0 github: Update dev container to add FreeBSD target 2024-03-07 09:18:05 +01:00
Valentin Tolmer 3d8aafaa9d app: Improve the email reset message 2024-02-27 08:41:24 +01:00
Valentin Tolmer f93681239b app: default to user_id if display_name is empty, when adding users to groups 2024-02-27 08:27:33 +01:00
Valentin Tolmer 13720c101c server: silence clippy warnings 2024-02-27 08:22:58 +01:00
Valentin Tolmer a1eb708cf3 server: Add missing unique indices on lowercase email/group names, fix memberof lookup 2024-02-26 10:53:51 +01:00
Adam Shand 959bb907d8 example_configs: Add OCIS 2024-02-20 10:40:47 +01:00
jakob42 22074f56d2 mentioned dokuwiki authchained plugin 2024-02-12 09:24:14 +01:00
268 changed files with 20303 additions and 9931 deletions
+46
View File
@@ -0,0 +1,46 @@
# docs: https://docs.coderabbit.ai/reference/yaml-template for full configuration options
tone_instructions: "Be concise"
reviews:
profile: "chill"
high_level_summary: false
review_status: false
commit_status: false
collapse_walkthrough: true
changed_files_summary: false
sequence_diagrams: false
estimate_code_review_effort: false
assess_linked_issues: false
related_issues: false
related_prs: false
suggested_labels: false
suggested_reviewers: false
poem: false
auto_review:
enabled: true
auto_incremental_review: true
finishing_touches:
docstrings:
enabled: false
unit_tests:
enabled: false
pre_merge_checks:
docstrings:
mode: "off"
title:
mode: "off"
description:
mode: "off"
issue_assessment:
mode: "off"
chat:
art: false
auto_reply: false
knowledge_base:
web_search:
enabled: true
code_guidelines:
enabled: false
+1 -1
View File
@@ -1,4 +1,4 @@
FROM rust:1.74
FROM rust:1.89
ARG USERNAME=lldapdev
# We need to keep the user as 1001 to match the GitHub runner's UID.
+20 -2
View File
@@ -1,8 +1,26 @@
{
"name": "LLDAP dev",
"build": { "dockerfile": "Dockerfile" },
"build": {
"dockerfile": "Dockerfile"
},
"customizations": {
"vscode": {
"extensions": [
"rust-lang.rust-analyzer"
],
"settings": {
"rust-analyzer.linkedProjects": [
"./Cargo.toml"
]
}
}
},
"features": {
"ghcr.io/devcontainers/features/rust:1": {}
},
"forwardPorts": [
3890,
17170
]
],
"remoteUser": "lldapdev"
}
+1
View File
@@ -0,0 +1 @@
use flake
-1
View File
@@ -1 +0,0 @@
* @nitnelave
+5 -8
View File
@@ -1,19 +1,16 @@
codecov:
require_ci_to_pass: yes
comment:
layout: "header,diff,files"
require_changes: true
require_base: true
require_head: true
layout: "condensed_header, diff, condensed_files"
hide_project_coverage: true
require_changes: "coverage_drop"
coverage:
range: "70...100"
status:
project:
default:
target: "75%"
threshold: "0.1%"
removed_code_behavior: adjust_base
github_checks:
annotations: true
threshold: 5
ignore:
- "app"
- "docs"
+159
View File
@@ -0,0 +1,159 @@
# LLDAP - Light LDAP implementation for authentication
LLDAP is a lightweight LDAP authentication server written in Rust with a WebAssembly frontend. It provides an opinionated, simplified LDAP interface for authentication and integrates with many popular services.
**ALWAYS reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.**
## Working Effectively
### Bootstrap and Build the Repository
- Install dependencies: `sudo apt-get update && sudo apt-get install -y curl gzip binaryen`
- Install Rust if not available: `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` then `source ~/.cargo/env`
- Install wasm-pack for frontend: `cargo install wasm-pack` -- takes 90 seconds. NEVER CANCEL. Set timeout to 180+ seconds.
- Build entire workspace: `cargo build --workspace` -- takes 3-4 minutes. NEVER CANCEL. Set timeout to 300+ seconds.
- Build release server binary: `cargo build --release -p lldap` -- takes 5-6 minutes. NEVER CANCEL. Set timeout to 420+ seconds.
- Build frontend WASM: `./app/build.sh` -- takes 3-4 minutes including wasm-pack installation. NEVER CANCEL. Set timeout to 300+ seconds.
### Testing and Validation
- Run all tests: `cargo test --workspace` -- takes 2-3 minutes. NEVER CANCEL. Set timeout to 240+ seconds.
- Check formatting: `cargo fmt --all --check` -- takes <5 seconds.
- Run linting: `cargo clippy --tests --all -- -D warnings` -- takes 60-90 seconds. NEVER CANCEL. Set timeout to 120+ seconds.
- Export GraphQL schema: `./export_schema.sh` -- takes 70-80 seconds. NEVER CANCEL. Set timeout to 120+ seconds.
### Running the Application
- **ALWAYS run the build steps first before starting the server.**
- Start development server: `cargo run -- run --config-file <config_file>`
- **CRITICAL**: Server requires a valid configuration file. Use `lldap_config.docker_template.toml` as reference.
- **CRITICAL**: Avoid key conflicts by removing existing `server_key*` files when testing with `key_seed` in config.
- Server binds to:
- LDAP: port 3890 (configurable)
- Web interface: port 17170 (configurable)
- LDAPS: port 6360 (optional, disabled by default)
### Manual Validation Requirements
- **ALWAYS test both LDAP and web interfaces after making changes.**
- Test web interface: `curl -s http://localhost:17170/` should return HTML with "LLDAP Administration" title.
- Test GraphQL API: `curl -s -X POST -H "Content-Type: application/json" -d '{"query": "query { __schema { queryType { name } } }"}' http://localhost:17170/api/graphql`
- Run healthcheck: `cargo run -- healthcheck --config-file <config_file>` (requires running server)
- **ALWAYS ensure server starts without errors and serves the web interface before considering changes complete.**
## Validation Scenarios
After making code changes, ALWAYS:
1. **Build validation**: Run `cargo build --workspace` to ensure compilation succeeds.
2. **Test validation**: Run `cargo test --workspace` to ensure existing functionality works.
3. **Lint validation**: Run `cargo clippy --tests --all -- -D warnings` to catch potential issues.
4. **Format validation**: Run `cargo fmt --all --check` to ensure code style compliance.
5. **Frontend validation**: Run `./app/build.sh` to ensure WASM compilation succeeds.
6. **Runtime validation**: Start the server and verify web interface accessibility.
7. **Schema validation**: If GraphQL changes made, run `./export_schema.sh` to update schema.
### Test User Scenarios
- **Login flow**: Access web interface at `http://localhost:17170`, attempt login with admin/password (default).
- **LDAP binding**: Test LDAP connection on port 3890 with appropriate LDAP tools if available.
- **Configuration changes**: Test with different configuration files to validate config parsing.
## Project Structure and Key Components
### Backend (Rust)
- **Server**: `/server` - Main application binary
- **Crates**: `/crates/*` - Modularized components:
- `auth` - Authentication and OPAQUE protocol
- `domain*` - Domain models and handlers
- `ldap` - LDAP protocol implementation
- `graphql-server` - GraphQL API server
- `sql-backend-handler` - Database operations
- `validation` - Input validation utilities
### Frontend (Rust + WASM)
- **App**: `/app` - Yew-based WebAssembly frontend
- **Build**: `./app/build.sh` - Compiles Rust to WASM using wasm-pack
- **Assets**: `/app/static` - Static web assets
### Configuration and Deployment
- **Config template**: `lldap_config.docker_template.toml` - Reference configuration
- **Docker**: `Dockerfile` - Container build definition
- **Scripts**:
- `prepare-release.sh` - Cross-platform release builds
- `export_schema.sh` - GraphQL schema export
- `generate_secrets.sh` - Random secret generation
- `scripts/bootstrap.sh` - User/group management script
## Common Development Workflows
### Making Backend Changes
1. Edit Rust code in `/server` or `/crates`
2. Run `cargo build --workspace` to test compilation
3. Run `cargo test --workspace` to ensure tests pass
4. Run `cargo clippy --tests --all -- -D warnings` to check for warnings
5. If GraphQL schema affected, run `./export_schema.sh`
6. Test by running server and validating functionality
### Making Frontend Changes
1. Edit code in `/app/src`
2. Run `./app/build.sh` to rebuild WASM package
3. Start server and test web interface functionality
4. Verify no JavaScript errors in browser console
### Adding New Dependencies
- Backend: Add to appropriate `Cargo.toml` in `/server` or `/crates/*`
- Frontend: Add to `/app/Cargo.toml`
- **Always rebuild after dependency changes**
## CI/CD Integration
The repository uses GitHub Actions (`.github/workflows/rust.yml`):
- **Build job**: Validates workspace compilation
- **Test job**: Runs full test suite
- **Clippy job**: Linting with warnings as errors
- **Format job**: Code formatting validation
- **Coverage job**: Code coverage analysis
**ALWAYS ensure your changes pass all CI checks by running equivalent commands locally.**
## Timing Expectations and Timeouts
| Command | Expected Time | Timeout Setting |
|---------|---------------|-----------------|
| `cargo build --workspace` | 3-4 minutes | 300+ seconds |
| `cargo build --release -p lldap` | 5-6 minutes | 420+ seconds |
| `cargo test --workspace` | 2-3 minutes | 240+ seconds |
| `./app/build.sh` | 3-4 minutes | 300+ seconds |
| `cargo clippy --tests --all -- -D warnings` | 60-90 seconds | 120+ seconds |
| `./export_schema.sh` | 70-80 seconds | 120+ seconds |
| `cargo install wasm-pack` | 90 seconds | 180+ seconds |
**NEVER CANCEL** any of these commands. Builds may take longer on slower systems.
## Troubleshooting Common Issues
### Build Issues
- **Missing wasm-pack**: Run `cargo install wasm-pack`
- **Missing binaryen**: Run `sudo apt-get install -y binaryen` or disable wasm-opt
- **Clippy warnings**: Fix all warnings as they are treated as errors in CI
- **GraphQL schema mismatch**: Run `./export_schema.sh` to update schema
### Runtime Issues
- **Key conflicts**: Remove `server_key*` files when using `key_seed` in config
- **Port conflicts**: Check if ports 3890/17170 are available
- **Database issues**: Ensure database URL in config is valid and accessible
- **Asset missing**: Ensure frontend is built with `./app/build.sh`
### Development Environment
- **Rust version**: Use stable Rust toolchain (2024 edition)
- **System dependencies**: curl, gzip, build tools
- **Database**: SQLite (default), MySQL, or PostgreSQL supported
## Configuration Reference
Essential configuration parameters:
- `ldap_base_dn`: LDAP base DN (e.g., "dc=example,dc=com")
- `ldap_user_dn`: Admin user DN
- `ldap_user_pass`: Admin password
- `jwt_secret`: Secret for JWT tokens (generate with `./generate_secrets.sh`)
- `key_seed`: Encryption key seed
- `database_url`: Database connection string
- `http_port`: Web interface port (default: 17170)
- `ldap_port`: LDAP server port (default: 3890)
**Always use the provided config template as starting point for new configurations.**
+26
View File
@@ -0,0 +1,26 @@
name: Copilot Setup Steps for LLDAP Development
steps:
- name: Update package list
run: sudo apt-get update
- name: Install system dependencies
run: sudo apt-get install -y curl gzip binaryen build-essential
- name: Install Rust toolchain
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source ~/.cargo/env
echo 'source ~/.cargo/env' >> ~/.bashrc
- name: Install wasm-pack for frontend builds
run: |
source ~/.cargo/env
cargo install wasm-pack
- name: Verify installations
run: |
source ~/.cargo/env
rustc --version
cargo --version
wasm-pack --version
+13 -2
View File
@@ -1,6 +1,6 @@
FROM localhost:5000/lldap/lldap:alpine-base
# Taken directly from https://github.com/tianon/gosu/blob/master/INSTALL.md
ENV GOSU_VERSION 1.17
ENV GOSU_VERSION=1.17
RUN set -eux; \
\
apk add --no-cache --virtual .gosu-deps \
@@ -15,7 +15,18 @@ RUN set -eux; \
\
# verify the signature
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
for server in \
hkps://keys.openpgp.org \
ha.pool.sks-keyservers.net \
hkp://p80.pool.sks-keyservers.net:80 \
keyserver.ubuntu.com \
hkp://keyserver.ubuntu.com:80 \
pgp.mit.edu \
; do \
if gpg --batch --keyserver "$server" --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; then \
break; \
fi; \
done; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
gpgconf --kill all; \
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
+3 -2
View File
@@ -59,12 +59,12 @@ RUN set -x \
&& 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
FROM alpine:3.19
WORKDIR /app
ENV UID=1000
ENV GID=1000
ENV USER=lldap
RUN apk add --no-cache tini ca-certificates bash tzdata && \
RUN apk add --no-cache tini ca-certificates bash tzdata jq curl jo && \
addgroup -g $GID $USER && \
adduser \
--disabled-password \
@@ -80,5 +80,6 @@ COPY --from=lldap --chown=$USER:$USER /lldap /app
VOLUME ["/data"]
HEALTHCHECK CMD ["/app/lldap", "healthcheck", "--config-file", "/data/lldap_config.toml"]
WORKDIR /app
COPY scripts/bootstrap.sh ./
ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"]
CMD ["run", "--config-file", "/data/lldap_config.toml"]
+19 -5
View File
@@ -1,12 +1,15 @@
FROM localhost:5000/lldap/lldap:debian-base
# Taken directly from https://github.com/tianon/gosu/blob/master/INSTALL.md
ENV GOSU_VERSION 1.17
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/*; \
for i in 1 2 3; do \
apt-get update && \
apt-get install -y --no-install-recommends wget ca-certificates gnupg && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && break || sleep 5; \
done; \
\
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"; \
@@ -14,7 +17,18 @@ RUN set -eux; \
\
# verify the signature
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
for server in \
hkps://keys.openpgp.org \
ha.pool.sks-keyservers.net \
hkp://p80.pool.sks-keyservers.net:80 \
keyserver.ubuntu.com \
hkp://keyserver.ubuntu.com:80 \
pgp.mit.edu \
; do \
if gpg --batch --keyserver "$server" --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; then \
break; \
fi; \
done; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
gpgconf --kill all; \
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
+2 -1
View File
@@ -65,7 +65,7 @@ ENV UID=1000
ENV GID=1000
ENV USER=lldap
RUN apt update && \
apt install -y --no-install-recommends tini openssl ca-certificates tzdata && \
apt install -y --no-install-recommends tini openssl ca-certificates tzdata jq curl jo && \
apt clean && \
rm -rf /var/lib/apt/lists/* && \
groupadd -g $GID $USER && useradd --system -m -g $USER --uid $UID $USER && \
@@ -74,6 +74,7 @@ COPY --from=lldap --chown=$USER:$USER /lldap /app
COPY --from=lldap --chown=$USER:$USER /docker-entrypoint.sh /docker-entrypoint.sh
VOLUME ["/data"]
WORKDIR /app
COPY scripts/bootstrap.sh ./
ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"]
CMD ["run", "--config-file", "/data/lldap_config.toml"]
HEALTHCHECK CMD ["/app/lldap", "healthcheck", "--config-file", "/data/lldap_config.toml"]
+3 -2
View File
@@ -1,5 +1,5 @@
# Keep tracking base image
FROM rust:1.74-slim-bookworm
FROM rust:1.89-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"
@@ -34,7 +34,8 @@ RUN wget -c https://musl.cc/x86_64-linux-musl-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
rustup target add armv7-unknown-linux-musleabihf && \
rustup target add x86_64-unknown-freebsd
CMD ["bash"]
+68 -40
View File
@@ -24,7 +24,7 @@ on:
env:
CARGO_TERM_COLOR: always
MSRV: "1.89.0"
### CI Docs
@@ -39,7 +39,7 @@ env:
# 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 #
# lldap/rust-dev #
#######################################################################################
# Cargo build
### armv7, aarch64 and amd64 is musl based
@@ -87,8 +87,14 @@ jobs:
image: lldap/rust-dev:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.1
- uses: actions/cache@v4
uses: actions/checkout@v6.0.2
- name: Install Rust
id: toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: "${{ env.MSRV }}"
targets: "wasm32-unknown-unknown"
- uses: actions/cache@v5
with:
path: |
/usr/local/cargo/bin
@@ -99,8 +105,6 @@ jobs:
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:
@@ -110,7 +114,7 @@ jobs:
- name: Check build path
run: ls -al app/
- name: Upload ui artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: ui
path: app/
@@ -132,8 +136,14 @@ jobs:
CARGO_HOME: ${GITHUB_WORKSPACE}/.cargo
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.1
- uses: actions/cache@v4
uses: actions/checkout@v6.0.2
- name: Install Rust
id: toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: "${{ env.MSRV }}"
targets: "${{ matrix.target }}"
- uses: actions/cache@v5
with:
path: |
.cargo/bin
@@ -149,17 +159,17 @@ jobs:
- name: Check path
run: ls -al target/release
- name: Upload ${{ matrix.target}} lldap artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.target}}-lldap-bin
path: target/${{ matrix.target }}/release/lldap
- name: Upload ${{ matrix.target }} migration tool artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
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
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.target }}-lldap_set_password-bin
path: target/${{ matrix.target }}/release/lldap_set_password
@@ -199,7 +209,7 @@ jobs:
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: x86_64-unknown-linux-musl-lldap-bin
path: bin/
@@ -216,6 +226,8 @@ jobs:
LLDAP_database_url: postgres://lldapuser:lldappass@localhost/lldap
LLDAP_ldap_port: 3890
LLDAP_http_port: 17170
LLDAP_JWT_SECRET: verysecret
LLDAP_LDAP_USER_PASS: password
- name: Run lldap with mariadb DB (MySQL Compatible) and healthcheck
@@ -227,6 +239,8 @@ jobs:
LLDAP_database_url: mysql://lldapuser:lldappass@localhost/lldap
LLDAP_ldap_port: 3891
LLDAP_http_port: 17171
LLDAP_JWT_SECRET: verysecret
LLDAP_LDAP_USER_PASS: password
- name: Run lldap with sqlite DB and healthcheck
@@ -238,6 +252,8 @@ jobs:
LLDAP_database_url: sqlite://users.db?mode=rwc
LLDAP_ldap_port: 3892
LLDAP_http_port: 17172
LLDAP_JWT_SECRET: verysecret
LLDAP_LDAP_USER_PASS: password
- name: Check DB container logs
run: |
@@ -294,18 +310,18 @@ jobs:
steps:
- name: Checkout scripts
uses: actions/checkout@v4.1.1
uses: actions/checkout@v6.0.2
with:
sparse-checkout: 'scripts'
- name: Download LLDAP artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: x86_64-unknown-linux-musl-lldap-bin
path: bin/
- name: Download LLDAP set password
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: x86_64-unknown-linux-musl-lldap_set_password-bin
path: bin/
@@ -324,9 +340,9 @@ jobs:
sleep 10s
bin/lldap healthcheck
env:
LLDAP_database_url: sqlite://users.db?mode=rwc
LLDAP_ldap_port: 3890
LLDAP_http_port: 17170
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
@@ -350,8 +366,11 @@ jobs:
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
env:
LLDAP_DATABASE_URL: postgres://lldapuser:lldappass@localhost:5432/lldap
LLDAP_JWT_SECRET: somejwtsecret
run: |
bin/lldap create_schema -d postgres://lldapuser:lldappass@localhost:5432/lldap
bin/lldap create_schema
- name: Copy converted db to postgress and import
run: |
@@ -368,7 +387,10 @@ jobs:
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
env:
LLDAP_DATABASE_URL: mysql://lldapuser:lldappass@localhost:3306/lldap
LLDAP_JWT_SECRET: somejwtsecret
run: bin/lldap create_schema
- name: Copy converted db to mariadb and import
run: |
@@ -384,7 +406,10 @@ jobs:
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
env:
LLDAP_DATABASE_URL: mysql://lldapuser:lldappass@localhost:3307/lldap
LLDAP_JWT_SECRET: somejwtsecret
run: bin/lldap create_schema
- name: Copy converted db to mysql and import
run: |
@@ -399,10 +424,9 @@ jobs:
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_DATABASE_URL: postgres://lldapuser:lldappass@localhost:5432/lldap
LLDAP_LDAP_PORT: 3891
LLDAP_HTTP_PORT: 17171
LLDAP_JWT_SECRET: somejwtsecret
- name: Run lldap with mariaDB and healthcheck again
@@ -411,9 +435,9 @@ jobs:
sleep 10s
bin/lldap healthcheck
env:
LLDAP_database_url: mysql://lldapuser:lldappass@localhost:3306/lldap
LLDAP_ldap_port: 3892
LLDAP_http_port: 17172
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
@@ -422,9 +446,9 @@ jobs:
sleep 10s
bin/lldap healthcheck
env:
LLDAP_database_url: mysql://lldapuser:lldappass@localhost:3307/lldap
LLDAP_ldap_port: 3893
LLDAP_http_port: 17173
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
@@ -482,15 +506,15 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.1
uses: actions/checkout@v6.0.2
- name: Download all artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
path: bin
- name: Download llap ui artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: ui
path: web
@@ -512,7 +536,7 @@ jobs:
tags: ${{ matrix.container }}-base
- name: Build ${{ matrix.container }} Base Docker Image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
# On PR will fail, force fully uncomment push: true, or docker image will fail for next steps
@@ -613,7 +637,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build ${{ matrix.container }}-rootless Docker Image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
@@ -627,7 +651,7 @@ jobs:
### 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
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
@@ -667,7 +691,7 @@ jobs:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
path: bin/
- name: Check file
@@ -688,7 +712,7 @@ jobs:
chmod +x bin/*-lldap_set_password
- name: Download llap ui artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: ui
path: web
@@ -738,5 +762,9 @@ jobs:
artifacts: aarch64-lldap.tar.gz,
amd64-lldap.tar.gz,
armhf-lldap.tar.gz
draft: true
omitBodyDuringUpdate: true
omitDraftDuringUpdate: true
omitNameDuringUpdate: true
env:
GITHUB_TOKEN: ${{ github.token }}
-20
View File
@@ -1,20 +0,0 @@
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.
+29 -29
View File
@@ -8,6 +8,7 @@ on:
env:
CARGO_TERM_COLOR: always
MSRV: "1.89.0"
jobs:
pre_job:
@@ -33,14 +34,19 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@v4.1.1
uses: actions/checkout@v6.0.2
- name: Install Rust
id: toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: "${{ env.MSRV }}"
- uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --verbose --workspace
run: cargo +${{steps.toolchain.outputs.name}} build --verbose --workspace
- name: Run tests
run: cargo test --verbose --workspace
run: cargo +${{steps.toolchain.outputs.name}} test --verbose --workspace
- name: Generate GraphQL schema
run: cargo run -- export_graphql_schema -o generated_schema.graphql
run: cargo +${{steps.toolchain.outputs.name}} run -- export_graphql_schema -o generated_schema.graphql
- name: Check schema
run: diff schema.graphql generated_schema.graphql || (echo "The schema file is out of date. Please run `./export_schema.sh`" && false)
@@ -52,15 +58,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4.1.1
- uses: Swatinem/rust-cache@v2
- name: Run cargo clippy
uses: actions-rs/cargo@v1
uses: actions/checkout@v6.0.2
- name: Install Rust
id: toolchain
uses: dtolnay/rust-toolchain@master
with:
command: clippy
args: --tests --all -- -D warnings
toolchain: "${{ env.MSRV }}"
components: clippy
- uses: Swatinem/rust-cache@v2
- run: cargo +${{steps.toolchain.outputs.name}} clippy --tests --workspace -- -D warnings
format:
name: cargo fmt
@@ -69,15 +75,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4.1.1
- uses: Swatinem/rust-cache@v2
- name: Run cargo fmt
uses: actions-rs/cargo@v1
uses: actions/checkout@v6.0.2
- name: Install Rust
id: toolchain
uses: dtolnay/rust-toolchain@master
with:
command: fmt
args: --all -- --check
toolchain: "${{ env.MSRV }}"
components: rustfmt
- uses: Swatinem/rust-cache@v2
- run: cargo +${{steps.toolchain.outputs.name}} fmt --check --all
coverage:
name: Code coverage
@@ -88,7 +94,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4.1.1
uses: actions/checkout@v6.0.2
- 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
@@ -101,16 +107,10 @@ jobs:
run: cargo llvm-cov --workspace --no-report
- name: Aggregate reports
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'
uses: codecov/codecov-action@v4
with:
files: lcov.info
fail_ci_if_error: true
codecov_yml_path: .github/codecov.yml
token: ${{ secrets.CODECOV_TOKEN }}
+5
View File
@@ -29,3 +29,8 @@ recipe.json
lldap_config.toml
cert.pem
key.pem
# Nix
result
result-*
.direnv
+157 -1
View File
@@ -5,6 +5,162 @@ 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).
## [0.6.2] 2025-07-21
Small release, focused on LDAP improvements and ongoing maintenance.
### Added
- LDAP
- Support for searching groups by their `groupid`
- Support for `whoamiOID`
- Support for creating groups
- Support for subschema entry
- Custom assets path.
- New endpoint for requesting client settings
### Changed
- A missing JWT secret now prevents startup.
- Attributes with invalid characters (such as underscores) cannot be created anymore.
- Searching custom (string) attributes is now case insensitive.
- Using the top-level `firstName`, `lastName` and `avatar` GraphQL fields for users is now deprecated. Use the `attributes` field instead.
### Fixed
- `lldap_set_password` now uses the system's SSL certificates.
### Cleanups
- Split the main `lldap` crate into many sub-crates
- Various dependency version bumps
- Upgraded to 2024 Rust edition
- Docs/FAQ improvements
### Bootstrap script
- Custom attributes support
- Read the paswsord from a file
- Resilient to no user or group files
### New services
- Discord integration (Discord role to LLDAP user)
- HashiCorp
- Jellyfin 2FA with Duo
- Kimai
- Mailcow
- Peertube
- Penpot
- PgAdmin
- Project Quay
- Quadlet
- Snipe-IT
- SSSD
- Stalwart
- UnifiOS
## [0.6.1] 2024-11-22
Small release, mainly to fix a migration issue with Sqlite and Postgresql.
### Added
- Added a link to a community terraform provider (#1035)
### Changed
- The opaque dependency now points to the official crate rather than a fork (#1040)
### Fixed
- Migration of the DB schema from 7 to 8 is now automatic for sqlite, and fixed for postgres (#1045)
- The startup warning about `key_seed` applying instead of `key_file` now has instructions on how to silence it (#1032)
### New services
- OneDev
## [0.6.0] 2024-11-09
### Breaking
- The endpoint `/auth/reset/step1` is now `POST` instead of `GET` (#704)
### Added
- Custom attributes are now supported (#67) ! You can add new fields (string, integers, JPEG or dates) to users and query them. That unlocks many integrations with other services, and allows for a deeper/more customized integration. Special thanks to @pixelrazor and @bojidar-bg for their help with the UI.
- Custom object classes (for all users/groups) can now be added (#833)
- Barebones support for Paged Results Control (no paging, no respect for windows, but a correct response with all the results) (#698)
- A daily docker image is tagged and released. (#613)
- A bootstrap script allows reading the list of users/groups from a file and making sure the server contains exactly the same thing. (#654)
- Make it possible to serve lldap behind a sub-path in (#752)
- LLDAP can now be found on a custom package repository for opensuse, fedora, ubuntu, debian and centos ([Repository link](https://software.opensuse.org//download.html?project=home%3AMasgalor%3ALLDAP&package=lldap)). Thanks @Masgalor for setting it up and maintaining it.
- There's now an option to force reset the admin password (#748) optionally on every restart (#959)
- There's a rootless docker container (#755)
- entryDN is now supported (#780)
- Unknown LDAP controls are now detected and ignored (#787, #799)
- A community-developed CLI for scripting (#793)
- Added a way to print raw logs to debug long-running sessions (#992)
### Changed
- The official docker repository is now `lldap/lldap`
- Removed password length limitation in lldap_set_password tool
- Group names and emails are now case insensitive, but keep their casing (#666)
- Better error messages (and exit code (#745)) when changing the private key (#778, #1008), using the wrong SMTP port (#970), using the wrong env variables (#972)
- Allow `member=` filters with plain user names (not full DNs) (#949)
- Correctly detect and refuse anonymous binds (#974)
- Clearer logging (#971, #981, #982)
### Fixed
- Logging out applies globally, not just in the local browser. (#721)
- It's no longer possible to create the same user twice (#745)
- Fix wide substring filters (#738)
- Don't log the database password if provided in the connection URL (#735)
- Fix a panic when postgres uses a different collation (#821)
- The UI now defaults to the user ID for users with no display names (#843)
- Fix searching for users with more than one `memberOf` filter (#872)
- Fix compilation on Windows (#932) and Illumos (#964)
- The UI now correctly detects whether password resets are enabled. (#753)
- Fix a missing lowercasing of username when changing passwords through LDAP (#1012)
- Fix SQLite writers erroring when racing (#1021)
- LDAP sessions no longer buffer their logs until unbind, causing memory leaks (#1025)
### Performance
- Only expand attributes once per query, not per result (#687)
### Security
- When asked to send a password reset to an unknown email, sleep for 3 seconds and don't print the email in the error (#887)
### New services
Linux user accounts can now be managed by LLDAP, using PAM and nslcd.
- Apereo CAS server
- Carpal
- Gitlab
- Grocy
- Harbor
- Home Assistant
- Jenkins
- Kasm
- Maddy
- Mastodon
- Metabase
- MegaRAC-BMC
- Netbox
- OCIS
- Prosody
- Radicale
- SonarQube
- Traccar
- Zitadel
## [0.5.0] 2023-09-14
### Breaking
@@ -71,7 +227,7 @@ systems, including PAM authentication.
## [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
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`.
+3 -1
View File
@@ -46,7 +46,9 @@ 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).
[good first issues](https://github.com/lldap/lldap/labels/good%20first%20issue).
For an alternative development environment setup, see [docs/nix-development.md](docs/nix-development.md).
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
Generated
+2564 -1291
View File
File diff suppressed because it is too large Load Diff
+19 -12
View File
@@ -1,25 +1,32 @@
[workspace]
members = [
"server",
"auth",
"app",
"migration-tool",
"set-password",
"server",
"app",
"migration-tool",
"set-password",
"crates/*",
]
default-members = ["server"]
resolver = "2"
[workspace.package]
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
documentation = "https://github.com/lldap/lldap"
edition = "2024"
homepage = "https://github.com/lldap/lldap"
license = "GPL-3.0-only"
repository = "https://github.com/lldap/lldap"
rust-version = "1.89.0"
[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'
[workspace.dependencies.sea-orm]
version = "1.1.8"
default-features = false
[patch.crates-io.lber]
git = 'https://github.com/inejge/ldap3/'
[workspace.dependencies.serde]
version = "1"
+4 -3
View File
@@ -1,5 +1,5 @@
# Build image
FROM rust:alpine3.16 AS chef
FROM rust:alpine3.21 AS chef
RUN set -x \
# Add user
@@ -41,9 +41,9 @@ RUN cargo build --release -p lldap -p lldap_migration_tool -p lldap_set_password
&& ./app/build.sh
# Final image
FROM alpine:3.16
FROM alpine:3.21
ENV GOSU_VERSION 1.14
ENV GOSU_VERSION=1.14
# Fetch gosu from git
RUN set -eux; \
\
@@ -80,6 +80,7 @@ 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/lldap_migration_tool /app/target/release/lldap_set_password ./
COPY docker-entrypoint.sh lldap_config.docker_template.toml ./
COPY scripts/bootstrap.sh ./
RUN set -x \
&& apk add --no-cache bash tzdata \
+5
View File
@@ -0,0 +1,5 @@
build-dev-container:
docker buildx build --tag lldap/rust-dev --file .github/workflows/Dockerfile.dev --push .github/workflows
prepare-release:
./prepare-release.sh
+35 -323
View File
@@ -34,27 +34,14 @@
</p>
- [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)
- [Installation](docs/install.md)
- [Usage](#usage)
- [Recommended architecture](#recommended-architecture)
- [Client configuration](#client-configuration)
- [Compatible services](#compatible-services)
- [Known compatible services](#known-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)
- [Frequently Asked Questions](#frequently-asked-questions)
- [Contributions](#contributions)
## About
@@ -96,179 +83,9 @@ MySQL/MariaDB or PostgreSQL.
## Installation
### With Docker
It's possible to install lldap from OCI images ([docker](docs/install.md#with-docker)/[podman](docs/install.md#with-podman)), from [Kubernetes](docs/install.md#with-kubernetes), [TrueNAS](docs/install.md#truenas-scale), or from [a regular distribution package manager](docs/install.md/#from-a-package-repository) (Archlinux, Debian, CentOS, Fedora, OpenSuse, Ubuntu, FreeBSD).
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
`jwt_secret` and `ldap_user_pass`, unless you override them with env variables).
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_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: lldap/lldap:stable
ports:
# 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:
- "lldap_data:/data"
# Alternatively, you can mount a local folder
# - "./lldap_data:/data"
environment:
- UID=####
- GID=####
- TZ=####/####
- LLDAP_JWT_SECRET=REPLACE_WITH_RANDOM
- 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 WASM-pack, which can be installed by running `cargo install wasm-pack`.
Then you can build the frontend files with
```shell
./app/build.sh
```
(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
Docker images are provided for AMD64, ARM64 and ARM/V7.
If you want to cross-compile yourself, you can do so by installing
[`cross`](https://github.com/rust-embedded/cross):
```sh
cargo install cross
cross build --target=armv7-unknown-linux-musleabihf -p lldap --release
./app/build.sh
```
(Replace `armv7-unknown-linux-musleabihf` with the correct Rust target for your
device.)
You can then get the compiled server binary in
`target/armv7-unknown-linux-musleabihf/release/lldap` and the various needed files
(`index.html`, `main.js`, `pkg` folder) in the `app` folder. Copy them to the
Raspberry Pi (or other target), with the folder structure maintained (`app`
files in an `app` folder next to the binary).
Building [from source](docs/install.md#from-source) and [cross-compiling](docs/install.md#cross-compilation) to a different hardware architecture is also supported.
## Usage
@@ -277,10 +94,16 @@ 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.
You can create and manage custom attributes through the Web UI, or through the
community-contributed CLI frontend (
[Zepmann/lldap-cli](https://github.com/Zepmann/lldap-cli)). This is necessary
for some service integrations.
The [bootstrap.sh](scripts/bootstrap.sh) script can enforce a list of
users/groups/attributes from a given file, reflecting it on the server.
To manage the user, group and membership lifecycle in an infrastructure-as-code
scenario you can use the unofficial [LLDAP terraform provider in the terraform registry](https://registry.terraform.io/providers/tasansga/lldap/latest).
LLDAP is also very scriptable, through its GraphQL API. See the
[Scripting](docs/scripting.md) docs for more info.
@@ -313,7 +136,7 @@ If you are using containers, a sample architecture could look like this:
## Client configuration
### Compatible services
### Known compatible services
Most services that can use LDAP as an authentication provider should work out
of the box. For new services, it's possible that they require a bit of tweaking
@@ -321,6 +144,13 @@ on LLDAP's side to make things work. In that case, just create an issue with
the relevant details (logs of the service, LLDAP logs with `verbose=true` in
the config).
Some specific clients have been tested to work and come with sample
configuration files, or guides. See the [`example_configs`](example_configs/README.md)
folder for example configs for integration with specific services.
Integration with Linux accounts is possible, through PAM and nslcd. See [PAM
configuration guide](example_configs/pam/README.md). Integration with Windows (e.g. Samba) is WIP.
### General configuration guide
To configure the services that will talk to LLDAP, here are the values:
@@ -340,67 +170,9 @@ filter like: `(memberOf=cn=admins,ou=groups,dc=example,dc=com)`.
The administrator group for LLDAP is `lldap_admin`: anyone in this group has
admin rights in the Web UI. Most LDAP integrations should instead use a user in
the `lldap_strict_readonly` or `lldap_password_manager` group, to avoid granting full
administration access to many services.
### Sample client configurations
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:
- [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)
- [Maddy](example_configs/maddy.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)
- [Radicale](example_configs/radicale.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)
administration access to many services. To prevent privilege escalation users in the
`lldap_password_manager` group are not allowed to change passwords of admins in the
`lldap_admin` group.
### Incompatible services
@@ -423,76 +195,16 @@ 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
## Frequently Asked Questions
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](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 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 web UI.
However, it's not as flexible as OpenLDAP.
### vs FreeIPA
[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 (<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.
- [I can't login](docs/faq.md#i-cant-log-in)
- [Discord Integration](docs/faq.md#discord-integration)
- [Migrating from SQLite](docs/faq.md#migrating-from-sqlite)
- How does lldap compare [with OpenLDAP](docs/faq.md#how-does-lldap-compare-with-openldap)? [With FreeIPA](docs/faq.md#how-does-lldap-compare-with-freeipa)? [With Kanidm](docs/faq.md#how-does-lldap-compare-with-kanidm)?
- [Does lldap support vhosts?](docs/faq.md#does-lldap-support-vhosts)
- [Does lldap provide commercial support contracts?](docs/faq.md#does-lldap-provide-commercial-support-contracts)
- [Can I make a donation to fund development?](docs/faq.md#can-i-make-a-donation-to-fund-development)
- [Is lldap sustainable? Can we depend on it for our infrastructure?](docs/faq.md#is-lldap-sustainable-can-we-depend-on-it-for-our-infrastructure)
## Contributions
+46 -12
View File
@@ -1,13 +1,14 @@
[package]
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"
version = "0.6.2"
description = "Frontend for LLDAP"
edition.workspace = true
include = ["src/**/*", "queries/**/*", "Cargo.toml", "../schema.graphql"]
authors.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow = "1"
@@ -19,12 +20,11 @@ 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"
validator = "0.14"
validator_derive = "0.14"
wasm-bindgen = "0.2.100"
wasm-bindgen-futures = "*"
yew = "0.19.3"
yew-router = "0.16"
@@ -37,12 +37,16 @@ version = "0.3"
features = [
"Document",
"Element",
"Event",
"FileReader",
"FormData",
"HtmlDocument",
"HtmlFormElement",
"HtmlInputElement",
"HtmlOptionElement",
"HtmlOptionsCollection",
"HtmlSelectElement",
"SubmitEvent",
"console",
]
@@ -52,15 +56,33 @@ features = [
"wasmbind"
]
[dependencies.derive_more]
features = ["debug", "display", "from", "from_str"]
default-features = false
version = "1"
[dependencies.lldap_auth]
path = "../auth"
path = "../crates/auth"
features = [ "opaque_client" ]
[dependencies.lldap_frontend_options]
path = "../crates/frontend-options"
[dependencies.lldap_validation]
path = "../crates/validation"
[dependencies.image]
features = ["jpeg"]
default-features = false
version = "0.24"
[dependencies.serde]
workspace = true
[dependencies.strum]
features = ["derive"]
version = "0.25"
[dependencies.yew_form]
git = "https://github.com/jfbilodeau/yew_form"
rev = "4b9fabffb63393ec7626a4477fd36de12a07fac9"
@@ -71,3 +93,15 @@ rev = "4b9fabffb63393ec7626a4477fd36de12a07fac9"
[lib]
crate-type = ["cdylib"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(wasm_bindgen_unstable_test_coverage)',
] }
[package.metadata.wasm-pack.profile.dev]
wasm-opt = ['--enable-bulk-memory']
[package.metadata.wasm-pack.profile.profiling]
wasm-opt = ['--enable-bulk-memory']
[package.metadata.wasm-pack.profile.release]
wasm-opt = ['--enable-bulk-memory']
+2 -2
View File
@@ -1,5 +1,5 @@
mutation CreateGroup($name: String!) {
createGroup(name: $name) {
mutation CreateGroup($group: CreateGroupInput!) {
createGroupWithDetails(request: $group) {
id
displayName
}
@@ -7,6 +7,7 @@ query GetGroupAttributesSchema {
isList
isVisible
isHardcoded
isReadonly
}
}
}
+17
View File
@@ -8,5 +8,22 @@ query GetGroupDetails($id: Int!) {
id
displayName
}
attributes {
name
value
}
}
schema {
groupSchema {
attributes {
name
attributeType
isList
isVisible
isEditable
isHardcoded
isReadonly
}
}
}
}
@@ -8,6 +8,7 @@ query GetUserAttributesSchema {
isVisible
isEditable
isHardcoded
isReadonly
}
}
}
+18 -3
View File
@@ -2,15 +2,30 @@ query GetUserDetails($id: String!) {
user(userId: $id) {
id
email
displayName
firstName
lastName
avatar
displayName
creationDate
uuid
groups {
id
displayName
}
attributes {
name
value
}
}
schema {
userSchema {
attributes {
name
attributeType
isList
isVisible
isEditable
isHardcoded
isReadonly
}
}
}
}
+6
View File
@@ -0,0 +1,6 @@
mutation UpdateGroup($group: UpdateGroupInput!) {
updateGroup(group: $group) {
ok
}
}
+6 -1
View File
@@ -155,8 +155,13 @@ impl Component for AddGroupMemberComponent {
let to_add_user_list = self.get_selectable_user_list(ctx, user_list);
#[allow(unused_braces)]
let make_select_option = |user: User| {
let name = if user.display_name.is_empty() {
user.id.clone()
} else {
user.display_name.clone()
};
html_nested! {
<SelectOption value={user.id.clone()} text={user.display_name.clone()} key={user.id} />
<SelectOption value={user.id.clone()} text={name} key={user.id} />
}
};
html! {
+47 -35
View File
@@ -21,16 +21,16 @@ use crate::{
};
use gloo_console::error;
use lldap_frontend_options::Options;
use yew::{
function_component,
Context, function_component,
html::Scope,
prelude::{html, Component, Html},
Context,
prelude::{Component, Html, html},
};
use yew_router::{
BrowserRouter, Switch,
prelude::{History, Location},
scope_ext::RouterScopeExt,
BrowserRouter, Switch,
};
#[function_component(AppContainer)]
@@ -51,7 +51,7 @@ pub struct App {
pub enum Msg {
Login((String, bool)),
Logout,
PasswordResetProbeFinished(anyhow::Result<bool>),
SettingsReceived(anyhow::Result<Options>),
}
impl Component for App {
@@ -76,9 +76,8 @@ impl Component for App {
redirect_to: Self::get_redirect_route(ctx),
password_reset_enabled: None,
};
ctx.link().send_future(async move {
Msg::PasswordResetProbeFinished(HostService::probe_password_reset().await)
});
ctx.link()
.send_future(async move { Msg::SettingsReceived(HostService::get_settings().await) });
app.apply_initial_redirections(ctx);
app
}
@@ -103,14 +102,11 @@ impl Component for App {
self.redirect_to = None;
history.push(AppRoute::Login);
}
Msg::PasswordResetProbeFinished(Ok(enabled)) => {
self.password_reset_enabled = Some(enabled);
Msg::SettingsReceived(Ok(settings)) => {
self.password_reset_enabled = Some(settings.password_reset_enabled);
}
Msg::PasswordResetProbeFinished(Err(err)) => {
self.password_reset_enabled = Some(false);
error!(&format!(
"Could not probe for password reset support: {err:#}"
));
Msg::SettingsReceived(Err(err)) => {
error!(err.to_string());
}
}
true
@@ -126,7 +122,7 @@ impl Component for App {
<Banner is_admin={is_admin} username={username} on_logged_out={link.callback(|_| Msg::Logout)} />
<div class="container py-3 bg-kug">
<div class="row justify-content-center" style="padding-bottom: 80px;">
<main class="py-3" style="max-width: 1000px">
<main class="py-3">
<Switch<AppRoute>
render={Switch::render(move |routes| Self::dispatch_route(routes, &link, is_admin, password_reset_enabled))}
/>
@@ -200,15 +196,23 @@ impl App {
AppRoute::CreateUser => html! {
<CreateUserForm/>
},
AppRoute::Index | AppRoute::ListUsers => html! {
<div>
<UserTable />
<Link classes="btn btn-primary" to={AppRoute::CreateUser}>
<i class="bi-person-plus me-2"></i>
{"Create a user"}
</Link>
</div>
},
AppRoute::Index | AppRoute::ListUsers => {
let user_button = |key| {
html! {
<Link classes="btn btn-primary" key={key} to={AppRoute::CreateUser}>
<i class="bi-person-plus me-2"></i>
{"Create a user"}
</Link>
}
};
html! {
<div>
{ user_button("top-create-user") }
<UserTable />
{ user_button("bottom-create-user") }
</div>
}
}
AppRoute::CreateGroup => html! {
<CreateGroupForm/>
},
@@ -218,15 +222,23 @@ impl App {
AppRoute::CreateGroupAttribute => html! {
<CreateGroupAttributeForm/>
},
AppRoute::ListGroups => html! {
<div>
<GroupTable />
<Link classes="btn btn-primary" to={AppRoute::CreateGroup}>
<i class="bi-plus-circle me-2"></i>
{"Create a group"}
</Link>
</div>
},
AppRoute::ListGroups => {
let group_button = |key| {
html! {
<Link classes="btn btn-primary" key={key} to={AppRoute::CreateGroup}>
<i class="bi-plus-circle me-2"></i>
{"Create a group"}
</Link>
}
};
html! {
<div>
{ group_button("top-create-group") }
<GroupTable />
{ group_button("bottom-create-group") }
</div>
}
}
AppRoute::ListUserSchema => html! {
<ListUserSchema />
},
@@ -234,7 +246,7 @@ impl App {
<ListGroupSchema />
},
AppRoute::GroupDetails { group_id } => html! {
<GroupDetails group_id={*group_id} />
<GroupDetails group_id={*group_id} is_admin={is_admin} />
},
AppRoute::UserDetails { user_id } => html! {
<UserDetails username={user_id.clone()} is_admin={is_admin} />
+3 -2
View File
@@ -1,11 +1,12 @@
use crate::infra::functional::{use_graphql_call, LoadableResult};
use crate::infra::functional::{LoadableResult, use_graphql_call};
use graphql_client::GraphQLQuery;
use yew::{function_component, html, virtual_dom::AttrValue, Properties};
use yew::{Properties, function_component, html, virtual_dom::AttrValue};
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "queries/get_user_details.graphql",
variables_derives = "Clone,PartialEq,Eq",
response_derives = "Debug, Hash, PartialEq, Eq, Clone",
custom_scalars_module = "crate::infra::graphql"
)]
+1 -1
View File
@@ -4,7 +4,7 @@ use crate::components::{
router::{AppRoute, Link},
};
use wasm_bindgen::prelude::wasm_bindgen;
use yew::{function_component, html, Callback, Properties};
use yew::{Callback, Properties, function_component, html};
#[derive(Properties, PartialEq)]
pub struct Props {
+1 -1
View File
@@ -8,7 +8,7 @@ use crate::{
common_component::{CommonComponent, CommonComponentParts},
},
};
use anyhow::{anyhow, bail, Result};
use anyhow::{Result, anyhow, bail};
use gloo_console::error;
use lldap_auth::*;
use validator_derive::Validate;
+116 -12
View File
@@ -1,11 +1,22 @@
use crate::{
components::{
form::{field::Field, submit::Submit},
form::{
attribute_input::{ListAttributeInput, SingleAttributeInput},
field::Field,
submit::Submit,
},
router::AppRoute,
},
infra::common_component::{CommonComponent, CommonComponentParts},
infra::{
common_component::{CommonComponent, CommonComponentParts},
form_utils::{
AttributeValue, EmailIsRequired, GraphQlAttributeSchema, IsAdmin,
read_all_form_attributes,
},
schema::AttributeType,
},
};
use anyhow::{bail, Result};
use anyhow::{Result, ensure};
use gloo_console::log;
use graphql_client::GraphQLQuery;
use validator_derive::Validate;
@@ -13,6 +24,32 @@ use yew::prelude::*;
use yew_form_derive::Model;
use yew_router::{prelude::History, scope_ext::RouterScopeExt};
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "queries/get_group_attributes_schema.graphql",
response_derives = "Debug,Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct GetGroupAttributesSchema;
use get_group_attributes_schema::ResponseData;
pub type Attribute =
get_group_attributes_schema::GetGroupAttributesSchemaSchemaGroupSchemaAttributes;
impl From<&Attribute> for GraphQlAttributeSchema {
fn from(attr: &Attribute) -> Self {
Self {
name: attr.name.clone(),
is_list: attr.is_list,
is_readonly: attr.is_readonly,
is_editable: false, // Need to be admin to edit it.
}
}
}
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
@@ -25,6 +62,8 @@ pub struct CreateGroup;
pub struct CreateGroupForm {
common: CommonComponentParts<Self>,
form: yew_form::Form<CreateGroupModel>,
attributes_schema: Option<Vec<Attribute>>,
form_ref: NodeRef,
}
#[derive(Model, Validate, PartialEq, Eq, Clone, Default)]
@@ -35,6 +74,7 @@ pub struct CreateGroupModel {
pub enum Msg {
Update,
ListAttributesResponse(Result<ResponseData>),
SubmitForm,
CreateGroupResponse(Result<create_group::ResponseData>),
}
@@ -48,12 +88,33 @@ impl CommonComponent<CreateGroupForm> for CreateGroupForm {
match msg {
Msg::Update => Ok(true),
Msg::SubmitForm => {
if !self.form.validate() {
bail!("Check the form for errors");
}
ensure!(self.form.validate(), "Check the form for errors");
let all_values = read_all_form_attributes(
self.attributes_schema.iter().flatten(),
&self.form_ref,
IsAdmin(true),
EmailIsRequired(false),
)?;
let attributes = Some(
all_values
.into_iter()
.filter(|a| !a.values.is_empty())
.map(
|AttributeValue { name, values }| create_group::AttributeValueInput {
name,
value: values,
},
)
.collect(),
);
let model = self.form.model();
let req = create_group::Variables {
name: model.groupname,
group: create_group::CreateGroupInput {
displayName: model.groupname,
attributes,
},
};
self.common.call_graphql::<CreateGroup, _>(
ctx,
@@ -66,11 +127,16 @@ impl CommonComponent<CreateGroupForm> for CreateGroupForm {
Msg::CreateGroupResponse(response) => {
log!(&format!(
"Created group '{}'",
&response?.create_group.display_name
&response?.create_group_with_details.display_name
));
ctx.link().history().unwrap().push(AppRoute::ListGroups);
Ok(true)
}
Msg::ListAttributesResponse(schema) => {
self.attributes_schema =
Some(schema?.schema.group_schema.attributes.into_iter().collect());
Ok(true)
}
}
}
@@ -83,11 +149,22 @@ impl Component for CreateGroupForm {
type Message = Msg;
type Properties = ();
fn create(_: &Context<Self>) -> Self {
Self {
fn create(ctx: &Context<Self>) -> Self {
let mut component = Self {
common: CommonComponentParts::<Self>::create(),
form: yew_form::Form::<CreateGroupModel>::new(CreateGroupModel::default()),
}
attributes_schema: None,
form_ref: NodeRef::default(),
};
component
.common
.call_graphql::<GetGroupAttributesSchema, _>(
ctx,
get_group_attributes_schema::Variables {},
Msg::ListAttributesResponse,
"Error trying to fetch group schema",
);
component
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
@@ -98,7 +175,8 @@ impl Component for CreateGroupForm {
let link = ctx.link();
html! {
<div class="row justify-content-center">
<form class="form py-3" style="max-width: 636px">
<form class="form py-3" style="max-width: 636px"
ref={self.form_ref.clone()}>
<div class="row mb-3">
<h5 class="fw-bold">{"Create a group"}</h5>
</div>
@@ -108,6 +186,14 @@ impl Component for CreateGroupForm {
label="Group name"
field_name="groupname"
oninput={link.callback(|_| Msg::Update)} />
{
self.attributes_schema
.iter()
.flatten()
.filter(|a| !a.is_readonly && a.name != "display_name")
.map(get_custom_attribute_input)
.collect::<Vec<_>>()
}
<Submit
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})} />
@@ -124,3 +210,21 @@ impl Component for CreateGroupForm {
}
}
}
fn get_custom_attribute_input(attribute_schema: &Attribute) -> Html {
if attribute_schema.is_list {
html! {
<ListAttributeInput
name={attribute_schema.name.clone()}
attribute_type={attribute_schema.attribute_type}
/>
}
} else {
html! {
<SingleAttributeInput
name={attribute_schema.name.clone()}
attribute_type={attribute_schema.attribute_type}
/>
}
}
}
+16 -9
View File
@@ -3,15 +3,15 @@ use crate::{
form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit},
router::AppRoute,
},
convert_attribute_type,
infra::{
common_component::{CommonComponent, CommonComponentParts},
schema::{validate_attribute_type, AttributeType},
schema::{AttributeType, validate_attribute_type},
},
};
use anyhow::{bail, Result};
use anyhow::{Result, bail};
use gloo_console::log;
use graphql_client::GraphQLQuery;
use lldap_validation::attributes::validate_attribute_name;
use validator_derive::Validate;
use yew::prelude::*;
use yew_form_derive::Model;
@@ -22,12 +22,11 @@ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
schema_path = "../schema.graphql",
query_path = "queries/create_group_attribute.graphql",
response_derives = "Debug",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct CreateGroupAttribute;
convert_attribute_type!(create_group_attribute::AttributeType);
pub struct CreateGroupAttributeForm {
common: CommonComponentParts<Self>,
form: yew_form::Form<CreateGroupAttributeModel>,
@@ -62,10 +61,18 @@ impl CommonComponent<CreateGroupAttributeForm> for CreateGroupAttributeForm {
bail!("Check the form for errors");
}
let model = self.form.model();
let attribute_type = model.attribute_type.parse::<AttributeType>().unwrap();
validate_attribute_name(&model.attribute_name).or_else(|invalid_chars| {
let invalid = String::from_iter(invalid_chars);
bail!(
"Attribute name contains one or more invalid characters: {}",
invalid
);
})?;
let attribute_type =
AttributeType::try_from(model.attribute_type.as_str()).unwrap();
let req = create_group_attribute::Variables {
name: model.attribute_name,
attribute_type: create_group_attribute::AttributeType::from(attribute_type),
attribute_type,
is_list: model.is_list,
is_visible: model.is_visible,
};
@@ -137,7 +144,7 @@ impl Component for CreateGroupAttributeForm {
oninput={link.callback(|_| Msg::Update)}>
<option selected=true value="String">{"String"}</option>
<option value="Integer">{"Integer"}</option>
<option value="Jpeg">{"Jpeg"}</option>
<option value="JpegPhoto">{"Jpeg"}</option>
<option value="DateTime">{"DateTime"}</option>
</Select<CreateGroupAttributeModel>>
<CheckBox<CreateGroupAttributeModel>
+114 -45
View File
@@ -1,14 +1,23 @@
use crate::{
components::{
form::{field::Field, submit::Submit},
form::{
attribute_input::{ListAttributeInput, SingleAttributeInput},
field::Field,
submit::Submit,
},
router::AppRoute,
},
infra::{
api::HostService,
common_component::{CommonComponent, CommonComponentParts},
form_utils::{
AttributeValue, EmailIsRequired, GraphQlAttributeSchema, IsAdmin,
read_all_form_attributes,
},
schema::AttributeType,
},
};
use anyhow::{bail, Result};
use anyhow::{Result, ensure};
use gloo_console::log;
use graphql_client::GraphQLQuery;
use lldap_auth::{opaque, registration};
@@ -17,6 +26,31 @@ use yew::prelude::*;
use yew_form_derive::Model;
use yew_router::{prelude::History, scope_ext::RouterScopeExt};
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "queries/get_user_attributes_schema.graphql",
response_derives = "Debug,Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct GetUserAttributesSchema;
use get_user_attributes_schema::ResponseData;
pub type Attribute = get_user_attributes_schema::GetUserAttributesSchemaSchemaUserSchemaAttributes;
impl From<&Attribute> for GraphQlAttributeSchema {
fn from(attr: &Attribute) -> Self {
Self {
name: attr.name.clone(),
is_list: attr.is_list,
is_readonly: attr.is_readonly,
is_editable: attr.is_editable,
}
}
}
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
@@ -29,17 +63,14 @@ pub struct CreateUser;
pub struct CreateUserForm {
common: CommonComponentParts<Self>,
form: yew_form::Form<CreateUserModel>,
attributes_schema: Option<Vec<Attribute>>,
form_ref: NodeRef,
}
#[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,
display_name: String,
first_name: String,
last_name: String,
#[validate(custom(
function = "empty_or_long",
message = "Password should be longer than 8 characters (or left empty)"
@@ -59,6 +90,7 @@ fn empty_or_long(value: &str) -> Result<(), validator::ValidationError> {
pub enum Msg {
Update,
ListAttributesResponse(Result<ResponseData>),
SubmitForm,
CreateUserResponse(Result<create_user::ResponseData>),
SuccessfulCreation,
@@ -79,21 +111,43 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
) -> Result<bool> {
match msg {
Msg::Update => Ok(true),
Msg::ListAttributesResponse(schema) => {
self.attributes_schema =
Some(schema?.schema.user_schema.attributes.into_iter().collect());
Ok(true)
}
Msg::SubmitForm => {
if !self.form.validate() {
bail!("Check the form for errors");
}
ensure!(self.form.validate(), "Check the form for errors");
let all_values = read_all_form_attributes(
self.attributes_schema.iter().flatten(),
&self.form_ref,
IsAdmin(true),
EmailIsRequired(true),
)?;
let attributes = Some(
all_values
.into_iter()
.filter(|a| !a.values.is_empty())
.map(
|AttributeValue { name, values }| create_user::AttributeValueInput {
name,
value: values,
},
)
.collect(),
);
let model = self.form.model();
let to_option = |s: String| if s.is_empty() { None } else { Some(s) };
let req = create_user::Variables {
user: create_user::CreateUserInput {
id: model.username,
email: model.email,
displayName: to_option(model.display_name),
firstName: to_option(model.first_name),
lastName: to_option(model.last_name),
email: None,
displayName: None,
firstName: None,
lastName: None,
avatar: None,
attributes: None,
attributes,
},
};
self.common.call_graphql::<CreateUser, _>(
@@ -177,11 +231,20 @@ impl Component for CreateUserForm {
type Message = Msg;
type Properties = ();
fn create(_: &Context<Self>) -> Self {
Self {
fn create(ctx: &Context<Self>) -> Self {
let mut component = Self {
common: CommonComponentParts::<Self>::create(),
form: yew_form::Form::<CreateUserModel>::new(CreateUserModel::default()),
}
attributes_schema: None,
form_ref: NodeRef::default(),
};
component.common.call_graphql::<GetUserAttributesSchema, _>(
ctx,
get_user_attributes_schema::Variables {},
Msg::ListAttributesResponse,
"Error trying to fetch user schema",
);
component
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
@@ -192,38 +255,22 @@ impl Component for CreateUserForm {
let link = &ctx.link();
html! {
<div class="row justify-content-center">
<form class="form py-3" style="max-width: 636px">
<form class="form py-3"
ref={self.form_ref.clone()}>
<Field<CreateUserModel>
form={&self.form}
required=true
label="User name"
field_name="username"
oninput={link.callback(|_| Msg::Update)} />
<Field<CreateUserModel>
form={&self.form}
required=true
label="Email"
field_name="email"
input_type="email"
oninput={link.callback(|_| Msg::Update)} />
<Field<CreateUserModel>
form={&self.form}
label="Display name"
field_name="display_name"
autocomplete="name"
oninput={link.callback(|_| Msg::Update)} />
<Field<CreateUserModel>
form={&self.form}
label="First name"
field_name="first_name"
autocomplete="given-name"
oninput={link.callback(|_| Msg::Update)} />
<Field<CreateUserModel>
form={&self.form}
label="Last name"
field_name="last_name"
autocomplete="family-name"
oninput={link.callback(|_| Msg::Update)} />
{
self.attributes_schema
.iter()
.flatten()
.filter(|a| !a.is_readonly)
.map(get_custom_attribute_input)
.collect::<Vec<_>>()
}
<Field<CreateUserModel>
form={&self.form}
label="Password"
@@ -255,3 +302,25 @@ impl Component for CreateUserForm {
}
}
}
fn get_custom_attribute_input(attribute_schema: &Attribute) -> Html {
let mail_is_required = attribute_schema.name.as_str() == "mail";
if attribute_schema.is_list {
html! {
<ListAttributeInput
name={attribute_schema.name.clone()}
attribute_type={attribute_schema.attribute_type}
required={mail_is_required}
/>
}
} else {
html! {
<SingleAttributeInput
name={attribute_schema.name.clone()}
attribute_type={attribute_schema.attribute_type}
required={mail_is_required}
/>
}
}
}
+16 -9
View File
@@ -3,15 +3,15 @@ use crate::{
form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit},
router::AppRoute,
},
convert_attribute_type,
infra::{
common_component::{CommonComponent, CommonComponentParts},
schema::{validate_attribute_type, AttributeType},
schema::{AttributeType, validate_attribute_type},
},
};
use anyhow::{bail, Result};
use anyhow::{Result, bail};
use gloo_console::log;
use graphql_client::GraphQLQuery;
use lldap_validation::attributes::validate_attribute_name;
use validator_derive::Validate;
use yew::prelude::*;
use yew_form_derive::Model;
@@ -22,12 +22,11 @@ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
schema_path = "../schema.graphql",
query_path = "queries/create_user_attribute.graphql",
response_derives = "Debug",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct CreateUserAttribute;
convert_attribute_type!(create_user_attribute::AttributeType);
pub struct CreateUserAttributeForm {
common: CommonComponentParts<Self>,
form: yew_form::Form<CreateUserAttributeModel>,
@@ -66,10 +65,18 @@ impl CommonComponent<CreateUserAttributeForm> for CreateUserAttributeForm {
if model.is_editable && !model.is_visible {
bail!("Editable attributes must also be visible");
}
let attribute_type = model.attribute_type.parse::<AttributeType>().unwrap();
validate_attribute_name(&model.attribute_name).or_else(|invalid_chars| {
let invalid = String::from_iter(invalid_chars);
bail!(
"Attribute name contains one or more invalid characters: {}",
invalid
);
})?;
let attribute_type =
AttributeType::try_from(model.attribute_type.as_str()).unwrap();
let req = create_user_attribute::Variables {
name: model.attribute_name,
attribute_type: create_user_attribute::AttributeType::from(attribute_type),
attribute_type,
is_editable: model.is_editable,
is_list: model.is_list,
is_visible: model.is_visible,
@@ -139,7 +146,7 @@ impl Component for CreateUserAttributeForm {
oninput={link.callback(|_| Msg::Update)}>
<option selected=true value="String">{"String"}</option>
<option value="Integer">{"Integer"}</option>
<option value="Jpeg">{"Jpeg"}</option>
<option value="JpegPhoto">{"Jpeg"}</option>
<option value="DateTime">{"DateTime"}</option>
</Select<CreateUserAttributeModel>>
<CheckBox<CreateUserAttributeModel>
+198
View File
@@ -0,0 +1,198 @@
use crate::{
components::form::{date_input::DateTimeInput, file_input::JpegFileInput},
infra::{schema::AttributeType, tooltip::Tooltip},
};
use web_sys::Element;
use yew::{
Component, Context, Html, Properties, function_component, html, use_effect_with_deps,
use_node_ref, virtual_dom::AttrValue,
};
#[derive(Properties, PartialEq)]
struct AttributeInputProps {
name: AttrValue,
attribute_type: AttributeType,
#[prop_or(None)]
value: Option<String>,
}
#[function_component(AttributeInput)]
fn attribute_input(props: &AttributeInputProps) -> Html {
let input_type = match props.attribute_type {
AttributeType::String => "text",
AttributeType::Integer => "number",
AttributeType::DateTime => {
return html! {
<DateTimeInput name={props.name.clone()} value={props.value.clone()} />
};
}
AttributeType::JpegPhoto => {
return html! {
<JpegFileInput name={props.name.clone()} value={props.value.clone()} />
};
}
};
html! {
<input
type={input_type}
name={props.name.clone()}
class="form-control"
value={props.value.clone()} />
}
}
#[derive(Properties, PartialEq)]
struct AttributeLabelProps {
pub name: String,
#[prop_or(false)]
pub required: bool,
}
#[function_component(AttributeLabel)]
fn attribute_label(props: &AttributeLabelProps) -> Html {
let tooltip_ref = use_node_ref();
use_effect_with_deps(
move |tooltip_ref| {
Tooltip::new(
tooltip_ref
.cast::<Element>()
.expect("Tooltip element should exist"),
);
|| {}
},
tooltip_ref.clone(),
);
html! {
<label for={props.name.clone()}
class="form-label col-4 col-form-label"
>
{props.name[0..1].to_uppercase() + &props.name[1..].replace('_', " ")}
{if props.required { html!{<span class="text-danger">{"*"}</span>} } else { html!{} }}
{":"}
<button
class="btn btn-sm btn-link"
type="button"
data-bs-placement="right"
title={props.name.clone()}
ref={tooltip_ref}>
<i class="bi bi-info-circle" aria-label="Info" />
</button>
</label>
}
}
#[derive(Properties, PartialEq)]
pub struct SingleAttributeInputProps {
pub name: String,
pub(crate) attribute_type: AttributeType,
#[prop_or(None)]
pub value: Option<String>,
#[prop_or(false)]
pub required: bool,
}
#[function_component(SingleAttributeInput)]
pub fn single_attribute_input(props: &SingleAttributeInputProps) -> Html {
html! {
<div class="row mb-3">
<AttributeLabel name={props.name.clone()} required={props.required} />
<div class="col-8">
<AttributeInput
attribute_type={props.attribute_type}
name={props.name.clone()}
value={props.value.clone()} />
</div>
</div>
}
}
#[derive(Properties, PartialEq)]
pub struct ListAttributeInputProps {
pub name: String,
pub(crate) attribute_type: AttributeType,
#[prop_or(vec!())]
pub values: Vec<String>,
#[prop_or(false)]
pub required: bool,
}
pub enum ListAttributeInputMsg {
Remove(usize),
Append,
}
pub struct ListAttributeInput {
indices: Vec<usize>,
next_index: usize,
values: Vec<String>,
}
impl Component for ListAttributeInput {
type Message = ListAttributeInputMsg;
type Properties = ListAttributeInputProps;
fn create(ctx: &Context<Self>) -> Self {
let values = ctx.props().values.clone();
Self {
indices: (0..values.len()).collect(),
next_index: values.len(),
values,
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
ListAttributeInputMsg::Remove(removed) => {
self.indices.retain_mut(|x| *x != removed);
}
ListAttributeInputMsg::Append => {
self.indices.push(self.next_index);
self.next_index += 1;
}
};
true
}
fn changed(&mut self, ctx: &Context<Self>) -> bool {
if ctx.props().values != self.values {
self.values.clone_from(&ctx.props().values);
self.indices = (0..self.values.len()).collect();
self.next_index = self.values.len();
}
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
let props = &ctx.props();
let link = &ctx.link();
html! {
<div class="row mb-3">
<AttributeLabel name={props.name.clone()} required={props.required} />
<div class="col-8">
{self.indices.iter().map(|&i| html! {
<div class="input-group mb-2" key={i}>
<AttributeInput
attribute_type={props.attribute_type}
name={props.name.clone()}
value={props.values.get(i).cloned().unwrap_or_default()} />
<button
class="btn btn-danger"
type="button"
onclick={link.callback(move |_| ListAttributeInputMsg::Remove(i))}>
<i class="bi-x-circle-fill" aria-label="Remove value" />
</button>
</div>
}).collect::<Html>()}
<button
class="btn btn-secondary"
type="button"
onclick={link.callback(|_| ListAttributeInputMsg::Append)}>
<i class="bi-plus-circle me-2"></i>
{"Add value"}
</button>
</div>
</div>
}
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
use yew::{function_component, html, virtual_dom::AttrValue, Callback, Properties};
use yew::{Callback, Properties, function_component, html, virtual_dom::AttrValue};
use yew_form::{Form, Model};
#[derive(Properties, PartialEq)]
+49
View File
@@ -0,0 +1,49 @@
use std::str::FromStr;
use chrono::{DateTime, NaiveDateTime, Utc};
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
use yew::{Event, Properties, function_component, html, use_state, virtual_dom::AttrValue};
#[derive(Properties, PartialEq)]
pub struct DateTimeInputProps {
pub name: AttrValue,
pub value: Option<String>,
}
#[function_component(DateTimeInput)]
pub fn date_time_input(props: &DateTimeInputProps) -> Html {
let value = use_state(|| {
props
.value
.as_ref()
.and_then(|x| DateTime::<Utc>::from_str(x).ok())
});
html! {
<div class="input-group">
<input
type="hidden"
name={props.name.clone()}
value={value.as_ref().map(|v: &DateTime<Utc>| v.to_rfc3339())} />
<input
type="datetime-local"
step="1"
class="form-control"
value={value.as_ref().map(|v: &DateTime<Utc>| v.naive_utc().to_string())}
onchange={move |e: Event| {
let string_val =
e.target()
.expect("Event should have target")
.unchecked_into::<HtmlInputElement>()
.value();
value.set(
NaiveDateTime::from_str(&string_val)
.ok()
.map(|x| DateTime::from_naive_utc_and_offset(x, Utc))
)
}} />
<span class="input-group-text">{"UTC"}</span>
</div>
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
use yew::{function_component, html, virtual_dom::AttrValue, Callback, InputEvent, Properties};
use yew::{Callback, InputEvent, Properties, function_component, html, virtual_dom::AttrValue};
use yew_form::{Form, Model};
#[derive(Properties, PartialEq)]
+236
View File
@@ -0,0 +1,236 @@
use std::{fmt::Display, str::FromStr};
use anyhow::{Error, Ok, Result, bail};
use gloo_file::{
File,
callbacks::{FileReader, read_as_bytes},
};
use web_sys::{FileList, HtmlInputElement, InputEvent};
use yew::Properties;
use yew::{prelude::*, virtual_dom::AttrValue};
#[derive(Default)]
struct JsFile {
file: Option<File>,
contents: Option<Vec<u8>>,
}
impl Display for JsFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
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")
}
}
}
fn to_base64(file: &JsFile) -> Result<String> {
match file {
JsFile {
file: None,
contents: None,
} => 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))
}
JsFile {
file: None,
contents: Some(data),
} => Ok(base64::encode(data)),
}
}
/// A [yew::Component] to display the user details, with a form allowing to edit them.
pub struct JpegFileInput {
// None means that the avatar hasn't changed.
avatar: Option<JsFile>,
reader: Option<FileReader>,
}
pub enum Msg {
Update,
/// A new file was selected.
FileSelected(File),
/// The "Clear" button for the avatar was clicked.
ClearClicked,
/// A picked file finished loading.
FileLoaded(String, Result<Vec<u8>>),
}
#[derive(Properties, Clone, PartialEq, Eq)]
pub struct Props {
pub name: AttrValue,
pub value: Option<String>,
}
impl Component for JpegFileInput {
type Message = Msg;
type Properties = Props;
fn create(ctx: &Context<Self>) -> Self {
Self {
avatar: Some(JsFile {
file: None,
contents: ctx
.props()
.value
.as_ref()
.and_then(|x| base64::decode(x).ok()),
}),
reader: None,
}
}
fn changed(&mut self, ctx: &Context<Self>) -> bool {
self.avatar = Some(JsFile {
file: None,
contents: ctx
.props()
.value
.as_ref()
.and_then(|x| base64::decode(x).ok()),
});
self.reader = None;
true
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::Update => true,
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,
});
}
true
}
Msg::ClearClicked => {
self.avatar = Some(JsFile::default());
true
}
Msg::FileLoaded(file_name, data) => {
if let Some(avatar) = &mut self.avatar
&& let Some(file) = &avatar.file
&& file.name() == file_name
&& let Result::Ok(data) = data
{
if !is_valid_jpeg(data.as_slice()) {
// Clear the selection.
self.avatar = Some(JsFile::default());
// TODO: bail!("Chosen image is not a valid JPEG");
} else {
avatar.contents = Some(data);
return true;
}
}
self.reader = None;
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
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 => String::new(),
};
html! {
<div class="row align-items-center">
<div class="col-5">
<input type="hidden" name={ctx.props().name.clone()} value={avatar_string.clone()} />
<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"
type="button"
onclick={link.callback(|_| {Msg::ClearClicked})}>
{"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>
}
}
}
impl JpegFileInput {
fn upload_files(files: Option<FileList>) -> Msg {
match files {
Some(files) if files.length() > 0 => {
Msg::FileSelected(File::from(files.item(0).unwrap()))
}
Some(_) | None => 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()
}
+3
View File
@@ -1,5 +1,8 @@
pub mod attribute_input;
pub mod checkbox;
pub mod date_input;
pub mod field;
pub mod file_input;
pub mod select;
pub mod static_value;
pub mod submit;
+1 -1
View File
@@ -1,5 +1,5 @@
use yew::{
function_component, html, virtual_dom::AttrValue, Callback, Children, InputEvent, Properties,
Callback, Children, InputEvent, Properties, function_component, html, virtual_dom::AttrValue,
};
use yew_form::{Form, Model};
+1 -1
View File
@@ -1,4 +1,4 @@
use yew::{function_component, html, virtual_dom::AttrValue, Children, Properties};
use yew::{Children, Properties, function_component, html, virtual_dom::AttrValue};
#[derive(Properties, PartialEq)]
pub struct Props {
+1 -1
View File
@@ -1,5 +1,5 @@
use web_sys::MouseEvent;
use yew::{function_component, html, virtual_dom::AttrValue, Callback, Children, Properties};
use yew::{Callback, Children, Properties, function_component, html, virtual_dom::AttrValue};
#[derive(Properties, PartialEq)]
pub struct Props {
@@ -0,0 +1,52 @@
use crate::infra::attributes::AttributeDescription;
use lldap_validation::attributes::{ALLOWED_CHARACTERS_DESCRIPTION, validate_attribute_name};
use yew::{Html, html};
fn render_attribute_aliases(attribute_description: &AttributeDescription) -> Html {
if attribute_description.aliases.is_empty() {
html! {}
} else {
html! {
<>
<br/>
<small class="text-muted">
{"Aliases: "}
{attribute_description.aliases.join(", ")}
</small>
</>
}
}
}
fn render_attribute_validation_warnings(attribute_name: &str) -> Html {
match validate_attribute_name(attribute_name) {
Ok(()) => {
html! {}
}
Err(_invalid_chars) => {
html! {
<>
<br/>
<small class="text-warning">
{"Warning: This attribute uses one or more invalid characters "}
{"("}{ALLOWED_CHARACTERS_DESCRIPTION}{"). "}
{"Some clients may not support it."}
</small>
</>
}
}
}
}
pub fn render_attribute_name(
hardcoded: bool,
attribute_description: &AttributeDescription,
) -> Html {
html! {
<>
{&attribute_description.attribute_name}
{if hardcoded {render_attribute_aliases(attribute_description)} else {html!{}}}
{render_attribute_validation_warnings(attribute_description.attribute_name)}
</>
}
}
+1
View File
@@ -0,0 +1 @@
pub mod attribute_schema;
+52 -47
View File
@@ -1,12 +1,17 @@
use crate::{
components::{
add_group_member::{self, AddGroupMemberComponent},
group_details_form::GroupDetailsForm,
remove_user_from_group::RemoveUserFromGroupComponent,
router::{AppRoute, Link},
},
infra::common_component::{CommonComponent, CommonComponentParts},
infra::{
common_component::{CommonComponent, CommonComponentParts},
form_utils::GraphQlAttributeSchema,
schema::AttributeType,
},
};
use anyhow::{bail, Error, Result};
use anyhow::{Error, Result, bail};
use graphql_client::GraphQLQuery;
use yew::prelude::*;
@@ -15,19 +20,33 @@ use yew::prelude::*;
schema_path = "../schema.graphql",
query_path = "queries/get_group_details.graphql",
response_derives = "Debug, Hash, PartialEq, Eq, Clone",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct GetGroupDetails;
pub type Group = get_group_details::GetGroupDetailsGroup;
pub type User = get_group_details::GetGroupDetailsGroupUsers;
pub type AddGroupMemberUser = add_group_member::User;
pub type Attribute = get_group_details::GetGroupDetailsGroupAttributes;
pub type AttributeSchema = get_group_details::GetGroupDetailsSchemaGroupSchemaAttributes;
impl From<&AttributeSchema> for GraphQlAttributeSchema {
fn from(attr: &AttributeSchema) -> Self {
Self {
name: attr.name.clone(),
is_list: attr.is_list,
is_readonly: attr.is_readonly,
is_editable: attr.is_editable,
}
}
}
pub struct GroupDetails {
common: CommonComponentParts<Self>,
/// The group info. If none, the error is in `error`. If `error` is None, then we haven't
/// received the server response yet.
group: Option<Group>,
group_and_schema: Option<(Group, Vec<AttributeSchema>)>,
}
/// State machine describing the possible transitions of the component state.
@@ -38,11 +57,13 @@ pub enum Msg {
OnError(Error),
OnUserAddedToGroup(AddGroupMemberUser),
OnUserRemovedFromGroup((String, i64)),
DisplayNameUpdated,
}
#[derive(yew::Properties, Clone, PartialEq, Eq)]
pub struct Props {
pub group_id: i64,
pub is_admin: bool,
}
impl GroupDetails {
@@ -69,41 +90,16 @@ impl GroupDetails {
}
}
fn view_details(&self, g: &Group) -> Html {
fn view_details(&self, ctx: &Context<Self>, g: &Group, schema: Vec<AttributeSchema>) -> 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>
<GroupDetailsForm
group={g.clone()}
group_attributes_schema={schema}
is_admin={ctx.props().is_admin}
on_display_name_updated={ctx.link().callback(|_| Msg::DisplayNameUpdated)}
/>
</>
}
}
@@ -182,29 +178,38 @@ impl GroupDetails {
}
impl CommonComponent<GroupDetails> for GroupDetails {
fn handle_msg(&mut self, _: &Context<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::GroupDetailsResponse(response) => match response {
Ok(group) => self.group = Some(group.group),
Ok(group) => {
self.group_and_schema =
Some((group.group, group.schema.group_schema.attributes))
}
Err(e) => {
self.group = None;
self.group_and_schema = None;
bail!("Error getting user details: {}", e);
}
},
Msg::OnError(e) => return Err(e),
Msg::OnUserAddedToGroup(user) => {
self.group.as_mut().unwrap().users.push(User {
self.group_and_schema.as_mut().unwrap().0.users.push(User {
id: user.id,
display_name: user.display_name,
});
}
Msg::OnUserRemovedFromGroup((user_id, _)) => {
self.group
self.group_and_schema
.as_mut()
.unwrap()
.0
.users
.retain(|u| u.id != user_id);
}
Msg::DisplayNameUpdated => self.get_group_details(ctx),
}
Ok(true)
}
@@ -221,7 +226,7 @@ impl Component for GroupDetails {
fn create(ctx: &Context<Self>) -> Self {
let mut table = Self {
common: CommonComponentParts::<Self>::create(),
group: None,
group_and_schema: None,
};
table.get_group_details(ctx);
table
@@ -232,15 +237,15 @@ impl Component for GroupDetails {
}
fn view(&self, ctx: &Context<Self>) -> Html {
match (&self.group, &self.common.error) {
match (&self.group_and_schema, &self.common.error) {
(None, None) => html! {{"Loading..."}},
(None, Some(e)) => html! {<div>{"Error: "}{e.to_string()}</div>},
(Some(u), error) => {
(Some((group, schema)), error) => {
html! {
<div>
{self.view_details(u)}
{self.view_user_list(ctx, u)}
{self.view_add_user_button(ctx, u)}
{self.view_details(ctx, group, schema.clone())}
{self.view_user_list(ctx, group)}
{self.view_add_user_button(ctx, group)}
{self.view_messages(error)}
</div>
}
+271
View File
@@ -0,0 +1,271 @@
use crate::{
components::{
form::{
attribute_input::{ListAttributeInput, SingleAttributeInput},
static_value::StaticValue,
submit::Submit,
},
group_details::{Attribute, AttributeSchema, Group},
},
infra::{
common_component::{CommonComponent, CommonComponentParts},
form_utils::{AttributeValue, EmailIsRequired, IsAdmin, read_all_form_attributes},
},
};
use anyhow::{Ok, Result};
use graphql_client::GraphQLQuery;
use yew::prelude::*;
/// The GraphQL query sent to the server to update the group details.
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "queries/update_group.graphql",
response_derives = "Debug",
variables_derives = "Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql"
)]
pub struct UpdateGroup;
/// A [yew::Component] to display the group details, with a form allowing to edit them.
pub struct GroupDetailsForm {
common: CommonComponentParts<Self>,
/// True if we just successfully updated the group, to display a success message.
just_updated: bool,
updated_group_name: bool,
group: Group,
form_ref: NodeRef,
}
pub enum Msg {
/// A form field changed.
Update,
/// The "Submit" button was clicked.
SubmitClicked,
/// We got the response from the server about our update message.
GroupUpdated(Result<update_group::ResponseData>),
}
#[derive(yew::Properties, Clone, PartialEq)]
pub struct Props {
/// The current group details.
pub group: Group,
pub group_attributes_schema: Vec<AttributeSchema>,
pub is_admin: bool,
pub on_display_name_updated: Callback<()>,
}
impl CommonComponent<GroupDetailsForm> for GroupDetailsForm {
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_group_update_form(ctx),
Msg::GroupUpdated(Err(e)) => Err(e),
Msg::GroupUpdated(Result::Ok(_)) => {
self.just_updated = true;
if self.updated_group_name {
self.updated_group_name = false;
ctx.props().on_display_name_updated.emit(());
}
Ok(true)
}
}
}
fn mut_common(&mut self) -> &mut CommonComponentParts<Self> {
&mut self.common
}
}
impl Component for GroupDetailsForm {
type Message = Msg;
type Properties = Props;
fn create(ctx: &Context<Self>) -> Self {
Self {
common: CommonComponentParts::<Self>::create(),
just_updated: false,
updated_group_name: false,
group: ctx.props().group.clone(),
form_ref: NodeRef::default(),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
self.just_updated = false;
CommonComponentParts::<Self>::update(self, ctx, msg)
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = &ctx.link();
let can_edit =
|a: &AttributeSchema| (ctx.props().is_admin || a.is_editable) && !a.is_readonly;
let display_field = |a: &AttributeSchema| {
if can_edit(a) {
get_custom_attribute_input(a, &self.group.attributes)
} else {
get_custom_attribute_static(a, &self.group.attributes)
}
};
html! {
<div class="py-3">
<form
class="form"
ref={self.form_ref.clone()}>
<StaticValue label="Group ID" id="groupId">
<i>{&self.group.id}</i>
</StaticValue>
{
ctx
.props()
.group_attributes_schema
.iter()
.filter(|a| a.is_hardcoded && a.name != "group_id")
.map(display_field)
.collect::<Vec<_>>()
}
{
ctx
.props()
.group_attributes_schema
.iter()
.filter(|a| !a.is_hardcoded)
.map(display_field)
.collect::<Vec<_>>()
}
<Submit
text="Save changes"
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitClicked})} />
</form>
{
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">{"Group successfully updated!"}</div>
</div>
</div>
}
}
}
fn get_custom_attribute_input(
attribute_schema: &AttributeSchema,
group_attributes: &[Attribute],
) -> Html {
let values = group_attributes
.iter()
.find(|a| a.name == attribute_schema.name)
.map(|attribute| attribute.value.clone())
.unwrap_or_default();
if attribute_schema.is_list {
html! {
<ListAttributeInput
name={attribute_schema.name.clone()}
attribute_type={attribute_schema.attribute_type}
values={values}
/>
}
} else {
html! {
<SingleAttributeInput
name={attribute_schema.name.clone()}
attribute_type={attribute_schema.attribute_type}
value={values.first().cloned().unwrap_or_default()}
/>
}
}
}
fn get_custom_attribute_static(
attribute_schema: &AttributeSchema,
group_attributes: &[Attribute],
) -> Html {
let values = group_attributes
.iter()
.find(|a| a.name == attribute_schema.name)
.map(|attribute| attribute.value.clone())
.unwrap_or_default();
html! {
<StaticValue label={attribute_schema.name.clone()} id={attribute_schema.name.clone()}>
{values.into_iter().map(|x| html!{<div>{x}</div>}).collect::<Vec<_>>()}
</StaticValue>
}
}
impl GroupDetailsForm {
fn submit_group_update_form(&mut self, ctx: &Context<Self>) -> Result<bool> {
let mut all_values = read_all_form_attributes(
ctx.props().group_attributes_schema.iter(),
&self.form_ref,
IsAdmin(ctx.props().is_admin),
EmailIsRequired(false),
)?;
let base_attributes = &self.group.attributes;
all_values.retain(|a| {
let base_val = base_attributes
.iter()
.find(|base_val| base_val.name == a.name);
base_val
.map(|v| v.value != a.values)
.unwrap_or(!a.values.is_empty())
});
if all_values.iter().any(|a| a.name == "display_name") {
self.updated_group_name = true;
}
let remove_attributes: Option<Vec<String>> = if all_values.is_empty() {
None
} else {
Some(all_values.iter().map(|a| a.name.clone()).collect())
};
let insert_attributes: Option<Vec<update_group::AttributeValueInput>> =
if remove_attributes.is_none() {
None
} else {
Some(
all_values
.into_iter()
.filter(|a| !a.values.is_empty())
.map(
|AttributeValue { name, values }| update_group::AttributeValueInput {
name,
value: values,
},
)
.collect(),
)
};
let mut group_input = update_group::UpdateGroupInput {
id: self.group.id,
displayName: None,
removeAttributes: None,
insertAttributes: None,
};
let default_group_input = group_input.clone();
group_input.removeAttributes = remove_attributes;
group_input.insertAttributes = insert_attributes;
// Nothing changed.
if group_input == default_group_input {
return Ok(false);
}
let req = update_group::Variables { group: group_input };
self.common.call_graphql::<UpdateGroup, _>(
ctx,
req,
Msg::GroupUpdated,
"Error trying to update group",
);
Ok(false)
}
}
+22 -21
View File
@@ -1,15 +1,16 @@
use crate::{
components::{
delete_group_attribute::DeleteGroupAttribute,
fragments::attribute_schema::render_attribute_name,
router::{AppRoute, Link},
},
convert_attribute_type,
infra::{
attributes::group,
common_component::{CommonComponent, CommonComponentParts},
schema::AttributeType,
},
};
use anyhow::{anyhow, Error, Result};
use anyhow::{Error, Result, anyhow};
use gloo_console::log;
use graphql_client::GraphQLQuery;
use yew::prelude::*;
@@ -19,7 +20,8 @@ use yew::prelude::*;
schema_path = "../schema.graphql",
query_path = "queries/get_group_attributes_schema.graphql",
response_derives = "Debug,Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct GetGroupAttributesSchema;
@@ -28,8 +30,6 @@ use get_group_attributes_schema::ResponseData;
pub type Attribute =
get_group_attributes_schema::GetGroupAttributesSchemaSchemaGroupSchemaAttributes;
convert_attribute_type!(get_group_attributes_schema::AttributeType);
#[derive(yew::Properties, Clone, PartialEq, Eq)]
pub struct Props {
pub hardcoded: bool,
@@ -55,21 +55,21 @@ impl CommonComponent<GroupSchemaTable> for GroupSchemaTable {
Ok(true)
}
Msg::OnError(e) => Err(e),
Msg::OnAttributeDeleted(attribute_name) => {
match self.attributes {
None => {
log!(format!("Attribute {attribute_name} was deleted but component has no attributes"));
Err(anyhow!("invalid state"))
}
Some(_) => {
self.attributes
.as_mut()
.unwrap()
.retain(|a| a.name != attribute_name);
Ok(true)
}
Msg::OnAttributeDeleted(attribute_name) => match self.attributes {
None => {
log!(format!(
"Attribute {attribute_name} was deleted but component has no attributes"
));
Err(anyhow!("invalid state"))
}
}
Some(_) => {
self.attributes
.as_mut()
.unwrap()
.retain(|a| a.name != attribute_name);
Ok(true)
}
},
}
}
@@ -145,16 +145,17 @@ impl GroupSchemaTable {
fn view_attribute(&self, ctx: &Context<Self>, attribute: &Attribute) -> Html {
let link = ctx.link();
let attribute_type = AttributeType::from(attribute.attribute_type.clone());
let attribute_type = attribute.attribute_type;
let checkmark = html! {
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z"></path>
</svg>
};
let hardcoded = ctx.props().hardcoded;
let desc = group::resolve_group_attribute_description_or_default(&attribute.name);
html! {
<tr key={attribute.name.clone()}>
<td>{&attribute.name}</td>
<td>{render_attribute_name(hardcoded, &desc)}</td>
<td>{if attribute.is_list { format!("List<{attribute_type}>")} else {attribute_type.to_string()}}</td>
<td>{if attribute.is_visible {checkmark.clone()} else {html!{}}}</td>
{
+2 -2
View File
@@ -8,7 +8,7 @@ use crate::{
common_component::{CommonComponent, CommonComponentParts},
},
};
use anyhow::{anyhow, bail, Result};
use anyhow::{Result, anyhow, bail};
use gloo_console::error;
use lldap_auth::*;
use validator_derive::Validate;
@@ -27,7 +27,7 @@ pub struct LoginForm {
pub struct FormModel {
#[validate(length(min = 1, message = "Missing username"))]
username: String,
#[validate(length(min = 8, message = "Invalid password. Min length: 8"))]
#[validate(length(min = 1, message = "Missing password"))]
password: String,
}
+2
View File
@@ -13,7 +13,9 @@ pub mod delete_group_attribute;
pub mod delete_user;
pub mod delete_user_attribute;
pub mod form;
pub mod fragments;
pub mod group_details;
pub mod group_details_form;
pub mod group_schema_table;
pub mod group_table;
pub mod login;
+6 -2
View File
@@ -5,7 +5,7 @@ use crate::{
common_component::{CommonComponent, CommonComponentParts},
},
};
use anyhow::{bail, Result};
use anyhow::{Result, bail};
use validator_derive::Validate;
use yew::prelude::*;
use yew_form::Form;
@@ -104,7 +104,11 @@ impl Component for ResetPasswordStep1Form {
</div>
{ if self.just_succeeded {
html! {
{"A reset token has been sent to your email."}
{"If a user with this username or email exists, a password reset email will \
be sent to the associated email address. Please check your email and \
follow the instructions. If you don't receive an email, please check \
your spam folder. If you still don't receive an email, please contact \
your administrator."}
}
} else {
html! {
+3 -3
View File
@@ -8,7 +8,7 @@ use crate::{
common_component::{CommonComponent, CommonComponentParts},
},
};
use anyhow::{bail, Result};
use anyhow::{Result, bail};
use lldap_auth::{
opaque::client::registration as opaque_registration,
password_reset::ServerPasswordResetResponse, registration,
@@ -148,7 +148,7 @@ impl Component for ResetPasswordStep2Form {
(None, None) => {
return html! {
{"Validating token"}
}
};
}
(None, Some(e)) => {
return html! {
@@ -163,7 +163,7 @@ impl Component for ResetPasswordStep2Form {
{"Back"}
</Link>
</>
}
};
}
_ => (),
};
+45 -18
View File
@@ -5,9 +5,13 @@ use crate::{
router::{AppRoute, Link},
user_details_form::UserDetailsForm,
},
infra::common_component::{CommonComponent, CommonComponentParts},
infra::{
common_component::{CommonComponent, CommonComponentParts},
form_utils::GraphQlAttributeSchema,
schema::AttributeType,
},
};
use anyhow::{bail, Error, Result};
use anyhow::{Error, Result, bail};
use graphql_client::GraphQLQuery;
use yew::prelude::*;
@@ -16,18 +20,38 @@ use yew::prelude::*;
schema_path = "../schema.graphql",
query_path = "queries/get_user_details.graphql",
response_derives = "Debug, Hash, PartialEq, Eq, Clone",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct GetUserDetails;
pub type User = get_user_details::GetUserDetailsUser;
pub type Group = get_user_details::GetUserDetailsUserGroups;
pub type Attribute = get_user_details::GetUserDetailsUserAttributes;
pub type AttributeSchema = get_user_details::GetUserDetailsSchemaUserSchemaAttributes;
impl From<&AttributeSchema> for GraphQlAttributeSchema {
fn from(attr: &AttributeSchema) -> Self {
Self {
name: attr.name.clone(),
is_list: attr.is_list,
is_readonly: attr.is_readonly,
is_editable: attr.is_editable,
}
}
}
pub struct UserDetails {
common: CommonComponentParts<Self>,
/// The user info. If none, the error is in `error`. If `error` is None, then we haven't
/// received the server response yet.
user: Option<User>,
user_and_schema: Option<(User, Vec<AttributeSchema>)>,
}
impl UserDetails {
fn mut_groups(&mut self) -> &mut Vec<Group> {
&mut self.user_and_schema.as_mut().unwrap().0.groups
}
}
/// State machine describing the possible transitions of the component state.
@@ -50,22 +74,20 @@ impl CommonComponent<UserDetails> for UserDetails {
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),
Ok(user) => {
self.user_and_schema = Some((user.user, user.schema.user_schema.attributes))
}
Err(e) => {
self.user = None;
self.user_and_schema = None;
bail!("Error getting user details: {}", e);
}
},
Msg::OnError(e) => return Err(e),
Msg::OnUserAddedToGroup(group) => {
self.user.as_mut().unwrap().groups.push(group);
self.mut_groups().push(group);
}
Msg::OnUserRemovedFromGroup((_, group_id)) => {
self.user
.as_mut()
.unwrap()
.groups
.retain(|g| g.id != group_id);
self.mut_groups().retain(|g| g.id != group_id);
}
}
Ok(true)
@@ -178,7 +200,7 @@ impl Component for UserDetails {
fn create(ctx: &Context<Self>) -> Self {
let mut table = Self {
common: CommonComponentParts::<Self>::create(),
user: None,
user_and_schema: None,
};
table.get_user_details(ctx);
table
@@ -189,10 +211,8 @@ impl Component for UserDetails {
}
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>},
(Some(u), error) => {
match (&self.user_and_schema, &self.common.error) {
(Some((u, schema)), error) => {
html! {
<>
<h3>{u.id.to_string()}</h3>
@@ -207,13 +227,20 @@ impl Component for UserDetails {
<div>
<h5 class="row m-3 fw-bold">{"User details"}</h5>
</div>
<UserDetailsForm user={u.clone()} />
<UserDetailsForm
user={u.clone()}
user_attributes_schema={schema.clone()}
is_admin={ctx.props().is_admin}
is_edited_user_admin={u.groups.iter().any(|g| g.display_name == "lldap_admin")}
/>
{self.view_group_memberships(ctx, u)}
{self.view_add_group_button(ctx, u)}
{self.view_messages(error)}
</>
}
}
(None, None) => html! {{"Loading..."}},
(None, Some(e)) => html! {<div>{"Error: "}{e.to_string()}</div>},
}
}
}
+150 -272
View File
@@ -1,56 +1,22 @@
use std::str::FromStr;
use crate::{
components::{
form::{field::Field, static_value::StaticValue, submit::Submit},
user_details::User,
form::{
attribute_input::{ListAttributeInput, SingleAttributeInput},
static_value::StaticValue,
submit::Submit,
},
user_details::{Attribute, AttributeSchema, User},
},
infra::{
common_component::{CommonComponent, CommonComponentParts},
form_utils::{AttributeValue, EmailIsRequired, IsAdmin, read_all_form_attributes},
schema::AttributeType,
},
infra::common_component::{CommonComponent, CommonComponentParts},
};
use anyhow::{bail, Error, Result};
use gloo_file::{
callbacks::{read_as_bytes, FileReader},
File,
};
use anyhow::{Ok, Result};
use gloo_console::console;
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, Eq, Clone)]
pub struct UserModel {
#[validate(email)]
email: String,
display_name: String,
first_name: String,
last_name: String,
}
/// The GraphQL query sent to the server to update the user details.
#[derive(GraphQLQuery)]
@@ -66,26 +32,17 @@ pub struct UpdateUser;
/// A [yew::Component] to display the user details, with a form allowing to edit them.
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,
form_ref: NodeRef,
}
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>),
}
@@ -94,6 +51,9 @@ pub enum Msg {
pub struct Props {
/// The current user details.
pub user: User,
pub user_attributes_schema: Vec<AttributeSchema>,
pub is_admin: bool,
pub is_edited_user_admin: bool,
}
impl CommonComponent<UserDetailsForm> for UserDetailsForm {
@@ -104,53 +64,12 @@ impl CommonComponent<UserDetailsForm> for UserDetailsForm {
) -> Result<bool> {
match msg {
Msg::Update => Ok(true),
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());
Msg::UserUpdated(Err(e)) => Err(e),
Msg::UserUpdated(Result::Ok(_)) => {
self.just_updated = true;
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)
}
}
}
@@ -164,19 +83,11 @@ impl Component for UserDetailsForm {
type Properties = Props;
fn create(ctx: &Context<Self>) -> Self {
let model = UserModel {
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(),
form: yew_form::Form::new(model),
avatar: None,
just_updated: false,
reader: None,
user: ctx.props().user.clone(),
form_ref: NodeRef::default(),
}
}
@@ -188,93 +99,41 @@ impl Component for UserDetailsForm {
fn view(&self, ctx: &Context<Self>) -> Html {
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()
let can_edit =
|a: &AttributeSchema| (ctx.props().is_admin || a.is_editable) && !a.is_readonly;
let display_field = |a: &AttributeSchema| {
if can_edit(a) {
get_custom_attribute_input(a, &self.user.attributes)
} else {
get_custom_attribute_static(a, &self.user.attributes)
}
None => self.user.avatar.as_deref().unwrap_or("").to_owned(),
};
html! {
<div class="py-3">
<form class="form">
<form
class="form"
ref={self.form_ref.clone()}>
<StaticValue label="User ID" id="userId">
<i>{&self.user.id}</i>
</StaticValue>
<StaticValue label="Creation date" id="creationDate">
{&self.user.creation_date.naive_local().date()}
</StaticValue>
<StaticValue label="UUID" id="uuid">
{&self.user.uuid}
</StaticValue>
<Field<UserModel>
form={&self.form}
required=true
label="Email"
field_name="email"
input_type="email"
oninput={link.callback(|_| Msg::Update)} />
<Field<UserModel>
form={&self.form}
label="Display name"
field_name="display_name"
autocomplete="name"
oninput={link.callback(|_| Msg::Update)} />
<Field<UserModel>
form={&self.form}
label="First name"
field_name="first_name"
autocomplete="given-name"
oninput={link.callback(|_| Msg::Update)} />
<Field<UserModel>
form={&self.form}
label="Last name"
field_name="last_name"
autocomplete="family-name"
oninput={link.callback(|_| Msg::Update)} />
<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">
<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>
{
ctx
.props()
.user_attributes_schema
.iter()
.filter(|a| a.is_hardcoded && a.name != "user_id")
.map(display_field)
.collect::<Vec<_>>()
}
{
ctx
.props()
.user_attributes_schema
.iter()
.filter(|a| !a.is_hardcoded)
.map(display_field)
.collect::<Vec<_>>()
}
<Submit
text="Save changes"
disabled={self.common.is_task_running()}
@@ -297,19 +156,107 @@ impl Component for UserDetailsForm {
}
}
fn get_custom_attribute_input(
attribute_schema: &AttributeSchema,
user_attributes: &[Attribute],
) -> Html {
let values = user_attributes
.iter()
.find(|a| a.name == attribute_schema.name)
.map(|attribute| attribute.value.clone())
.unwrap_or_default();
if attribute_schema.is_list {
html! {
<ListAttributeInput
name={attribute_schema.name.clone()}
attribute_type={attribute_schema.attribute_type}
values={values}
/>
}
} else {
html! {
<SingleAttributeInput
name={attribute_schema.name.clone()}
attribute_type={attribute_schema.attribute_type}
value={values.first().cloned().unwrap_or_default()}
/>
}
}
}
fn get_custom_attribute_static(
attribute_schema: &AttributeSchema,
user_attributes: &[Attribute],
) -> Html {
let values = user_attributes
.iter()
.find(|a| a.name == attribute_schema.name)
.map(|attribute| attribute.value.clone())
.unwrap_or_default();
let value_to_str = match attribute_schema.attribute_type {
AttributeType::String | AttributeType::Integer => |v: String| v,
AttributeType::DateTime => |v: String| {
console!(format!("Parsing date: {}", &v));
chrono::DateTime::parse_from_rfc3339(&v)
.map(|dt| dt.naive_utc().to_string())
.unwrap_or_else(|_| "Invalid date".to_string())
},
AttributeType::JpegPhoto => |_: String| "Unimplemented JPEG display".to_string(),
};
html! {
<StaticValue label={attribute_schema.name.clone()} id={attribute_schema.name.clone()}>
{values.into_iter().map(|x| html!{<div>{value_to_str(x)}</div>}).collect::<Vec<_>>()}
</StaticValue>
}
}
impl UserDetailsForm {
fn submit_user_update_form(&mut self, ctx: &Context<Self>) -> Result<bool> {
if !self.form.validate() {
bail!("Invalid inputs");
}
if let Some(JsFile {
file: Some(_),
contents: None,
}) = &self.avatar
{
bail!("Image file hasn't finished loading, try again");
}
let base_user = &self.user;
// TODO: Handle unloaded files.
// if let Some(JsFile {
// file: Some(_),
// contents: None,
// }) = &self.avatar
// {
// bail!("Image file hasn't finished loading, try again");
// }
let mut all_values = read_all_form_attributes(
ctx.props().user_attributes_schema.iter(),
&self.form_ref,
IsAdmin(ctx.props().is_admin),
EmailIsRequired(!ctx.props().is_edited_user_admin),
)?;
let base_attributes = &self.user.attributes;
all_values.retain(|a| {
let base_val = base_attributes
.iter()
.find(|base_val| base_val.name == a.name);
base_val
.map(|v| v.value != a.values)
.unwrap_or(!a.values.is_empty())
});
let remove_attributes: Option<Vec<String>> = if all_values.is_empty() {
None
} else {
Some(all_values.iter().map(|a| a.name.clone()).collect())
};
let insert_attributes: Option<Vec<update_user::AttributeValueInput>> =
if remove_attributes.is_none() {
None
} else {
Some(
all_values
.into_iter()
.filter(|a| !a.values.is_empty())
.map(
|AttributeValue { name, values }| update_user::AttributeValueInput {
name,
value: values,
},
)
.collect(),
)
};
let mut user_input = update_user::UpdateUserInput {
id: self.user.id.clone(),
email: None,
@@ -321,23 +268,8 @@ impl UserDetailsForm {
insertAttributes: None,
};
let default_user_input = user_input.clone();
let model = self.form.model();
let email = model.email;
if base_user.email != email {
user_input.email = Some(email);
}
if base_user.display_name != model.display_name {
user_input.displayName = Some(model.display_name);
}
if base_user.first_name != model.first_name {
user_input.firstName = Some(model.first_name);
}
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)?);
}
user_input.removeAttributes = remove_attributes;
user_input.insertAttributes = insert_attributes;
// Nothing changed.
if user_input == default_user_input {
return Ok(false);
@@ -351,58 +283,4 @@ impl UserDetailsForm {
);
Ok(false)
}
fn user_update_finished(&mut self, r: Result<update_user::ResponseData>) -> Result<bool> {
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))
}
}
}
+22 -21
View File
@@ -1,15 +1,16 @@
use crate::{
components::{
delete_user_attribute::DeleteUserAttribute,
fragments::attribute_schema::render_attribute_name,
router::{AppRoute, Link},
},
convert_attribute_type,
infra::{
attributes::user,
common_component::{CommonComponent, CommonComponentParts},
schema::AttributeType,
},
};
use anyhow::{anyhow, Error, Result};
use anyhow::{Error, Result, anyhow};
use gloo_console::log;
use graphql_client::GraphQLQuery;
use yew::prelude::*;
@@ -19,7 +20,8 @@ use yew::prelude::*;
schema_path = "../schema.graphql",
query_path = "queries/get_user_attributes_schema.graphql",
response_derives = "Debug,Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct GetUserAttributesSchema;
@@ -27,8 +29,6 @@ use get_user_attributes_schema::ResponseData;
pub type Attribute = get_user_attributes_schema::GetUserAttributesSchemaSchemaUserSchemaAttributes;
convert_attribute_type!(get_user_attributes_schema::AttributeType);
#[derive(yew::Properties, Clone, PartialEq, Eq)]
pub struct Props {
pub hardcoded: bool,
@@ -53,21 +53,21 @@ impl CommonComponent<UserSchemaTable> for UserSchemaTable {
Ok(true)
}
Msg::OnError(e) => Err(e),
Msg::OnAttributeDeleted(attribute_name) => {
match self.attributes {
None => {
log!(format!("Attribute {attribute_name} was deleted but component has no attributes"));
Err(anyhow!("invalid state"))
}
Some(_) => {
self.attributes
.as_mut()
.unwrap()
.retain(|a| a.name != attribute_name);
Ok(true)
}
Msg::OnAttributeDeleted(attribute_name) => match self.attributes {
None => {
log!(format!(
"Attribute {attribute_name} was deleted but component has no attributes"
));
Err(anyhow!("invalid state"))
}
}
Some(_) => {
self.attributes
.as_mut()
.unwrap()
.retain(|a| a.name != attribute_name);
Ok(true)
}
},
}
}
@@ -144,16 +144,17 @@ impl UserSchemaTable {
fn view_attribute(&self, ctx: &Context<Self>, attribute: &Attribute) -> Html {
let link = ctx.link();
let attribute_type = AttributeType::from(attribute.attribute_type.clone());
let attribute_type = attribute.attribute_type;
let checkmark = html! {
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z"></path>
</svg>
};
let hardcoded = ctx.props().hardcoded;
let desc = user::resolve_user_attribute_description_or_default(&attribute.name);
html! {
<tr key={attribute.name.clone()}>
<td>{&attribute.name}</td>
<td>{render_attribute_name(hardcoded, &desc)}</td>
<td>{if attribute.is_list { format!("List<{attribute_type}>")} else {attribute_type.to_string()}}</td>
<td>{if attribute.is_editable {checkmark.clone()} else {html!{}}}</td>
<td>{if attribute.is_visible {checkmark.clone()} else {html!{}}}</td>
+22 -21
View File
@@ -1,10 +1,11 @@
use super::cookies::set_cookie;
use anyhow::{anyhow, Context, Result};
use gloo_net::http::{Method, Request};
use anyhow::{Context, Result, anyhow};
use gloo_net::http::{Method, RequestBuilder};
use graphql_client::GraphQLQuery;
use lldap_auth::{login, registration, JWTClaims};
use lldap_auth::{JWTClaims, login, registration};
use serde::{de::DeserializeOwned, Serialize};
use lldap_frontend_options::Options;
use serde::{Serialize, de::DeserializeOwned};
use web_sys::RequestCredentials;
#[derive(Default)]
@@ -32,14 +33,16 @@ async fn call_server<Body: Serialize>(
body: RequestType<Body>,
error_message: &'static str,
) -> Result<String> {
let mut request = Request::new(url)
let request_builder = RequestBuilder::new(url)
.header("Content-Type", "application/json")
.credentials(RequestCredentials::SameOrigin);
if let RequestType::Post(b) = body {
request = request
.body(serde_json::to_string(&b)?)
.method(Method::POST);
}
let request = if let RequestType::Post(b) = body {
request_builder
.method(Method::POST)
.body(serde_json::to_string(&b)?)?
} else {
request_builder.build()?
};
let response = request.send().await?;
if response.ok() {
Ok(response.text().await?)
@@ -135,6 +138,15 @@ impl HostService {
.and_then(set_cookies_from_jwt)
}
pub async fn get_settings() -> Result<Options> {
call_server_json_with_error_message::<Options, _>(
&(base_url() + "/settings"),
GET_REQUEST,
"Could not fetch settings: ",
)
.await
}
pub async fn register_start(
request: registration::ClientRegistrationStartRequest,
) -> Result<Box<registration::ServerRegistrationStartResponse>> {
@@ -200,15 +212,4 @@ impl HostService {
)
.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)
}
}
+128
View File
@@ -0,0 +1,128 @@
pub struct AttributeDescription<'a> {
pub attribute_identifier: &'a str,
pub attribute_name: &'a str,
pub aliases: Vec<&'a str>,
}
pub mod group {
use super::AttributeDescription;
pub fn resolve_group_attribute_description(name: &'_ str) -> Option<AttributeDescription<'_>> {
match name {
"creation_date" => Some(AttributeDescription {
attribute_identifier: name,
attribute_name: "creationdate",
aliases: vec![name, "createtimestamp"],
}),
"modified_date" => Some(AttributeDescription {
attribute_identifier: name,
attribute_name: "modifydate",
aliases: vec![name, "modifytimestamp"],
}),
"display_name" => Some(AttributeDescription {
attribute_identifier: name,
attribute_name: "displayname",
aliases: vec![name, "cn", "uid", "id"],
}),
"group_id" => Some(AttributeDescription {
attribute_identifier: name,
attribute_name: "groupid",
aliases: vec![name],
}),
"uuid" => Some(AttributeDescription {
attribute_identifier: name,
attribute_name: name,
aliases: vec!["entryuuid"],
}),
_ => None,
}
}
pub fn resolve_group_attribute_description_or_default(
name: &'_ str,
) -> AttributeDescription<'_> {
match resolve_group_attribute_description(name) {
Some(d) => d,
None => AttributeDescription {
attribute_identifier: name,
attribute_name: name,
aliases: vec![],
},
}
}
}
pub mod user {
use super::AttributeDescription;
pub fn resolve_user_attribute_description(name: &'_ str) -> Option<AttributeDescription<'_>> {
match name {
"avatar" => Some(AttributeDescription {
attribute_identifier: name,
attribute_name: name,
aliases: vec!["jpegphoto"],
}),
"creation_date" => Some(AttributeDescription {
attribute_identifier: name,
attribute_name: "creationdate",
aliases: vec![name, "createtimestamp"],
}),
"modified_date" => Some(AttributeDescription {
attribute_identifier: name,
attribute_name: "modifydate",
aliases: vec![name, "modifytimestamp"],
}),
"password_modified_date" => Some(AttributeDescription {
attribute_identifier: name,
attribute_name: "passwordmodifydate",
aliases: vec![name, "pwdchangedtime"],
}),
"display_name" => Some(AttributeDescription {
attribute_identifier: name,
attribute_name: "displayname",
aliases: vec![name, "cn"],
}),
"first_name" => Some(AttributeDescription {
attribute_identifier: name,
attribute_name: "firstname",
aliases: vec![name, "givenname"],
}),
"last_name" => Some(AttributeDescription {
attribute_identifier: name,
attribute_name: "lastname",
aliases: vec![name, "sn"],
}),
"mail" => Some(AttributeDescription {
attribute_identifier: name,
attribute_name: name,
aliases: vec!["email"],
}),
"user_id" => Some(AttributeDescription {
attribute_identifier: name,
attribute_name: "uid",
aliases: vec![name, "id"],
}),
"uuid" => Some(AttributeDescription {
attribute_identifier: name,
attribute_name: name,
aliases: vec!["entryuuid"],
}),
_ => None,
}
}
pub fn resolve_user_attribute_description_or_default(
name: &'_ str,
) -> AttributeDescription<'_> {
match resolve_user_attribute_description(name) {
Some(d) => d,
None => AttributeDescription {
attribute_identifier: name,
attribute_name: name,
aliases: vec![],
},
}
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow};
use chrono::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::HtmlDocument;
+68
View File
@@ -0,0 +1,68 @@
use anyhow::{Result, anyhow, ensure};
use validator::validate_email;
use web_sys::{FormData, HtmlFormElement};
use yew::NodeRef;
#[derive(Debug)]
pub struct AttributeValue {
pub name: String,
pub values: Vec<String>,
}
pub struct GraphQlAttributeSchema {
pub name: String,
pub is_list: bool,
pub is_readonly: bool,
pub is_editable: bool,
}
fn validate_email_attributes(all_values: &[AttributeValue]) -> Result<()> {
let maybe_email_values = all_values.iter().find(|a| a.name == "mail");
let email_values = &maybe_email_values
.ok_or_else(|| anyhow!("Email is required"))?
.values;
ensure!(!email_values.is_empty(), "Email is required");
ensure!(email_values.len() == 1, "Multiple emails are not supported");
ensure!(validate_email(&email_values[0]), "Email is not valid");
Ok(())
}
pub struct IsAdmin(pub bool);
pub struct EmailIsRequired(pub bool);
pub fn read_all_form_attributes(
schema: impl IntoIterator<Item = impl Into<GraphQlAttributeSchema>>,
form_ref: &NodeRef,
is_admin: IsAdmin,
email_is_required: EmailIsRequired,
) -> Result<Vec<AttributeValue>> {
let form = form_ref.cast::<HtmlFormElement>().unwrap();
let form_data = FormData::new_with_form(&form)
.map_err(|e| anyhow!("Failed to get FormData: {:#?}", e.as_string()))?;
let all_values = schema
.into_iter()
.map(Into::<GraphQlAttributeSchema>::into)
.filter(|attr| !attr.is_readonly && (is_admin.0 || attr.is_editable))
.map(|attr| -> Result<AttributeValue> {
let val = form_data
.get_all(attr.name.as_str())
.iter()
.map(|js_val| js_val.as_string().unwrap_or_default())
.filter(|val| !val.is_empty())
.collect::<Vec<String>>();
ensure!(
val.len() <= 1 || attr.is_list,
"Multiple values supplied for non-list attribute {}",
attr.name
);
Ok(AttributeValue {
name: attr.name.clone(),
values: val,
})
})
.collect::<Result<Vec<_>>>()?;
if email_is_required.0 {
validate_email_attributes(&all_values)?;
}
Ok(all_values)
}
+16 -9
View File
@@ -2,7 +2,7 @@ use crate::infra::api::HostService;
use anyhow::Result;
use graphql_client::GraphQLQuery;
use wasm_bindgen_futures::spawn_local;
use yew::{use_effect, use_state_eq, UseStateHandle};
use yew::{UseStateHandle, use_effect_with_deps, use_state_eq};
// Enum to represent a result that is fetched asynchronously.
#[derive(Debug)]
@@ -31,22 +31,29 @@ pub fn use_graphql_call<QueryType>(
) -> UseStateHandle<LoadableResult<QueryType::ResponseData>>
where
QueryType: GraphQLQuery + 'static,
<QueryType as graphql_client::GraphQLQuery>::Variables: std::cmp::PartialEq + Clone,
<QueryType as graphql_client::GraphQLQuery>::ResponseData: std::cmp::PartialEq,
{
let loadable_result: UseStateHandle<LoadableResult<QueryType::ResponseData>> =
use_state_eq(|| LoadableResult::Loading);
{
let loadable_result = loadable_result.clone();
use_effect(move || {
let task = HostService::graphql_query::<QueryType>(variables, "Failed graphql query");
use_effect_with_deps(
move |variables| {
let task = HostService::graphql_query::<QueryType>(
variables.clone(),
"Failed graphql query",
);
spawn_local(async move {
let response = task.await;
loadable_result.set(LoadableResult::Loaded(response));
});
spawn_local(async move {
let response = task.await;
loadable_result.set(LoadableResult::Loaded(response));
});
|| ()
})
|| ()
},
variables,
)
}
loadable_result.clone()
}
+3
View File
@@ -1,7 +1,10 @@
pub mod api;
pub mod attributes;
pub mod common_component;
pub mod cookies;
pub mod form_utils;
pub mod functional;
pub mod graphql;
pub mod modal;
pub mod schema;
pub mod tooltip;
+2
View File
@@ -1,3 +1,5 @@
#![allow(clippy::empty_docs)]
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
+31 -55
View File
@@ -1,66 +1,42 @@
use anyhow::Result;
use std::{fmt::Display, str::FromStr};
use derive_more::Display;
use serde::{Deserialize, Serialize};
use strum::EnumString;
use validator::ValidationError;
#[derive(Debug)]
pub enum AttributeType {
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash, EnumString, Display)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[strum(ascii_case_insensitive)]
pub(crate) enum AttributeType {
String,
Integer,
#[strum(serialize = "DATE_TIME", serialize = "DATETIME")]
DateTime,
Jpeg,
}
impl Display for AttributeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl FromStr for AttributeType {
type Err = ();
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"String" => Ok(AttributeType::String),
"Integer" => Ok(AttributeType::Integer),
"DateTime" => Ok(AttributeType::DateTime),
"Jpeg" => Ok(AttributeType::Jpeg),
_ => Err(()),
}
}
}
// Macro to generate traits for converting between AttributeType and the
// graphql generated equivalents.
#[macro_export]
macro_rules! convert_attribute_type {
($source_type:ty) => {
impl From<$source_type> for AttributeType {
fn from(value: $source_type) -> Self {
match value {
<$source_type>::STRING => AttributeType::String,
<$source_type>::INTEGER => AttributeType::Integer,
<$source_type>::DATE_TIME => AttributeType::DateTime,
<$source_type>::JPEG_PHOTO => AttributeType::Jpeg,
_ => panic!("Unknown attribute type"),
}
}
}
impl From<AttributeType> for $source_type {
fn from(value: AttributeType) -> Self {
match value {
AttributeType::String => <$source_type>::STRING,
AttributeType::Integer => <$source_type>::INTEGER,
AttributeType::DateTime => <$source_type>::DATE_TIME,
AttributeType::Jpeg => <$source_type>::JPEG_PHOTO,
}
}
}
};
#[strum(serialize = "JPEG_PHOTO", serialize = "JPEGPHOTO")]
JpegPhoto,
}
pub fn validate_attribute_type(attribute_type: &str) -> Result<(), ValidationError> {
AttributeType::from_str(attribute_type)
AttributeType::try_from(attribute_type)
.map_err(|_| ValidationError::new("Invalid attribute type"))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deserialize_attribute_type() {
let attr_type: AttributeType = "STRING".try_into().unwrap();
assert_eq!(attr_type, AttributeType::String);
let attr_type: AttributeType = "Integer".try_into().unwrap();
assert_eq!(attr_type, AttributeType::Integer);
let attr_type: AttributeType = "DATE_TIME".try_into().unwrap();
assert_eq!(attr_type, AttributeType::DateTime);
let attr_type: AttributeType = "JpegPhoto".try_into().unwrap();
assert_eq!(attr_type, AttributeType::JpegPhoto);
}
}
+12
View File
@@ -0,0 +1,12 @@
#![allow(clippy::empty_docs)]
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = bootstrap)]
pub type Tooltip;
#[wasm_bindgen(constructor, js_namespace = bootstrap)]
pub fn new(e: web_sys::Element) -> Tooltip;
}
+2 -1
View File
@@ -2,11 +2,12 @@
#![forbid(non_ascii_idents)]
#![allow(clippy::uninlined_format_args)]
#![allow(clippy::let_unit_value)]
#![allow(clippy::unnecessary_operation)] // Doesn't work well with the html macro.
pub mod components;
pub mod infra;
use wasm_bindgen::prelude::{wasm_bindgen, JsValue};
use wasm_bindgen::prelude::{JsValue, wasm_bindgen};
#[wasm_bindgen]
pub fn run_app() -> Result<(), JsValue> {
+27
View File
@@ -0,0 +1,27 @@
[package]
name = "lldap_access_control"
version = "0.1.0"
description = "Access control wrappers for LLDAP"
authors.workspace = true
edition.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
tracing = "*"
async-trait = "0.1"
[dependencies.lldap_auth]
path = "../auth"
features = ["opaque_server", "opaque_client", "sea_orm"]
[dependencies.lldap_domain]
path = "../domain"
[dependencies.lldap_domain_handlers]
path = "../domain-handlers"
[dependencies.lldap_domain_model]
path = "../domain-model"
@@ -1,78 +1,25 @@
use std::collections::HashSet;
use async_trait::async_trait;
use tracing::info;
use crate::domain::{
error::Result,
handler::{
AttributeSchema, BackendHandler, CreateAttributeRequest, CreateGroupRequest,
CreateUserRequest, GroupBackendHandler, GroupListerBackendHandler, GroupRequestFilter,
ReadSchemaBackendHandler, Schema, SchemaBackendHandler, UpdateGroupRequest,
UpdateUserRequest, UserBackendHandler, UserListerBackendHandler, UserRequestFilter,
use lldap_auth::access_control::{Permission, ValidationResults};
use lldap_domain::{
public_schema::PublicSchema,
requests::{
CreateAttributeRequest, CreateGroupRequest, CreateUserRequest, UpdateGroupRequest,
UpdateUserRequest,
},
schema::PublicSchema,
schema::{AttributeSchema, Schema},
types::{
AttributeName, Group, GroupDetails, GroupId, GroupName, LdapObjectClass, User,
UserAndGroups, UserId,
},
};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Permission {
Admin,
PasswordManager,
Readonly,
Regular,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationResults {
pub user: UserId,
pub permission: Permission,
}
impl ValidationResults {
#[cfg(test)]
pub fn admin() -> Self {
Self {
user: UserId::new("admin"),
permission: Permission::Admin,
}
}
#[must_use]
pub fn is_admin(&self) -> bool {
self.permission == Permission::Admin
}
#[must_use]
pub fn can_read_all(&self) -> bool {
self.permission == Permission::Admin
|| self.permission == Permission::Readonly
|| self.permission == Permission::PasswordManager
}
#[must_use]
pub fn can_read(&self, user: &UserId) -> bool {
self.permission == Permission::Admin
|| self.permission == Permission::PasswordManager
|| self.permission == Permission::Readonly
|| &self.user == user
}
#[must_use]
pub fn can_change_password(&self, user: &UserId, user_is_admin: bool) -> bool {
self.permission == Permission::Admin
|| (self.permission == Permission::PasswordManager && !user_is_admin)
|| &self.user == user
}
#[must_use]
pub fn can_write(&self, user: &UserId) -> bool {
self.permission == Permission::Admin || &self.user == user
}
}
use lldap_domain_handlers::handler::{
BackendHandler, GroupBackendHandler, GroupListerBackendHandler, GroupRequestFilter,
ReadSchemaBackendHandler, SchemaBackendHandler, UserBackendHandler, UserListerBackendHandler,
UserRequestFilter,
};
use lldap_domain_model::error::Result;
use std::collections::HashSet;
use tracing::info;
#[async_trait]
pub trait UserReadableBackendHandler: ReadSchemaBackendHandler {
@@ -231,17 +178,24 @@ impl<Handler: BackendHandler> AccessControlledBackendHandler<Handler> {
Self { handler }
}
pub fn get_schema_only_handler(
&self,
_validation_result: &ValidationResults,
) -> Option<&impl ReadSchemaBackendHandler> {
Some(&self.handler)
}
pub fn get_admin_handler(
&self,
validation_result: &ValidationResults,
) -> Option<&impl AdminBackendHandler> {
) -> Option<&(impl AdminBackendHandler + use<Handler>)> {
validation_result.is_admin().then_some(&self.handler)
}
pub fn get_readonly_handler(
&self,
validation_result: &ValidationResults,
) -> Option<&impl ReadonlyBackendHandler> {
) -> Option<&(impl ReadonlyBackendHandler + use<Handler>)> {
validation_result.can_read_all().then_some(&self.handler)
}
@@ -249,7 +203,7 @@ impl<Handler: BackendHandler> AccessControlledBackendHandler<Handler> {
&self,
validation_result: &ValidationResults,
user_id: &UserId,
) -> Option<&impl UserWriteableBackendHandler> {
) -> Option<&(impl UserWriteableBackendHandler + use<Handler>)> {
validation_result
.can_write(user_id)
.then_some(&self.handler)
@@ -259,7 +213,7 @@ impl<Handler: BackendHandler> AccessControlledBackendHandler<Handler> {
&self,
validation_result: &ValidationResults,
user_id: &UserId,
) -> Option<&impl UserReadableBackendHandler> {
) -> Option<&(impl UserReadableBackendHandler + use<Handler>)> {
validation_result.can_read(user_id).then_some(&self.handler)
}
@@ -310,12 +264,12 @@ impl<Handler: BackendHandler> AccessControlledBackendHandler<Handler> {
pub struct UserRestrictedListerBackendHandler<'a, Handler> {
handler: &'a Handler,
pub user_filter: Option<UserId>,
user_filter: Option<UserId>,
}
#[async_trait]
impl<'a, Handler: ReadSchemaBackendHandler + Sync> ReadSchemaBackendHandler
for UserRestrictedListerBackendHandler<'a, Handler>
impl<Handler: ReadSchemaBackendHandler + Sync> ReadSchemaBackendHandler
for UserRestrictedListerBackendHandler<'_, Handler>
{
async fn get_schema(&self) -> Result<Schema> {
let mut schema = self.handler.get_schema().await?;
@@ -331,8 +285,8 @@ impl<'a, Handler: ReadSchemaBackendHandler + Sync> ReadSchemaBackendHandler
}
#[async_trait]
impl<'a, Handler: UserListerBackendHandler + Sync> UserListerBackendHandler
for UserRestrictedListerBackendHandler<'a, Handler>
impl<Handler: UserListerBackendHandler + Sync> UserListerBackendHandler
for UserRestrictedListerBackendHandler<'_, Handler>
{
async fn list_users(
&self,
@@ -354,8 +308,8 @@ impl<'a, Handler: UserListerBackendHandler + Sync> UserListerBackendHandler
}
#[async_trait]
impl<'a, Handler: GroupListerBackendHandler + Sync> GroupListerBackendHandler
for UserRestrictedListerBackendHandler<'a, Handler>
impl<Handler: GroupListerBackendHandler + Sync> GroupListerBackendHandler
for UserRestrictedListerBackendHandler<'_, Handler>
{
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>> {
let group_filter = self
@@ -376,10 +330,14 @@ impl<'a, Handler: GroupListerBackendHandler + Sync> GroupListerBackendHandler
pub trait UserAndGroupListerBackendHandler:
UserListerBackendHandler + GroupListerBackendHandler
{
fn user_filter(&self) -> &Option<UserId>;
}
#[async_trait]
impl<'a, Handler: GroupListerBackendHandler + UserListerBackendHandler + Sync>
UserAndGroupListerBackendHandler for UserRestrictedListerBackendHandler<'a, Handler>
impl<Handler: GroupListerBackendHandler + UserListerBackendHandler + Sync>
UserAndGroupListerBackendHandler for UserRestrictedListerBackendHandler<'_, Handler>
{
fn user_filter(&self) -> &Option<UserId> {
&self.user_filter
}
}
+23 -14
View File
@@ -1,12 +1,13 @@
[package]
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"
version = "0.6.0"
description = "Authentication protocol for LLDAP"
edition.workspace = true
authors.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[features]
default = ["opaque_server", "opaque_client"]
@@ -14,30 +15,38 @@ opaque_server = []
opaque_client = []
js = []
sea_orm = ["dep:sea-orm"]
test = []
[dependencies]
rust-argon2 = "0.8"
rust-argon2 = "2"
curve25519-dalek = "3"
digest = "0.9"
generic-array = "0.14"
rand = "0.8"
serde = "*"
sha2 = "0.9"
thiserror = "*"
thiserror = "2"
uuid = { version = "1.18.1", features = ["serde"] }
[dependencies.derive_more]
features = ["debug", "display"]
default-features = false
version = "1"
[dependencies.opaque-ke]
version = "0.6"
version = "0.7"
[dependencies.chrono]
version = "*"
features = [ "serde" ]
features = ["serde"]
[dependencies.sea-orm]
version= "0.12"
default-features = false
workspace = true
features = ["macros"]
optional = true
[dependencies.serde]
workspace = true
# For WASM targets, use the JS getrandom.
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.getrandom]
version = "0.2"
+58
View File
@@ -0,0 +1,58 @@
use crate::types::UserId;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum Permission {
Admin,
PasswordManager,
Readonly,
Regular,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ValidationResults {
pub user: UserId,
pub permission: Permission,
}
impl ValidationResults {
#[cfg(feature = "test")]
pub fn admin() -> Self {
Self {
user: UserId::new("admin"),
permission: Permission::Admin,
}
}
#[must_use]
pub fn is_admin(&self) -> bool {
self.permission == Permission::Admin
}
#[must_use]
pub fn can_read_all(&self) -> bool {
self.permission == Permission::Admin
|| self.permission == Permission::Readonly
|| self.permission == Permission::PasswordManager
}
#[must_use]
pub fn can_read(&self, user: &UserId) -> bool {
self.permission == Permission::Admin
|| self.permission == Permission::PasswordManager
|| self.permission == Permission::Readonly
|| &self.user == user
}
#[must_use]
pub fn can_change_password(&self, user: &UserId, user_is_admin: bool) -> bool {
self.permission == Permission::Admin
|| (self.permission == Permission::PasswordManager && !user_is_admin)
|| &self.user == user
}
#[must_use]
pub fn can_write(&self, user: &UserId) -> bool {
self.permission == Permission::Admin || &self.user == user
}
}
+19 -7
View File
@@ -4,7 +4,9 @@ use chrono::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fmt;
use uuid::Uuid;
pub mod access_control;
pub mod opaque;
/// The messages for the 3-step OPAQUE and simple login process.
@@ -108,7 +110,7 @@ pub mod types {
use serde::{Deserialize, Serialize};
#[cfg(feature = "sea_orm")]
use sea_orm::{DbErr, DeriveValueType, QueryResult, TryFromU64, Value};
use sea_orm::{DbErr, DeriveValueType, TryFromU64, Value};
#[derive(
PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Default, Hash, Serialize, Deserialize,
@@ -151,10 +153,22 @@ pub mod types {
}
#[derive(
PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Default, Hash, Serialize, Deserialize,
PartialEq,
Eq,
PartialOrd,
Ord,
Clone,
Default,
Hash,
Serialize,
Deserialize,
derive_more::Debug,
derive_more::Display,
)]
#[cfg_attr(feature = "sea_orm", derive(DeriveValueType))]
#[serde(from = "CaseInsensitiveString")]
#[debug(r#""{}""#, _0.as_str())]
#[display("{}", _0.as_str())]
pub struct UserId(CaseInsensitiveString);
impl UserId {
@@ -176,11 +190,6 @@ pub mod types {
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 {
@@ -200,8 +209,11 @@ pub mod types {
#[derive(Clone, Serialize, Deserialize)]
pub struct JWTClaims {
#[serde(with = "chrono::serde::ts_seconds")]
pub exp: DateTime<Utc>,
#[serde(with = "chrono::serde::ts_seconds")]
pub iat: DateTime<Utc>,
pub jti: Uuid,
pub user: String,
pub groups: HashSet<String>,
}
@@ -32,7 +32,6 @@ impl ArgonHasher {
lanes: 1,
mem_cost: 50 * 1024, // 50 MB, in KB
secret: &[],
thread_mode: argon2::ThreadMode::Sequential,
time_cost: 1,
variant: argon2::Variant::Argon2id,
version: argon2::Version::Version13,
@@ -133,6 +132,12 @@ pub mod server {
pub use super::*;
pub type ServerRegistration = opaque_ke::ServerRegistration<DefaultSuite>;
pub type ServerSetup = opaque_ke::ServerSetup<DefaultSuite>;
pub fn generate_random_private_key() -> ServerSetup {
let mut rng = rand::rngs::OsRng;
ServerSetup::new(&mut rng)
}
/// Methods to register a new user, from the server side.
pub mod registration {
pub use super::*;
+47
View File
@@ -0,0 +1,47 @@
[package]
name = "lldap_domain_handlers"
version = "0.1.0"
edition.workspace = true
authors.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[features]
test = []
[dependencies]
async-trait = "0.1"
base64 = "0.21"
ldap3_proto = "0.6.0"
serde_bytes = "0.11"
[dev-dependencies]
pretty_assertions = "1"
[dependencies.chrono]
features = ["serde"]
version = "0.4"
[dependencies.derive_more]
features = ["debug", "display", "from", "from_str"]
default-features = false
version = "1"
[dependencies.lldap_auth]
path = "../auth"
features = ["opaque_server", "opaque_client", "sea_orm"]
[dependencies.lldap_domain]
path = "../domain"
[dependencies.lldap_domain_model]
path = "../domain-model"
[dependencies.serde]
workspace = true
[dependencies.uuid]
features = ["v1", "v3"]
version = "1"
@@ -1,12 +1,17 @@
use crate::domain::{
error::Result,
use async_trait::async_trait;
use ldap3_proto::proto::LdapSubstringFilter;
use lldap_domain::{
requests::{
CreateAttributeRequest, CreateGroupRequest, CreateUserRequest, UpdateGroupRequest,
UpdateUserRequest,
},
schema::Schema,
types::{
AttributeName, AttributeType, AttributeValue, Email, Group, GroupDetails, GroupId,
GroupName, JpegPhoto, LdapObjectClass, Serialized, User, UserAndGroups, UserColumn, UserId,
Uuid,
AttributeName, AttributeValue, Group, GroupDetails, GroupId, GroupName, LdapObjectClass,
User, UserAndGroups, UserId, Uuid,
},
};
use async_trait::async_trait;
use lldap_domain_model::{error::Result, model::UserColumn};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
@@ -47,34 +52,51 @@ impl SubStringFilter {
}
}
impl From<LdapSubstringFilter> for SubStringFilter {
fn from(
LdapSubstringFilter {
initial,
any,
final_,
}: LdapSubstringFilter,
) -> Self {
Self {
initial,
any,
final_,
}
}
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
pub enum UserRequestFilter {
True,
False,
And(Vec<UserRequestFilter>),
Or(Vec<UserRequestFilter>),
Not(Box<UserRequestFilter>),
UserId(UserId),
UserIdSubString(SubStringFilter),
Equality(UserColumn, String),
AttributeEquality(AttributeName, Serialized),
AttributeEquality(AttributeName, AttributeValue),
SubString(UserColumn, SubStringFilter),
// Check if a user belongs to a group identified by name.
MemberOf(GroupName),
// Same, by id.
MemberOfId(GroupId),
CustomAttributePresent(AttributeName),
}
impl From<bool> for UserRequestFilter {
fn from(val: bool) -> Self {
if val {
Self::And(vec![])
} else {
Self::Not(Box::new(Self::And(vec![])))
}
if val { Self::True } else { Self::False }
}
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
pub enum GroupRequestFilter {
True,
False,
And(Vec<GroupRequestFilter>),
Or(Vec<GroupRequestFilter>),
Not(Box<GroupRequestFilter>),
@@ -84,102 +106,16 @@ pub enum GroupRequestFilter {
GroupId(GroupId),
// Check if the group contains a user identified by uid.
Member(UserId),
AttributeEquality(AttributeName, Serialized),
AttributeEquality(AttributeName, AttributeValue),
CustomAttributePresent(AttributeName),
}
impl From<bool> for GroupRequestFilter {
fn from(val: bool) -> Self {
if val {
Self::And(vec![])
} else {
Self::Not(Box::new(Self::And(vec![])))
}
if val { Self::True } else { Self::False }
}
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)]
pub struct CreateUserRequest {
// Same fields as User, but no creation_date, and with password.
pub user_id: UserId,
pub email: Email,
pub display_name: Option<String>,
pub first_name: Option<String>,
pub last_name: Option<String>,
pub avatar: Option<JpegPhoto>,
pub attributes: Vec<AttributeValue>,
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)]
pub struct UpdateUserRequest {
// Same fields as CreateUserRequest, but no with an extra layer of Option.
pub user_id: UserId,
pub email: Option<Email>,
pub display_name: Option<String>,
pub first_name: Option<String>,
pub last_name: Option<String>,
pub avatar: Option<JpegPhoto>,
pub delete_attributes: Vec<AttributeName>,
pub insert_attributes: Vec<AttributeValue>,
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)]
pub struct CreateGroupRequest {
pub display_name: GroupName,
pub attributes: Vec<AttributeValue>,
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
pub struct UpdateGroupRequest {
pub group_id: GroupId,
pub display_name: Option<GroupName>,
pub delete_attributes: Vec<AttributeName>,
pub insert_attributes: Vec<AttributeValue>,
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
pub struct AttributeSchema {
pub name: AttributeName,
//TODO: pub aliases: Vec<String>,
pub attribute_type: AttributeType,
pub is_list: bool,
pub is_visible: bool,
pub is_editable: bool,
pub is_hardcoded: bool,
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
pub struct CreateAttributeRequest {
pub name: AttributeName,
pub attribute_type: AttributeType,
pub is_list: bool,
pub is_visible: bool,
pub is_editable: bool,
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
pub struct AttributeList {
pub attributes: Vec<AttributeSchema>,
}
impl AttributeList {
pub fn get_attribute_schema(&self, name: &AttributeName) -> Option<&AttributeSchema> {
self.attributes.iter().find(|a| a.name == *name)
}
pub fn get_attribute_type(&self, name: &AttributeName) -> Option<(AttributeType, bool)> {
self.get_attribute_schema(name)
.map(|a| (a.attribute_type, a.is_list))
}
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
pub struct Schema {
pub user_attributes: AttributeList,
pub group_attributes: AttributeList,
pub extra_user_object_classes: Vec<LdapObjectClass>,
pub extra_group_object_classes: Vec<LdapObjectClass>,
}
#[async_trait]
pub trait LoginHandler: Send + Sync {
async fn bind(&self, request: BindRequest) -> Result<()>;
@@ -254,6 +190,7 @@ pub trait BackendHandler:
mod tests {
use super::*;
use base64::Engine;
use lldap_domain::types::JpegPhoto;
use pretty_assertions::assert_ne;
#[test]
+1
View File
@@ -0,0 +1 @@
pub mod handler;
+49
View File
@@ -0,0 +1,49 @@
[package]
name = "lldap_domain_model"
version = "0.1.0"
edition.workspace = true
authors.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[features]
test = []
[dependencies]
base64 = "0.21"
bincode = "1.3"
orion = "0.17"
serde_bytes = "0.11"
thiserror = "2"
[dev-dependencies]
pretty_assertions = "1"
[dependencies.chrono]
features = ["serde"]
version = "0.4"
[dependencies.derive_more]
features = ["debug", "display", "from", "from_str"]
default-features = false
version = "1"
[dependencies.lldap_auth]
path = "../auth"
features = ["opaque_server", "opaque_client", "sea_orm"]
[dependencies.lldap_domain]
path = "../domain"
[dependencies.sea-orm]
workspace = true
features = ["macros"]
[dependencies.serde]
workspace = true
[dependencies.uuid]
features = ["v1", "v3"]
version = "1"
@@ -3,7 +3,7 @@ use thiserror::Error;
#[allow(clippy::enum_variant_names)]
#[derive(Error, Debug)]
pub enum DomainError {
#[error("Authentication error: `{0}`")]
#[error("Authentication error {0}")]
AuthenticationError(String),
#[error("Database error: `{0}`")]
DatabaseError(#[from] sea_orm::DbErr),
+2
View File
@@ -0,0 +1,2 @@
pub mod error;
pub mod model;
@@ -0,0 +1,57 @@
use crate::error::DomainError;
use lldap_domain::{
schema::AttributeList,
types::{Attribute, AttributeName, AttributeType, AttributeValue, Cardinality, Serialized},
};
// Value must be a serialized attribute value of the type denoted by typ,
// and either a singleton or unbounded list, depending on is_list.
pub fn deserialize_attribute_value(
value: &Serialized,
typ: AttributeType,
is_list: bool,
) -> AttributeValue {
match (typ, is_list) {
(AttributeType::String, false) => {
AttributeValue::String(Cardinality::Singleton(value.unwrap()))
}
(AttributeType::String, true) => {
AttributeValue::String(Cardinality::Unbounded(value.unwrap()))
}
(AttributeType::Integer, false) => {
AttributeValue::Integer(Cardinality::Singleton(value.unwrap::<i64>()))
}
(AttributeType::Integer, true) => {
AttributeValue::Integer(Cardinality::Unbounded(value.unwrap()))
}
(AttributeType::DateTime, false) => {
AttributeValue::DateTime(Cardinality::Singleton(value.unwrap()))
}
(AttributeType::DateTime, true) => {
AttributeValue::DateTime(Cardinality::Unbounded(value.unwrap()))
}
(AttributeType::JpegPhoto, false) => {
AttributeValue::JpegPhoto(Cardinality::Singleton(value.unwrap()))
}
(AttributeType::JpegPhoto, true) => {
AttributeValue::JpegPhoto(Cardinality::Unbounded(value.unwrap()))
}
}
}
pub fn deserialize_attribute(
name: AttributeName,
value: &Serialized,
schema: &AttributeList,
) -> Result<Attribute, DomainError> {
match schema.get_attribute_type(&name) {
Some((typ, is_list)) => Ok(Attribute {
name,
value: deserialize_attribute_value(value, typ, is_list),
}),
None => Err(DomainError::InternalError(format!(
"Unable to find schema for attribute named '{}'",
name.into_string()
))),
}
}
@@ -1,8 +1,8 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::domain::{
handler::AttributeSchema,
use lldap_domain::{
schema::AttributeSchema,
types::{AttributeName, AttributeType},
};
@@ -50,6 +50,7 @@ impl From<Model> for AttributeSchema {
is_visible: value.is_group_visible,
is_editable: value.is_group_editable,
is_hardcoded: value.is_hardcoded,
is_readonly: false,
}
}
}
@@ -1,7 +1,7 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::domain::types::{AttributeName, AttributeValue, GroupId, Serialized};
use lldap_domain::types::{AttributeName, GroupId, Serialized};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "group_attributes")]
@@ -55,18 +55,3 @@ impl Related<super::GroupAttributeSchema> for Entity {
}
impl ActiveModelBehavior for ActiveModel {}
impl From<Model> for AttributeValue {
fn from(
Model {
group_id: _,
attribute_name,
value,
}: Model,
) -> Self {
Self {
name: attribute_name,
value,
}
}
}
@@ -1,7 +1,7 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::domain::types::LdapObjectClass;
use lldap_domain::types::LdapObjectClass;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "group_object_classes")]
@@ -3,7 +3,7 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::domain::types::{GroupId, GroupName, Uuid};
use lldap_domain::types::{GroupId, GroupName, Uuid};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "groups")]
@@ -14,6 +14,7 @@ pub struct Model {
pub lowercase_display_name: String,
pub creation_date: chrono::NaiveDateTime,
pub uuid: Uuid,
pub modified_date: chrono::NaiveDateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@@ -30,7 +31,7 @@ impl Related<super::memberships::Entity> for Entity {
impl ActiveModelBehavior for ActiveModel {}
impl From<Model> for crate::domain::types::Group {
impl From<Model> for lldap_domain::types::Group {
fn from(group: Model) -> Self {
Self {
id: group.group_id,
@@ -39,11 +40,12 @@ impl From<Model> for crate::domain::types::Group {
uuid: group.uuid,
users: vec![],
attributes: Vec::new(),
modified_date: group.modified_date,
}
}
}
impl From<Model> for crate::domain::types::GroupDetails {
impl From<Model> for lldap_domain::types::GroupDetails {
fn from(group: Model) -> Self {
Self {
group_id: group.group_id,
@@ -51,6 +53,7 @@ impl From<Model> for crate::domain::types::GroupDetails {
creation_date: group.creation_date,
uuid: group.uuid,
attributes: Vec::new(),
modified_date: group.modified_date,
}
}
}
@@ -3,7 +3,7 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::domain::types::UserId;
use lldap_domain::types::UserId;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "jwt_refresh_storage")]
@@ -3,7 +3,7 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::domain::types::UserId;
use lldap_domain::types::UserId;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "jwt_storage")]
@@ -3,7 +3,7 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::domain::types::{GroupId, UserId};
use lldap_domain::types::{GroupId, UserId};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "memberships")]
@@ -1,5 +1,6 @@
pub mod prelude;
pub mod deserialize;
pub mod groups;
pub mod jwt_refresh_storage;
pub mod jwt_storage;
@@ -3,7 +3,7 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::domain::types::UserId;
use lldap_domain::types::UserId;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "password_reset_tokens")]
@@ -1,8 +1,8 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::domain::{
handler::AttributeSchema,
use lldap_domain::{
schema::AttributeSchema,
types::{AttributeName, AttributeType},
};
@@ -50,6 +50,7 @@ impl From<Model> for AttributeSchema {
is_visible: value.is_user_visible,
is_editable: value.is_user_editable,
is_hardcoded: value.is_hardcoded,
is_readonly: false,
}
}
}
@@ -1,7 +1,7 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::domain::types::{AttributeName, AttributeValue, Serialized, UserId};
use lldap_domain::types::{AttributeName, Serialized, UserId};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "user_attributes")]
@@ -55,18 +55,3 @@ impl Related<super::UserAttributeSchema> for Entity {
}
impl ActiveModelBehavior for ActiveModel {}
impl From<Model> for AttributeValue {
fn from(
Model {
user_id: _,
attribute_name,
value,
}: Model,
) -> Self {
Self {
name: attribute_name,
value,
}
}
}
@@ -1,7 +1,7 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::domain::types::LdapObjectClass;
use lldap_domain::types::LdapObjectClass;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "user_object_classes")]
@@ -1,9 +1,9 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.3
use sea_orm::{entity::prelude::*, sea_query::BlobSize};
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::domain::types::{Email, UserId, Uuid};
use lldap_domain::types::{Email, UserId, Uuid};
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;
@@ -21,6 +21,8 @@ pub struct Model {
pub totp_secret: Option<String>,
pub mfa_type: Option<String>,
pub uuid: Uuid,
pub modified_date: chrono::NaiveDateTime,
pub password_modified_date: chrono::NaiveDateTime,
}
impl EntityName for Entity {
@@ -40,6 +42,8 @@ pub enum Column {
TotpSecret,
MfaType,
Uuid,
ModifiedDate,
PasswordModifiedDate,
}
impl ColumnTrait for Column {
@@ -47,15 +51,17 @@ impl ColumnTrait for Column {
fn def(&self) -> ColumnDef {
match self {
Column::UserId => ColumnType::String(Some(255)),
Column::Email => ColumnType::String(Some(255)),
Column::LowercaseEmail => ColumnType::String(Some(255)),
Column::DisplayName => ColumnType::String(Some(255)),
Column::UserId => ColumnType::String(StringLen::N(255)),
Column::Email => ColumnType::String(StringLen::N(255)),
Column::LowercaseEmail => ColumnType::String(StringLen::N(255)),
Column::DisplayName => ColumnType::String(StringLen::N(255)),
Column::CreationDate => ColumnType::DateTime,
Column::PasswordHash => ColumnType::Binary(BlobSize::Medium),
Column::TotpSecret => ColumnType::String(Some(64)),
Column::MfaType => ColumnType::String(Some(64)),
Column::Uuid => ColumnType::String(Some(36)),
Column::PasswordHash => ColumnType::Blob,
Column::TotpSecret => ColumnType::String(StringLen::N(64)),
Column::MfaType => ColumnType::String(StringLen::N(64)),
Column::Uuid => ColumnType::String(StringLen::N(36)),
Column::ModifiedDate => ColumnType::DateTime,
Column::PasswordModifiedDate => ColumnType::DateTime,
}
.def()
}
@@ -112,7 +118,7 @@ impl Related<super::password_reset_tokens::Entity> for Entity {
impl ActiveModelBehavior for ActiveModel {}
impl From<Model> for crate::domain::types::User {
impl From<Model> for lldap_domain::types::User {
fn from(user: Model) -> Self {
Self {
user_id: user.user_id,
@@ -121,6 +127,8 @@ impl From<Model> for crate::domain::types::User {
creation_date: user.creation_date,
uuid: user.uuid,
attributes: Vec::new(),
modified_date: user.modified_date,
password_modified_date: user.password_modified_date,
}
}
}
+65
View File
@@ -0,0 +1,65 @@
[package]
name = "lldap_domain"
version = "0.1.0"
authors = [
"Valentin Tolmer <valentin@tolmer.fr>",
"Simon Broeng Jensen <sbj@cwconsult.dk>",
]
edition.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[features]
test = []
[dependencies]
anyhow = "*"
base64 = "0.21"
bincode = "1.3"
itertools = "0.10"
juniper = "0.15"
serde_bytes = "0.11"
[dev-dependencies]
pretty_assertions = "1"
[dependencies.chrono]
features = ["serde"]
version = "*"
[dependencies.derive_more]
features = ["debug", "display", "from", "from_str"]
default-features = false
version = "1"
[dependencies.image]
features = ["jpeg"]
default-features = false
version = "0.24"
[dependencies.lldap_auth]
path = "../auth"
features = ["opaque_server", "opaque_client", "sea_orm"]
[dependencies.sea-orm]
workspace = true
features = [
"macros",
"with-chrono",
"with-uuid",
"sqlx-all",
"runtime-actix-rustls",
]
[dependencies.serde]
workspace = true
[dependencies.strum]
features = ["derive"]
version = "0.25"
[dependencies.uuid]
features = ["v1", "v3"]
version = "1"
+41
View File
@@ -0,0 +1,41 @@
use crate::types::{AttributeType, AttributeValue, JpegPhoto};
use anyhow::{Context as AnyhowContext, Result, bail};
pub fn deserialize_attribute_value(
value: &[String],
typ: AttributeType,
is_list: bool,
) -> Result<AttributeValue> {
if !is_list && value.len() != 1 {
bail!("Attribute is not a list, but multiple values were provided",);
}
let parse_int = |value: &String| -> Result<i64> {
value
.parse::<i64>()
.with_context(|| format!("Invalid integer value {value}"))
};
let parse_date = |value: &String| -> Result<chrono::NaiveDateTime> {
Ok(chrono::DateTime::parse_from_rfc3339(value)
.with_context(|| format!("Invalid date value {value}"))?
.naive_utc())
};
let parse_photo = |value: &String| -> Result<JpegPhoto> {
JpegPhoto::try_from(value.as_str()).context("Provided image is not a valid JPEG")
};
Ok(match (typ, is_list) {
(AttributeType::String, false) => value[0].clone().into(),
(AttributeType::String, true) => value.to_vec().into(),
(AttributeType::Integer, false) => (parse_int(&value[0])?).into(),
(AttributeType::Integer, true) => {
(value.iter().map(parse_int).collect::<Result<Vec<_>>>()?).into()
}
(AttributeType::DateTime, false) => (parse_date(&value[0])?).into(),
(AttributeType::DateTime, true) => {
(value.iter().map(parse_date).collect::<Result<Vec<_>>>()?).into()
}
(AttributeType::JpegPhoto, false) => (parse_photo(&value[0])?).into(),
(AttributeType::JpegPhoto, true) => {
(value.iter().map(parse_photo).collect::<Result<Vec<_>>>()?).into()
}
})
}

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