Compare commits

..

22 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 982d79fb53 Add copilot-setup-steps.yml for GitHub Copilot agent environment setup
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-08-20 21:56:09 +00:00
copilot-swe-agent[bot] 4abe74dacc Add comprehensive GitHub Copilot instructions for LLDAP repository
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-08-20 21:40:54 +00:00
copilot-swe-agent[bot] 0635a21eb8 Generate comprehensive Copilot instructions for LLDAP repository
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-08-20 21:39:14 +00:00
copilot-swe-agent[bot] 840a115e0e Initial plan 2025-08-20 21:14:41 +00: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
60 changed files with 665 additions and 347 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
FROM rust:1.74 FROM rust:1.85
ARG USERNAME=lldapdev ARG USERNAME=lldapdev
# We need to keep the user as 1001 to match the GitHub runner's UID. # We need to keep the user as 1001 to match the GitHub runner's UID.
+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 FROM localhost:5000/lldap/lldap:alpine-base
# Taken directly from https://github.com/tianon/gosu/blob/master/INSTALL.md # 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; \ RUN set -eux; \
\ \
apk add --no-cache --virtual .gosu-deps \ apk add --no-cache --virtual .gosu-deps \
@@ -15,7 +15,18 @@ RUN set -eux; \
\ \
# verify the signature # verify the signature
export GNUPGHOME="$(mktemp -d)"; \ 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; \ gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
gpgconf --kill all; \ gpgconf --kill all; \
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \ rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
+19 -5
View File
@@ -1,12 +1,15 @@
FROM localhost:5000/lldap/lldap:debian-base FROM localhost:5000/lldap/lldap:debian-base
# Taken directly from https://github.com/tianon/gosu/blob/master/INSTALL.md # 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; \ RUN set -eux; \
# save list of currently installed packages for later so we can clean up # save list of currently installed packages for later so we can clean up
savedAptMark="$(apt-mark showmanual)"; \ savedAptMark="$(apt-mark showmanual)"; \
apt-get update; \ for i in 1 2 3; do \
apt-get install -y --no-install-recommends ca-certificates gnupg wget; \ apt-get update && \
rm -rf /var/lib/apt/lists/*; \ 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 }')"; \ dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \ wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
@@ -14,7 +17,18 @@ RUN set -eux; \
\ \
# verify the signature # verify the signature
export GNUPGHOME="$(mktemp -d)"; \ 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; \ gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
gpgconf --kill all; \ gpgconf --kill all; \
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \ rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
+4 -4
View File
@@ -87,7 +87,7 @@ jobs:
image: lldap/rust-dev:latest image: lldap/rust-dev:latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
- uses: actions/cache@v4 - uses: actions/cache@v4
with: with:
path: | path: |
@@ -132,7 +132,7 @@ jobs:
CARGO_HOME: ${GITHUB_WORKSPACE}/.cargo CARGO_HOME: ${GITHUB_WORKSPACE}/.cargo
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
- uses: actions/cache@v4 - uses: actions/cache@v4
with: with:
path: | path: |
@@ -300,7 +300,7 @@ jobs:
steps: steps:
- name: Checkout scripts - name: Checkout scripts
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
with: with:
sparse-checkout: 'scripts' sparse-checkout: 'scripts'
@@ -496,7 +496,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
- name: Download all artifacts - name: Download all artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
+4 -4
View File
@@ -33,7 +33,7 @@ jobs:
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- name: Build - name: Build
run: cargo build --verbose --workspace run: cargo build --verbose --workspace
@@ -52,7 +52,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
@@ -69,7 +69,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
@@ -88,7 +88,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
- name: Install Rust - 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 run: rustup toolchain install nightly --component llvm-tools-preview && rustup component add llvm-tools-preview --toolchain stable-x86_64-unknown-linux-gnu
+55
View File
@@ -5,6 +5,61 @@ 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/), 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). 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 ## [0.6.1] 2024-11-22
Small release, mainly to fix a migration issue with Sqlite and Postgresql. Small release, mainly to fix a migration issue with Sqlite and Postgresql.
Generated
+5 -3
View File
@@ -2506,7 +2506,7 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]] [[package]]
name = "lldap" name = "lldap"
version = "0.6.2-alpha" version = "0.6.2"
dependencies = [ dependencies = [
"actix", "actix",
"actix-files", "actix-files",
@@ -2532,7 +2532,7 @@ dependencies = [
"futures-util", "futures-util",
"graphql_client 0.11.0", "graphql_client 0.11.0",
"hmac 0.12.1", "hmac 0.12.1",
"http 1.1.0", "http 0.2.12",
"juniper", "juniper",
"jwt 0.16.0", "jwt 0.16.0",
"ldap3", "ldap3",
@@ -2599,11 +2599,12 @@ dependencies = [
[[package]] [[package]]
name = "lldap_app" name = "lldap_app"
version = "0.6.2-alpha" version = "0.6.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.13.1", "base64 0.13.1",
"chrono", "chrono",
"derive_more 1.0.0",
"gloo-console", "gloo-console",
"gloo-file", "gloo-file",
"gloo-net", "gloo-net",
@@ -2618,6 +2619,7 @@ dependencies = [
"rand 0.8.5", "rand 0.8.5",
"serde", "serde",
"serde_json", "serde_json",
"strum 0.25.0",
"url-escape", "url-escape",
"validator", "validator",
"validator_derive", "validator_derive",
+1 -1
View File
@@ -200,7 +200,7 @@ service that seems definitely incompatible with LLDAP.
- [I can't login](docs/faq.md#i-cant-log-in) - [I can't login](docs/faq.md#i-cant-log-in)
- [Discord Integration](docs/faq.md#discord-integration) - [Discord Integration](docs/faq.md#discord-integration)
- [Migrating from SQLite](docs/faq.md#migrating-from-sqlite) - [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) - 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 support vhosts?](docs/faq.md#does-lldap-support-vhosts)
- [Does lldap provide commercial support contracts?](docs/faq.md#does-lldap-provide-commercial-support-contracts) - [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) - [Can I make a donation to fund development?](docs/faq.md#can-i-make-a-donation-to-fund-development)
+10 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "lldap_app" name = "lldap_app"
version = "0.6.2-alpha" version = "0.6.2"
description = "Frontend for LLDAP" description = "Frontend for LLDAP"
edition.workspace = true edition.workspace = true
include = ["src/**/*", "queries/**/*", "Cargo.toml", "../schema.graphql"] include = ["src/**/*", "queries/**/*", "Cargo.toml", "../schema.graphql"]
@@ -55,6 +55,11 @@ features = [
"wasmbind" "wasmbind"
] ]
[dependencies.derive_more]
features = ["debug", "display", "from", "from_str"]
default-features = false
version = "1"
[dependencies.lldap_auth] [dependencies.lldap_auth]
path = "../crates/auth" path = "../crates/auth"
features = [ "opaque_client" ] features = [ "opaque_client" ]
@@ -73,6 +78,10 @@ version = "0.24"
[dependencies.serde] [dependencies.serde]
workspace = true workspace = true
[dependencies.strum]
features = ["derive"]
version = "0.25"
[dependencies.yew_form] [dependencies.yew_form]
git = "https://github.com/jfbilodeau/yew_form" git = "https://github.com/jfbilodeau/yew_form"
rev = "4b9fabffb63393ec7626a4477fd36de12a07fac9" rev = "4b9fabffb63393ec7626a4477fd36de12a07fac9"
+4 -6
View File
@@ -7,7 +7,6 @@ use crate::{
}, },
router::AppRoute, router::AppRoute,
}, },
convert_attribute_type,
infra::{ infra::{
common_component::{CommonComponent, CommonComponentParts}, common_component::{CommonComponent, CommonComponentParts},
form_utils::{ form_utils::{
@@ -30,7 +29,8 @@ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
schema_path = "../schema.graphql", schema_path = "../schema.graphql",
query_path = "queries/get_group_attributes_schema.graphql", query_path = "queries/get_group_attributes_schema.graphql",
response_derives = "Debug,Clone,PartialEq,Eq", response_derives = "Debug,Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql" custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)] )]
pub struct GetGroupAttributesSchema; pub struct GetGroupAttributesSchema;
@@ -39,8 +39,6 @@ use get_group_attributes_schema::ResponseData;
pub type Attribute = pub type Attribute =
get_group_attributes_schema::GetGroupAttributesSchemaSchemaGroupSchemaAttributes; get_group_attributes_schema::GetGroupAttributesSchemaSchemaGroupSchemaAttributes;
convert_attribute_type!(get_group_attributes_schema::AttributeType);
impl From<&Attribute> for GraphQlAttributeSchema { impl From<&Attribute> for GraphQlAttributeSchema {
fn from(attr: &Attribute) -> Self { fn from(attr: &Attribute) -> Self {
Self { Self {
@@ -218,14 +216,14 @@ fn get_custom_attribute_input(attribute_schema: &Attribute) -> Html {
html! { html! {
<ListAttributeInput <ListAttributeInput
name={attribute_schema.name.clone()} name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())} attribute_type={attribute_schema.attribute_type}
/> />
} }
} else { } else {
html! { html! {
<SingleAttributeInput <SingleAttributeInput
name={attribute_schema.name.clone()} name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())} attribute_type={attribute_schema.attribute_type}
/> />
} }
} }
+6 -7
View File
@@ -3,7 +3,6 @@ use crate::{
form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit}, form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit},
router::AppRoute, router::AppRoute,
}, },
convert_attribute_type,
infra::{ infra::{
common_component::{CommonComponent, CommonComponentParts}, common_component::{CommonComponent, CommonComponentParts},
schema::{AttributeType, validate_attribute_type}, schema::{AttributeType, validate_attribute_type},
@@ -23,12 +22,11 @@ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
schema_path = "../schema.graphql", schema_path = "../schema.graphql",
query_path = "queries/create_group_attribute.graphql", query_path = "queries/create_group_attribute.graphql",
response_derives = "Debug", response_derives = "Debug",
custom_scalars_module = "crate::infra::graphql" custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)] )]
pub struct CreateGroupAttribute; pub struct CreateGroupAttribute;
convert_attribute_type!(create_group_attribute::AttributeType);
pub struct CreateGroupAttributeForm { pub struct CreateGroupAttributeForm {
common: CommonComponentParts<Self>, common: CommonComponentParts<Self>,
form: yew_form::Form<CreateGroupAttributeModel>, form: yew_form::Form<CreateGroupAttributeModel>,
@@ -70,10 +68,11 @@ impl CommonComponent<CreateGroupAttributeForm> for CreateGroupAttributeForm {
invalid invalid
); );
})?; })?;
let attribute_type = model.attribute_type.parse::<AttributeType>().unwrap(); let attribute_type =
AttributeType::try_from(model.attribute_type.as_str()).unwrap();
let req = create_group_attribute::Variables { let req = create_group_attribute::Variables {
name: model.attribute_name, name: model.attribute_name,
attribute_type: create_group_attribute::AttributeType::from(attribute_type), attribute_type,
is_list: model.is_list, is_list: model.is_list,
is_visible: model.is_visible, is_visible: model.is_visible,
}; };
@@ -145,7 +144,7 @@ impl Component for CreateGroupAttributeForm {
oninput={link.callback(|_| Msg::Update)}> oninput={link.callback(|_| Msg::Update)}>
<option selected=true value="String">{"String"}</option> <option selected=true value="String">{"String"}</option>
<option value="Integer">{"Integer"}</option> <option value="Integer">{"Integer"}</option>
<option value="Jpeg">{"Jpeg"}</option> <option value="JpegPhoto">{"Jpeg"}</option>
<option value="DateTime">{"DateTime"}</option> <option value="DateTime">{"DateTime"}</option>
</Select<CreateGroupAttributeModel>> </Select<CreateGroupAttributeModel>>
<CheckBox<CreateGroupAttributeModel> <CheckBox<CreateGroupAttributeModel>
+4 -6
View File
@@ -7,7 +7,6 @@ use crate::{
}, },
router::AppRoute, router::AppRoute,
}, },
convert_attribute_type,
infra::{ infra::{
api::HostService, api::HostService,
common_component::{CommonComponent, CommonComponentParts}, common_component::{CommonComponent, CommonComponentParts},
@@ -32,7 +31,8 @@ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
schema_path = "../schema.graphql", schema_path = "../schema.graphql",
query_path = "queries/get_user_attributes_schema.graphql", query_path = "queries/get_user_attributes_schema.graphql",
response_derives = "Debug,Clone,PartialEq,Eq", response_derives = "Debug,Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql" custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)] )]
pub struct GetUserAttributesSchema; pub struct GetUserAttributesSchema;
@@ -40,8 +40,6 @@ use get_user_attributes_schema::ResponseData;
pub type Attribute = get_user_attributes_schema::GetUserAttributesSchemaSchemaUserSchemaAttributes; pub type Attribute = get_user_attributes_schema::GetUserAttributesSchemaSchemaUserSchemaAttributes;
convert_attribute_type!(get_user_attributes_schema::AttributeType);
impl From<&Attribute> for GraphQlAttributeSchema { impl From<&Attribute> for GraphQlAttributeSchema {
fn from(attr: &Attribute) -> Self { fn from(attr: &Attribute) -> Self {
Self { Self {
@@ -310,14 +308,14 @@ fn get_custom_attribute_input(attribute_schema: &Attribute) -> Html {
html! { html! {
<ListAttributeInput <ListAttributeInput
name={attribute_schema.name.clone()} name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())} attribute_type={attribute_schema.attribute_type}
/> />
} }
} else { } else {
html! { html! {
<SingleAttributeInput <SingleAttributeInput
name={attribute_schema.name.clone()} name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())} attribute_type={attribute_schema.attribute_type}
/> />
} }
} }
+6 -7
View File
@@ -3,7 +3,6 @@ use crate::{
form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit}, form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit},
router::AppRoute, router::AppRoute,
}, },
convert_attribute_type,
infra::{ infra::{
common_component::{CommonComponent, CommonComponentParts}, common_component::{CommonComponent, CommonComponentParts},
schema::{AttributeType, validate_attribute_type}, schema::{AttributeType, validate_attribute_type},
@@ -23,12 +22,11 @@ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
schema_path = "../schema.graphql", schema_path = "../schema.graphql",
query_path = "queries/create_user_attribute.graphql", query_path = "queries/create_user_attribute.graphql",
response_derives = "Debug", response_derives = "Debug",
custom_scalars_module = "crate::infra::graphql" custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)] )]
pub struct CreateUserAttribute; pub struct CreateUserAttribute;
convert_attribute_type!(create_user_attribute::AttributeType);
pub struct CreateUserAttributeForm { pub struct CreateUserAttributeForm {
common: CommonComponentParts<Self>, common: CommonComponentParts<Self>,
form: yew_form::Form<CreateUserAttributeModel>, form: yew_form::Form<CreateUserAttributeModel>,
@@ -74,10 +72,11 @@ impl CommonComponent<CreateUserAttributeForm> for CreateUserAttributeForm {
invalid invalid
); );
})?; })?;
let attribute_type = model.attribute_type.parse::<AttributeType>().unwrap(); let attribute_type =
AttributeType::try_from(model.attribute_type.as_str()).unwrap();
let req = create_user_attribute::Variables { let req = create_user_attribute::Variables {
name: model.attribute_name, name: model.attribute_name,
attribute_type: create_user_attribute::AttributeType::from(attribute_type), attribute_type,
is_editable: model.is_editable, is_editable: model.is_editable,
is_list: model.is_list, is_list: model.is_list,
is_visible: model.is_visible, is_visible: model.is_visible,
@@ -147,7 +146,7 @@ impl Component for CreateUserAttributeForm {
oninput={link.callback(|_| Msg::Update)}> oninput={link.callback(|_| Msg::Update)}>
<option selected=true value="String">{"String"}</option> <option selected=true value="String">{"String"}</option>
<option value="Integer">{"Integer"}</option> <option value="Integer">{"Integer"}</option>
<option value="Jpeg">{"Jpeg"}</option> <option value="JpegPhoto">{"Jpeg"}</option>
<option value="DateTime">{"DateTime"}</option> <option value="DateTime">{"DateTime"}</option>
</Select<CreateUserAttributeModel>> </Select<CreateUserAttributeModel>>
<CheckBox<CreateUserAttributeModel> <CheckBox<CreateUserAttributeModel>
+5 -5
View File
@@ -26,7 +26,7 @@ fn attribute_input(props: &AttributeInputProps) -> Html {
<DateTimeInput name={props.name.clone()} value={props.value.clone()} /> <DateTimeInput name={props.name.clone()} value={props.value.clone()} />
}; };
} }
AttributeType::Jpeg => { AttributeType::JpegPhoto => {
return html! { return html! {
<JpegFileInput name={props.name.clone()} value={props.value.clone()} /> <JpegFileInput name={props.name.clone()} value={props.value.clone()} />
}; };
@@ -82,7 +82,7 @@ fn attribute_label(props: &AttributeLabelProps) -> Html {
#[derive(Properties, PartialEq)] #[derive(Properties, PartialEq)]
pub struct SingleAttributeInputProps { pub struct SingleAttributeInputProps {
pub name: String, pub name: String,
pub attribute_type: AttributeType, pub(crate) attribute_type: AttributeType,
#[prop_or(None)] #[prop_or(None)]
pub value: Option<String>, pub value: Option<String>,
} }
@@ -94,7 +94,7 @@ pub fn single_attribute_input(props: &SingleAttributeInputProps) -> Html {
<AttributeLabel name={props.name.clone()} /> <AttributeLabel name={props.name.clone()} />
<div class="col-8"> <div class="col-8">
<AttributeInput <AttributeInput
attribute_type={props.attribute_type.clone()} attribute_type={props.attribute_type}
name={props.name.clone()} name={props.name.clone()}
value={props.value.clone()} /> value={props.value.clone()} />
</div> </div>
@@ -105,7 +105,7 @@ pub fn single_attribute_input(props: &SingleAttributeInputProps) -> Html {
#[derive(Properties, PartialEq)] #[derive(Properties, PartialEq)]
pub struct ListAttributeInputProps { pub struct ListAttributeInputProps {
pub name: String, pub name: String,
pub attribute_type: AttributeType, pub(crate) attribute_type: AttributeType,
#[prop_or(vec!())] #[prop_or(vec!())]
pub values: Vec<String>, pub values: Vec<String>,
} }
@@ -165,7 +165,7 @@ impl Component for ListAttributeInput {
{self.indices.iter().map(|&i| html! { {self.indices.iter().map(|&i| html! {
<div class="input-group mb-2" key={i}> <div class="input-group mb-2" key={i}>
<AttributeInput <AttributeInput
attribute_type={props.attribute_type.clone()} attribute_type={props.attribute_type}
name={props.name.clone()} name={props.name.clone()}
value={props.values.get(i).cloned().unwrap_or_default()} /> value={props.values.get(i).cloned().unwrap_or_default()} />
<button <button
+3 -5
View File
@@ -5,10 +5,10 @@ use crate::{
remove_user_from_group::RemoveUserFromGroupComponent, remove_user_from_group::RemoveUserFromGroupComponent,
router::{AppRoute, Link}, router::{AppRoute, Link},
}, },
convert_attribute_type,
infra::{ infra::{
common_component::{CommonComponent, CommonComponentParts}, common_component::{CommonComponent, CommonComponentParts},
form_utils::GraphQlAttributeSchema, form_utils::GraphQlAttributeSchema,
schema::AttributeType,
}, },
}; };
use anyhow::{Error, Result, bail}; use anyhow::{Error, Result, bail};
@@ -20,7 +20,8 @@ use yew::prelude::*;
schema_path = "../schema.graphql", schema_path = "../schema.graphql",
query_path = "queries/get_group_details.graphql", query_path = "queries/get_group_details.graphql",
response_derives = "Debug, Hash, PartialEq, Eq, Clone", 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 struct GetGroupDetails;
@@ -29,9 +30,6 @@ pub type User = get_group_details::GetGroupDetailsGroupUsers;
pub type AddGroupMemberUser = add_group_member::User; pub type AddGroupMemberUser = add_group_member::User;
pub type Attribute = get_group_details::GetGroupDetailsGroupAttributes; pub type Attribute = get_group_details::GetGroupDetailsGroupAttributes;
pub type AttributeSchema = get_group_details::GetGroupDetailsSchemaGroupSchemaAttributes; pub type AttributeSchema = get_group_details::GetGroupDetailsSchemaGroupSchemaAttributes;
pub type AttributeType = get_group_details::AttributeType;
convert_attribute_type!(AttributeType);
impl From<&AttributeSchema> for GraphQlAttributeSchema { impl From<&AttributeSchema> for GraphQlAttributeSchema {
fn from(attr: &AttributeSchema) -> Self { fn from(attr: &AttributeSchema) -> Self {
+2 -3
View File
@@ -10,7 +10,6 @@ use crate::{
infra::{ infra::{
common_component::{CommonComponent, CommonComponentParts}, common_component::{CommonComponent, CommonComponentParts},
form_utils::{AttributeValue, EmailIsRequired, IsAdmin, read_all_form_attributes}, form_utils::{AttributeValue, EmailIsRequired, IsAdmin, read_all_form_attributes},
schema::AttributeType,
}, },
}; };
use anyhow::{Ok, Result}; use anyhow::{Ok, Result};
@@ -174,7 +173,7 @@ fn get_custom_attribute_input(
html! { html! {
<ListAttributeInput <ListAttributeInput
name={attribute_schema.name.clone()} name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())} attribute_type={attribute_schema.attribute_type}
values={values} values={values}
/> />
} }
@@ -182,7 +181,7 @@ fn get_custom_attribute_input(
html! { html! {
<SingleAttributeInput <SingleAttributeInput
name={attribute_schema.name.clone()} name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())} attribute_type={attribute_schema.attribute_type}
value={values.first().cloned().unwrap_or_default()} value={values.first().cloned().unwrap_or_default()}
/> />
} }
+3 -5
View File
@@ -4,7 +4,6 @@ use crate::{
fragments::attribute_schema::render_attribute_name, fragments::attribute_schema::render_attribute_name,
router::{AppRoute, Link}, router::{AppRoute, Link},
}, },
convert_attribute_type,
infra::{ infra::{
attributes::group, attributes::group,
common_component::{CommonComponent, CommonComponentParts}, common_component::{CommonComponent, CommonComponentParts},
@@ -21,7 +20,8 @@ use yew::prelude::*;
schema_path = "../schema.graphql", schema_path = "../schema.graphql",
query_path = "queries/get_group_attributes_schema.graphql", query_path = "queries/get_group_attributes_schema.graphql",
response_derives = "Debug,Clone,PartialEq,Eq", response_derives = "Debug,Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql" custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)] )]
pub struct GetGroupAttributesSchema; pub struct GetGroupAttributesSchema;
@@ -30,8 +30,6 @@ use get_group_attributes_schema::ResponseData;
pub type Attribute = pub type Attribute =
get_group_attributes_schema::GetGroupAttributesSchemaSchemaGroupSchemaAttributes; get_group_attributes_schema::GetGroupAttributesSchemaSchemaGroupSchemaAttributes;
convert_attribute_type!(get_group_attributes_schema::AttributeType);
#[derive(yew::Properties, Clone, PartialEq, Eq)] #[derive(yew::Properties, Clone, PartialEq, Eq)]
pub struct Props { pub struct Props {
pub hardcoded: bool, pub hardcoded: bool,
@@ -147,7 +145,7 @@ impl GroupSchemaTable {
fn view_attribute(&self, ctx: &Context<Self>, attribute: &Attribute) -> Html { fn view_attribute(&self, ctx: &Context<Self>, attribute: &Attribute) -> Html {
let link = ctx.link(); let link = ctx.link();
let attribute_type = AttributeType::from(attribute.attribute_type.clone()); let attribute_type = attribute.attribute_type;
let checkmark = html! { 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"> <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> <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>
+3 -5
View File
@@ -5,10 +5,10 @@ use crate::{
router::{AppRoute, Link}, router::{AppRoute, Link},
user_details_form::UserDetailsForm, user_details_form::UserDetailsForm,
}, },
convert_attribute_type,
infra::{ infra::{
common_component::{CommonComponent, CommonComponentParts}, common_component::{CommonComponent, CommonComponentParts},
form_utils::GraphQlAttributeSchema, form_utils::GraphQlAttributeSchema,
schema::AttributeType,
}, },
}; };
use anyhow::{Error, Result, bail}; use anyhow::{Error, Result, bail};
@@ -20,7 +20,8 @@ use yew::prelude::*;
schema_path = "../schema.graphql", schema_path = "../schema.graphql",
query_path = "queries/get_user_details.graphql", query_path = "queries/get_user_details.graphql",
response_derives = "Debug, Hash, PartialEq, Eq, Clone", 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 struct GetUserDetails;
@@ -28,9 +29,6 @@ pub type User = get_user_details::GetUserDetailsUser;
pub type Group = get_user_details::GetUserDetailsUserGroups; pub type Group = get_user_details::GetUserDetailsUserGroups;
pub type Attribute = get_user_details::GetUserDetailsUserAttributes; pub type Attribute = get_user_details::GetUserDetailsUserAttributes;
pub type AttributeSchema = get_user_details::GetUserDetailsSchemaUserSchemaAttributes; pub type AttributeSchema = get_user_details::GetUserDetailsSchemaUserSchemaAttributes;
pub type AttributeType = get_user_details::AttributeType;
convert_attribute_type!(AttributeType);
impl From<&AttributeSchema> for GraphQlAttributeSchema { impl From<&AttributeSchema> for GraphQlAttributeSchema {
fn from(attr: &AttributeSchema) -> Self { fn from(attr: &AttributeSchema) -> Self {
+14 -3
View File
@@ -14,6 +14,7 @@ use crate::{
}, },
}; };
use anyhow::{Ok, Result}; use anyhow::{Ok, Result};
use gloo_console::console;
use graphql_client::GraphQLQuery; use graphql_client::GraphQLQuery;
use yew::prelude::*; use yew::prelude::*;
@@ -168,7 +169,7 @@ fn get_custom_attribute_input(
html! { html! {
<ListAttributeInput <ListAttributeInput
name={attribute_schema.name.clone()} name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())} attribute_type={attribute_schema.attribute_type}
values={values} values={values}
/> />
} }
@@ -176,7 +177,7 @@ fn get_custom_attribute_input(
html! { html! {
<SingleAttributeInput <SingleAttributeInput
name={attribute_schema.name.clone()} name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())} attribute_type={attribute_schema.attribute_type}
value={values.first().cloned().unwrap_or_default()} value={values.first().cloned().unwrap_or_default()}
/> />
} }
@@ -192,9 +193,19 @@ fn get_custom_attribute_static(
.find(|a| a.name == attribute_schema.name) .find(|a| a.name == attribute_schema.name)
.map(|attribute| attribute.value.clone()) .map(|attribute| attribute.value.clone())
.unwrap_or_default(); .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! { html! {
<StaticValue label={attribute_schema.name.clone()} id={attribute_schema.name.clone()}> <StaticValue label={attribute_schema.name.clone()} id={attribute_schema.name.clone()}>
{values.into_iter().map(|x| html!{<div>{x}</div>}).collect::<Vec<_>>()} {values.into_iter().map(|x| html!{<div>{value_to_str(x)}</div>}).collect::<Vec<_>>()}
</StaticValue> </StaticValue>
} }
} }
+3 -5
View File
@@ -4,7 +4,6 @@ use crate::{
fragments::attribute_schema::render_attribute_name, fragments::attribute_schema::render_attribute_name,
router::{AppRoute, Link}, router::{AppRoute, Link},
}, },
convert_attribute_type,
infra::{ infra::{
attributes::user, attributes::user,
common_component::{CommonComponent, CommonComponentParts}, common_component::{CommonComponent, CommonComponentParts},
@@ -21,7 +20,8 @@ use yew::prelude::*;
schema_path = "../schema.graphql", schema_path = "../schema.graphql",
query_path = "queries/get_user_attributes_schema.graphql", query_path = "queries/get_user_attributes_schema.graphql",
response_derives = "Debug,Clone,PartialEq,Eq", response_derives = "Debug,Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql" custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)] )]
pub struct GetUserAttributesSchema; pub struct GetUserAttributesSchema;
@@ -29,8 +29,6 @@ use get_user_attributes_schema::ResponseData;
pub type Attribute = get_user_attributes_schema::GetUserAttributesSchemaSchemaUserSchemaAttributes; pub type Attribute = get_user_attributes_schema::GetUserAttributesSchemaSchemaUserSchemaAttributes;
convert_attribute_type!(get_user_attributes_schema::AttributeType);
#[derive(yew::Properties, Clone, PartialEq, Eq)] #[derive(yew::Properties, Clone, PartialEq, Eq)]
pub struct Props { pub struct Props {
pub hardcoded: bool, pub hardcoded: bool,
@@ -146,7 +144,7 @@ impl UserSchemaTable {
fn view_attribute(&self, ctx: &Context<Self>, attribute: &Attribute) -> Html { fn view_attribute(&self, ctx: &Context<Self>, attribute: &Attribute) -> Html {
let link = ctx.link(); let link = ctx.link();
let attribute_type = AttributeType::from(attribute.attribute_type.clone()); let attribute_type = attribute.attribute_type;
let checkmark = html! { 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"> <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> <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>
+31 -55
View File
@@ -1,66 +1,42 @@
use anyhow::Result; use derive_more::Display;
use std::{fmt::Display, str::FromStr}; use serde::{Deserialize, Serialize};
use strum::EnumString;
use validator::ValidationError; use validator::ValidationError;
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash, EnumString, Display)]
pub enum AttributeType { #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[strum(ascii_case_insensitive)]
pub(crate) enum AttributeType {
String, String,
Integer, Integer,
#[strum(serialize = "DATE_TIME", serialize = "DATETIME")]
DateTime, DateTime,
Jpeg, #[strum(serialize = "JPEG_PHOTO", serialize = "JPEGPHOTO")]
} JpegPhoto,
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 $crate::infra::schema::AttributeType {
fn from(value: $source_type) -> Self {
match value {
<$source_type>::STRING => $crate::infra::schema::AttributeType::String,
<$source_type>::INTEGER => $crate::infra::schema::AttributeType::Integer,
<$source_type>::DATE_TIME => $crate::infra::schema::AttributeType::DateTime,
<$source_type>::JPEG_PHOTO => $crate::infra::schema::AttributeType::Jpeg,
_ => panic!("Unknown attribute type"),
}
}
}
impl From<$crate::infra::schema::AttributeType> for $source_type {
fn from(value: $crate::infra::schema::AttributeType) -> Self {
match value {
$crate::infra::schema::AttributeType::String => <$source_type>::STRING,
$crate::infra::schema::AttributeType::Integer => <$source_type>::INTEGER,
$crate::infra::schema::AttributeType::DateTime => <$source_type>::DATE_TIME,
$crate::infra::schema::AttributeType::Jpeg => <$source_type>::JPEG_PHOTO,
}
}
}
};
} }
pub fn validate_attribute_type(attribute_type: &str) -> Result<(), ValidationError> { 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"))?; .map_err(|_| ValidationError::new("Invalid attribute type"))?;
Ok(()) 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);
}
}
+2 -2
View File
@@ -12,11 +12,11 @@ pub fn deserialize_attribute_value(
let parse_int = |value: &String| -> Result<i64> { let parse_int = |value: &String| -> Result<i64> {
value value
.parse::<i64>() .parse::<i64>()
.with_context(|| format!("Invalid integer value {}", value)) .with_context(|| format!("Invalid integer value {value}"))
}; };
let parse_date = |value: &String| -> Result<chrono::NaiveDateTime> { let parse_date = |value: &String| -> Result<chrono::NaiveDateTime> {
Ok(chrono::DateTime::parse_from_rfc3339(value) Ok(chrono::DateTime::parse_from_rfc3339(value)
.with_context(|| format!("Invalid date value {}", value))? .with_context(|| format!("Invalid date value {value}"))?
.naive_utc()) .naive_utc())
}; };
let parse_photo = |value: &String| -> Result<JpegPhoto> { let parse_photo = |value: &String| -> Result<JpegPhoto> {
+1 -1
View File
@@ -377,7 +377,7 @@ impl std::fmt::Debug for JpegPhoto {
encoded.push_str(" ..."); encoded.push_str(" ...");
}; };
f.debug_tuple("JpegPhoto") f.debug_tuple("JpegPhoto")
.field(&format!("b64[{}]", encoded)) .field(&format!("b64[{encoded}]"))
.finish() .finish()
} }
} }
+1 -1
View File
@@ -75,7 +75,7 @@ pub fn export_schema(output_file: Option<String>) -> anyhow::Result<()> {
use lldap_sql_backend_handler::SqlBackendHandler; use lldap_sql_backend_handler::SqlBackendHandler;
let output = schema::<SqlBackendHandler>().as_schema_language(); let output = schema::<SqlBackendHandler>().as_schema_language();
match output_file { match output_file {
None => println!("{}", output), None => println!("{output}"),
Some(path) => { Some(path) => {
use std::fs::File; use std::fs::File;
use std::io::prelude::*; use std::io::prelude::*;
+6 -8
View File
@@ -76,7 +76,7 @@ pub fn get_group_attribute(
.users .users
.iter() .iter()
.filter(|u| user_filter.as_ref().map(|f| *u == f).unwrap_or(true)) .filter(|u| user_filter.as_ref().map(|f| *u == f).unwrap_or(true))
.map(|u| format!("uid={},ou=people,{}", u, base_dn_str).into_bytes()) .map(|u| format!("uid={u},ou=people,{base_dn_str}").into_bytes())
.collect(), .collect(),
GroupFieldType::Uuid => vec![group.uuid.to_string().into_bytes()], GroupFieldType::Uuid => vec![group.uuid.to_string().into_bytes()],
GroupFieldType::Attribute(attr, _, _) => get_custom_attribute(&group.attributes, &attr)?, GroupFieldType::Attribute(attr, _, _) => get_custom_attribute(&group.attributes, &attr)?,
@@ -86,8 +86,7 @@ pub fn get_group_attribute(
"+" => return None, "+" => return None,
"*" => { "*" => {
panic!( panic!(
"Matched {}, * should have been expanded into attribute list and * removed", "Matched {attribute}, * should have been expanded into attribute list and * removed"
attribute
) )
} }
_ => { _ => {
@@ -211,7 +210,7 @@ fn convert_group_filter(
.map(GroupRequestFilter::Uuid) .map(GroupRequestFilter::Uuid)
.map_err(|e| LdapError { .map_err(|e| LdapError {
code: LdapResultCode::Other, code: LdapResultCode::Other,
message: format!("Invalid UUID: {:#}", e), message: format!("Invalid UUID: {e:#}"),
}), }),
GroupFieldType::Member => Ok(get_user_id_from_distinguished_name_or_plain_name( GroupFieldType::Member => Ok(get_user_id_from_distinguished_name_or_plain_name(
&value_lc, &value_lc,
@@ -290,15 +289,14 @@ fn convert_group_filter(
_ => Err(LdapError { _ => Err(LdapError {
code: LdapResultCode::UnwillingToPerform, code: LdapResultCode::UnwillingToPerform,
message: format!( message: format!(
"Unsupported group attribute for substring filter: \"{}\"", "Unsupported group attribute for substring filter: \"{field}\""
field
), ),
}), }),
} }
} }
_ => Err(LdapError { _ => Err(LdapError {
code: LdapResultCode::UnwillingToPerform, code: LdapResultCode::UnwillingToPerform,
message: format!("Unsupported group filter: {:?}", filter), message: format!("Unsupported group filter: {filter:?}"),
}), }),
} }
} }
@@ -318,7 +316,7 @@ pub async fn get_groups_list<Backend: GroupListerBackendHandler>(
.await .await
.map_err(|e| LdapError { .map_err(|e| LdapError {
code: LdapResultCode::Other, code: LdapResultCode::Other,
message: format!(r#"Error while listing groups "{}": {:#}"#, base, e), message: format!(r#"Error while listing groups "{base}": {e:#}"#),
}) })
} }
+4 -8
View File
@@ -100,8 +100,7 @@ pub fn get_user_attribute(
"+" => return None, "+" => return None,
"*" => { "*" => {
panic!( panic!(
"Matched {}, * should have been expanded into attribute list and * removed", "Matched {attribute}, * should have been expanded into attribute list and * removed"
attribute
) )
} }
_ => { _ => {
@@ -298,10 +297,7 @@ fn convert_user_filter(
| UserFieldType::PrimaryField(UserColumn::CreationDate) | UserFieldType::PrimaryField(UserColumn::CreationDate)
| UserFieldType::PrimaryField(UserColumn::Uuid) => Err(LdapError { | UserFieldType::PrimaryField(UserColumn::Uuid) => Err(LdapError {
code: LdapResultCode::UnwillingToPerform, code: LdapResultCode::UnwillingToPerform,
message: format!( message: format!("Unsupported user attribute for substring filter: {field:?}"),
"Unsupported user attribute for substring filter: {:?}",
field
),
}), }),
UserFieldType::NoMatch => Ok(UserRequestFilter::from(false)), UserFieldType::NoMatch => Ok(UserRequestFilter::from(false)),
UserFieldType::PrimaryField(UserColumn::Email) => Ok(UserRequestFilter::SubString( UserFieldType::PrimaryField(UserColumn::Email) => Ok(UserRequestFilter::SubString(
@@ -316,7 +312,7 @@ fn convert_user_filter(
} }
_ => Err(LdapError { _ => Err(LdapError {
code: LdapResultCode::UnwillingToPerform, code: LdapResultCode::UnwillingToPerform,
message: format!("Unsupported user filter: {:?}", filter), message: format!("Unsupported user filter: {filter:?}"),
}), }),
} }
} }
@@ -341,7 +337,7 @@ pub async fn get_user_list<Backend: UserListerBackendHandler>(
.await .await
.map_err(|e| LdapError { .map_err(|e| LdapError {
code: LdapResultCode::Other, code: LdapResultCode::Other,
message: format!(r#"Error while searching user "{}": {:#}"#, base, e), message: format!(r#"Error while searching user "{base}": {e:#}"#),
}) })
} }
+19 -10
View File
@@ -66,10 +66,9 @@ impl UserOrGroupName {
UserOrGroupName::InvalidSyntax(err) => return err, UserOrGroupName::InvalidSyntax(err) => return err,
UserOrGroupName::UnexpectedFormat UserOrGroupName::UnexpectedFormat
| UserOrGroupName::User(_) | UserOrGroupName::User(_)
| UserOrGroupName::Group(_) => format!( | UserOrGroupName::Group(_) => {
r#"Unexpected DN format. Got "{}", expected: {}"#, format!(r#"Unexpected DN format. Got "{input}", expected: {expected_format}"#)
input, expected_format }
),
}, },
} }
} }
@@ -105,7 +104,7 @@ pub fn get_user_id_from_distinguished_name(
) -> LdapResult<UserId> { ) -> LdapResult<UserId> {
match get_user_or_group_id_from_distinguished_name(dn, base_tree) { match get_user_or_group_id_from_distinguished_name(dn, base_tree) {
UserOrGroupName::User(user_id) => Ok(user_id), UserOrGroupName::User(user_id) => Ok(user_id),
err => Err(err.into_ldap_error(dn, format!(r#""uid=id,ou=people,{}""#, base_dn_str))), err => Err(err.into_ldap_error(dn, format!(r#""uid=id,ou=people,{base_dn_str}""#))),
} }
} }
@@ -116,7 +115,7 @@ pub fn get_group_id_from_distinguished_name(
) -> LdapResult<GroupName> { ) -> LdapResult<GroupName> {
match get_user_or_group_id_from_distinguished_name(dn, base_tree) { match get_user_or_group_id_from_distinguished_name(dn, base_tree) {
UserOrGroupName::Group(group_name) => Ok(group_name), UserOrGroupName::Group(group_name) => Ok(group_name),
err => Err(err.into_ldap_error(dn, format!(r#""uid=id,ou=groups,{}""#, base_dn_str))), err => Err(err.into_ldap_error(dn, format!(r#""uid=id,ou=groups,{base_dn_str}""#))),
} }
} }
@@ -343,7 +342,7 @@ pub struct ObjectClassList(Vec<LdapObjectClass>);
// See RFC4512 section 4.2.1 "objectClasses" // See RFC4512 section 4.2.1 "objectClasses"
impl ObjectClassList { impl ObjectClassList {
pub fn format_for_ldap_schema_description(&self) -> String { pub fn format_for_ldap_schema_description(&self) -> String {
join(self.0.iter().map(|c| format!("'{}'", c)), " ") join(self.0.iter().map(|c| format!("'{c}'")), " ")
} }
} }
@@ -437,13 +436,23 @@ impl LdapSchemaDescription {
// See RFC4512 section 4.2.2 "attributeTypes" // See RFC4512 section 4.2.2 "attributeTypes"
// Parameter 'index_offset' is an offset for the enumeration of this list of attributes, // Parameter 'index_offset' is an offset for the enumeration of this list of attributes,
// it has been preceeded by the list of hardcoded attributes. // it has been preceeded by the list of hardcoded attributes.
pub fn formatted_attribute_list(&self, index_offset: usize) -> Vec<Vec<u8>> { pub fn formatted_attribute_list(
&self,
index_offset: usize,
exclude_attributes: Vec<&str>,
) -> Vec<Vec<u8>> {
let mut formatted_list: Vec<Vec<u8>> = Vec::new(); let mut formatted_list: Vec<Vec<u8>> = Vec::new();
for (index, attribute) in self.all_attributes().attributes.into_iter().enumerate() { for (index, attribute) in self
.all_attributes()
.attributes
.into_iter()
.filter(|attr| !exclude_attributes.contains(&attr.name.as_str()))
.enumerate()
{
formatted_list.push( formatted_list.push(
format!( format!(
"( 2.{} NAME '{}' DESC 'LLDAP: {}' SUP {:?} )", "( 10.{} NAME '{}' DESC 'LLDAP: {}' SUP {:?} )",
(index + index_offset), (index + index_offset),
attribute.name, attribute.name,
if attribute.is_hardcoded { if attribute.is_hardcoded {
+5 -11
View File
@@ -33,10 +33,7 @@ pub(crate) async fn create_user_or_group(
} }
err => Err(err.into_ldap_error( err => Err(err.into_ldap_error(
&request.dn, &request.dn,
format!( format!(r#""uid=id,ou=people,{base_dn_str}" or "uid=id,ou=groups,{base_dn_str}""#),
r#""uid=id,ou=people,{}" or "uid=id,ou=groups,{}""#,
base_dn_str, base_dn_str
),
)), )),
} }
} }
@@ -73,10 +70,7 @@ async fn create_user(
std::str::from_utf8(val) std::str::from_utf8(val)
.map_err(|e| LdapError { .map_err(|e| LdapError {
code: LdapResultCode::ConstraintViolation, code: LdapResultCode::ConstraintViolation,
message: format!( message: format!("Attribute value is invalid UTF-8: {e:#?} (value {val:?})"),
"Attribute value is invalid UTF-8: {:#?} (value {:?})",
e, val
),
}) })
.map(str::to_owned) .map(str::to_owned)
} }
@@ -92,7 +86,7 @@ async fn create_user(
value: deserialize::deserialize_attribute_value(&[value], typ, false).map_err(|e| { value: deserialize::deserialize_attribute_value(&[value], typ, false).map_err(|e| {
LdapError { LdapError {
code: LdapResultCode::ConstraintViolation, code: LdapResultCode::ConstraintViolation,
message: format!("Invalid attribute value: {}", e), message: format!("Invalid attribute value: {e}"),
} }
})?, })?,
}) })
@@ -134,7 +128,7 @@ async fn create_user(
.await .await
.map_err(|e| LdapError { .map_err(|e| LdapError {
code: LdapResultCode::OperationsError, code: LdapResultCode::OperationsError,
message: format!("Could not create user: {:#?}", e), message: format!("Could not create user: {e:#?}"),
})?; })?;
Ok(vec![make_add_response( Ok(vec![make_add_response(
LdapResultCode::Success, LdapResultCode::Success,
@@ -156,7 +150,7 @@ async fn create_group(
.await .await
.map_err(|e| LdapError { .map_err(|e| LdapError {
code: LdapResultCode::OperationsError, code: LdapResultCode::OperationsError,
message: format!("Could not create group: {:#?}", e), message: format!("Could not create group: {e:#?}"),
})?; })?;
Ok(vec![make_add_response( Ok(vec![make_add_response(
LdapResultCode::Success, LdapResultCode::Success,
+5 -8
View File
@@ -30,10 +30,7 @@ pub(crate) async fn delete_user_or_group(
UserOrGroupName::Group(group_name) => delete_group(backend_handler, group_name).await, UserOrGroupName::Group(group_name) => delete_group(backend_handler, group_name).await,
err => Err(err.into_ldap_error( err => Err(err.into_ldap_error(
&request, &request,
format!( format!(r#""uid=id,ou=people,{base_dn_str}" or "uid=id,ou=groups,{base_dn_str}""#),
r#""uid=id,ou=people,{}" or "uid=id,ou=groups,{}""#,
base_dn_str, base_dn_str
),
)), )),
} }
} }
@@ -53,7 +50,7 @@ async fn delete_user(
}, },
e => LdapError { e => LdapError {
code: LdapResultCode::OperationsError, code: LdapResultCode::OperationsError,
message: format!("Error while finding user: {:?}", e), message: format!("Error while finding user: {e:?}"),
}, },
})?; })?;
backend_handler backend_handler
@@ -61,7 +58,7 @@ async fn delete_user(
.await .await
.map_err(|e| LdapError { .map_err(|e| LdapError {
code: LdapResultCode::OperationsError, code: LdapResultCode::OperationsError,
message: format!("Error while deleting user: {:?}", e), message: format!("Error while deleting user: {e:?}"),
})?; })?;
Ok(vec![make_del_response( Ok(vec![make_del_response(
LdapResultCode::Success, LdapResultCode::Success,
@@ -79,7 +76,7 @@ async fn delete_group(
.await .await
.map_err(|e| LdapError { .map_err(|e| LdapError {
code: LdapResultCode::OperationsError, code: LdapResultCode::OperationsError,
message: format!("Error while finding group: {:?}", e), message: format!("Error while finding group: {e:?}"),
})?; })?;
let group_id = groups let group_id = groups
.iter() .iter()
@@ -94,7 +91,7 @@ async fn delete_group(
.await .await
.map_err(|e| LdapError { .map_err(|e| LdapError {
code: LdapResultCode::OperationsError, code: LdapResultCode::OperationsError,
message: format!("Error while deleting group: {:?}", e), message: format!("Error while deleting group: {e:?}"),
})?; })?;
Ok(vec![make_del_response( Ok(vec![make_del_response(
LdapResultCode::Success, LdapResultCode::Success,
+4 -7
View File
@@ -100,10 +100,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
backend_handler, backend_handler,
ldap_info: LdapInfo { ldap_info: LdapInfo {
base_dn: parse_distinguished_name(&ldap_base_dn).unwrap_or_else(|_| { base_dn: parse_distinguished_name(&ldap_base_dn).unwrap_or_else(|_| {
panic!( panic!("Invalid value for ldap_base_dn in configuration: {ldap_base_dn}")
"Invalid value for ldap_base_dn in configuration: {}",
ldap_base_dn
)
}), }),
base_dn_str: ldap_base_dn, base_dn_str: ldap_base_dn,
ignored_user_attributes, ignored_user_attributes,
@@ -155,7 +152,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
let schema = backend_handler.get_schema().await.map_err(|e| LdapError { let schema = backend_handler.get_schema().await.map_err(|e| LdapError {
code: LdapResultCode::OperationsError, code: LdapResultCode::OperationsError,
message: format!("Unable to get schema: {:#}", e), message: format!("Unable to get schema: {e:#}"),
})?; })?;
return Ok(vec![ return Ok(vec![
make_ldap_subschema_entry(PublicSchema::from(schema)), make_ldap_subschema_entry(PublicSchema::from(schema)),
@@ -224,7 +221,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
} }
Err(e) => vec![make_extended_response( Err(e) => vec![make_extended_response(
LdapResultCode::ProtocolError, LdapResultCode::ProtocolError,
format!("Error while parsing password modify request: {:#?}", e), format!("Error while parsing password modify request: {e:#?}"),
)], )],
}, },
OID_WHOAMI => { OID_WHOAMI => {
@@ -343,7 +340,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
.unwrap_or_else(|e: LdapError| vec![make_search_error(e.code, e.message)]), .unwrap_or_else(|e: LdapError| vec![make_search_error(e.code, e.message)]),
op => vec![make_extended_response( op => vec![make_extended_response(
LdapResultCode::UnwillingToPerform, LdapResultCode::UnwillingToPerform,
format!("Unsupported operation: {:#?}", op), format!("Unsupported operation: {op:#?}"),
)], )],
}) })
} }
+5 -5
View File
@@ -47,7 +47,7 @@ async fn handle_modify_change(
.await .await
.map_err(|e| LdapError { .map_err(|e| LdapError {
code: LdapResultCode::Other, code: LdapResultCode::Other,
message: format!("Error while changing the password: {:#?}", e), message: format!("Error while changing the password: {e:#?}"),
})?; })?;
} else { } else {
return Err(LdapError { return Err(LdapError {
@@ -94,7 +94,7 @@ where
.await .await
.map_err(|e| LdapError { .map_err(|e| LdapError {
code: LdapResultCode::OperationsError, code: LdapResultCode::OperationsError,
message: format!("Internal error while requesting user's groups: {:#?}", e), message: format!("Internal error while requesting user's groups: {e:#?}"),
})? })?
.iter() .iter()
.any(|g| g.display_name == "lldap_admin".into()); .any(|g| g.display_name == "lldap_admin".into());
@@ -115,7 +115,7 @@ where
} }
Err(e) => Err(LdapError { Err(e) => Err(LdapError {
code: LdapResultCode::InvalidDNSyntax, code: LdapResultCode::InvalidDNSyntax,
message: format!("Invalid username: {}", e), message: format!("Invalid username: {e}"),
}), }),
} }
} }
@@ -166,7 +166,7 @@ mod tests {
fn make_password_modify_request(target_user: &str) -> LdapModifyRequest { fn make_password_modify_request(target_user: &str) -> LdapModifyRequest {
LdapModifyRequest { LdapModifyRequest {
dn: format!("uid={},ou=people,dc=example,dc=com", target_user), dn: format!("uid={target_user},ou=people,dc=example,dc=com"),
changes: vec![LdapModify { changes: vec![LdapModify {
operation: LdapModifyType::Replace, operation: LdapModifyType::Replace,
modification: ldap3_proto::LdapPartialAttribute { modification: ldap3_proto::LdapPartialAttribute {
@@ -284,7 +284,7 @@ mod tests {
let request = { let request = {
let target_user = "bob"; let target_user = "bob";
LdapModifyRequest { LdapModifyRequest {
dn: format!("uid={},ou=people,dc=example,dc=com", target_user), dn: format!("uid={target_user},ou=people,dc=example,dc=com"),
changes: vec![LdapModify { changes: vec![LdapModify {
operation: LdapModifyType::Replace, operation: LdapModifyType::Replace,
modification: ldap3_proto::LdapPartialAttribute { modification: ldap3_proto::LdapPartialAttribute {
+3 -4
View File
@@ -112,8 +112,7 @@ pub(crate) async fn do_password_modification<Handler: BackendHandler>(
.map_err(|e| LdapError { .map_err(|e| LdapError {
code: LdapResultCode::OperationsError, code: LdapResultCode::OperationsError,
message: format!( message: format!(
"Internal error while requesting user's groups: {:#?}", "Internal error while requesting user's groups: {e:#?}"
e
), ),
})? })?
.iter() .iter()
@@ -131,7 +130,7 @@ pub(crate) async fn do_password_modification<Handler: BackendHandler>(
{ {
Err(LdapError { Err(LdapError {
code: LdapResultCode::Other, code: LdapResultCode::Other,
message: format!("Error while changing the password: {:#?}", e), message: format!("Error while changing the password: {e:#?}"),
}) })
} else { } else {
Ok(vec![make_extended_response( Ok(vec![make_extended_response(
@@ -142,7 +141,7 @@ pub(crate) async fn do_password_modification<Handler: BackendHandler>(
} }
Err(e) => Err(LdapError { Err(e) => Err(LdapError {
code: LdapResultCode::InvalidDNSyntax, code: LdapResultCode::InvalidDNSyntax,
message: format!("Invalid username: {}", e), message: format!("Invalid username: {e}"),
}), }),
} }
} }
+101 -30
View File
@@ -202,25 +202,66 @@ pub fn make_ldap_subschema_entry(schema: PublicSchema) -> LdapOp {
LdapPartialAttribute { LdapPartialAttribute {
atype: "ldapSyntaxes".to_string(), atype: "ldapSyntaxes".to_string(),
vals: vec![ vals: vec![
b"( 1.3.6.1.1.16.1 DESC 'UUID' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.3 DESC 'Attribute Type Description' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.12 DESC 'Distinguished Name' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' )".to_vec(), b"( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.24 DESC 'Generalized Time' )".to_vec(), b"( 1.3.6.1.4.1.1466.115.121.1.24 DESC 'Generalized Time' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.27 DESC 'Integer' )".to_vec(), b"( 1.3.6.1.4.1.1466.115.121.1.27 DESC 'Integer' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.28 DESC 'JPEG' X-NOT-HUMAN-READABLE 'TRUE' )".to_vec(), b"( 1.3.6.1.4.1.1466.115.121.1.28 DESC 'JPEG' X-NOT-HUMAN-READABLE 'TRUE' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.34 DESC 'Name And Optional UID' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.37 DESC 'Object Class Description' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.38 DESC 'OID' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.54 DESC 'LDAP Syntax Description' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.58 DESC 'Substring Assertion' )".to_vec(),
],
},
LdapPartialAttribute {
atype: "matchingRules".to_string(),
vals: vec![
b"( 1.3.6.1.1.16.2 NAME 'UUIDMatch' SYNTAX 1.3.6.1.1.16.1 )".to_vec(),
b"( 1.3.6.1.1.16.3 NAME 'UUIDOrderingMatch' SYNTAX 1.3.6.1.1.16.1 )".to_vec(),
b"( 2.5.13.0 NAME 'objectIdentifierMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )".to_vec(),
b"( 2.5.13.1 NAME 'distinguishedNameMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )".to_vec(),
b"( 2.5.13.2 NAME 'caseIgnoreMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )".to_vec(),
b"( 2.5.13.4 NAME 'caseIgnoreSubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.58 )".to_vec(),
b"( 2.5.13.23 NAME 'uniqueMemberMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 )".to_vec(),
b"( 2.5.13.27 NAME 'generalizedTimeMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )".to_vec(),
b"( 2.5.13.28 NAME 'generalizedTimeOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )".to_vec(),
b"( 2.5.13.30 NAME 'objectIdentifierFirstComponentMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )".to_vec(),
], ],
}, },
LdapPartialAttribute { LdapPartialAttribute {
atype: "attributeTypes".to_string(), atype: "attributeTypes".to_string(),
vals: { vals: {
let hardcoded_attributes = [ let hardcoded_attributes = [
b"( 2.0 NAME 'String' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )".to_vec(), b"( 0.9.2342.19200300.100.1.1 NAME ( 'uid' 'userid' 'user_id' ) DESC 'RFC4519: user identifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} SINGLE-VALUE NO-USER-MODIFICATION )".to_vec(),
b"( 2.1 NAME 'Integer' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )".to_vec(), b"( 1.3.6.1.1.16.4 NAME ( 'entryUUID' 'uuid' ) DESC 'UUID of the entry' EQUALITY UUIDMatch ORDERING UUIDOrderingMatch SYNTAX 1.3.6.1.1.16.1 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )".to_vec(),
b"( 2.2 NAME 'JpegPhoto' SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 )".to_vec(), b"( 1.3.6.1.4.1.1466.101.120.16 NAME 'ldapSyntaxes' DESC 'RFC4512: LDAP syntaxes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.54 USAGE directoryOperation )".to_vec(),
b"( 2.3 NAME 'DateTime' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )".to_vec(), b"( 2.5.4.0 NAME 'objectClass' DESC 'RFC4512: object classes of the entity' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )".to_vec(),
b"( 2.5.4.3 NAME ( 'cn' 'commonName' 'display_name' ) DESC 'RFC4519: common name(s) for which the entity is known by' SUP name SINGLE-VALUE )".to_vec(),
b"( 2.5.4.4 NAME ( 'sn' 'surname' 'last_name' ) DESC 'RFC2256: last (family) name(s) for which the entity is known by' SUP name SINGLE-VALUE )".to_vec(),
b"( 2.5.4.11 NAME ( 'ou' 'organizationalUnitName' ) DESC 'RFC2256: organizational unit this object belongs to' SUP name )".to_vec(),
b"( 2.5.4.41 NAME 'name' DESC 'RFC4519: common supertype of name attributes' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )".to_vec(),
b"( 2.5.4.49 NAME 'distinguishedName' DESC 'RFC4519: common supertype of DN attributes' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )".to_vec(),
b"( 2.5.4.50 NAME ( 'uniqueMember' 'member' ) DESC 'RFC2256: unique member of a group' EQUALITY uniqueMemberMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 )".to_vec(),
b"( 2.5.18.1 NAME ( 'createTimestamp' 'creation_date' ) DESC 'RFC4512: time which object was created' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )".to_vec(),
b"( 2.5.18.2 NAME 'modifyTimestamp' DESC 'RFC4512: time which object was last modified' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )".to_vec(),
b"( 2.5.21.5 NAME 'attributeTypes' DESC 'RFC4512: attribute types' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.3 USAGE directoryOperation )".to_vec(),
b"( 2.5.21.6 NAME 'objectClasses' DESC 'RFC4512: object classes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.37 USAGE directoryOperation )".to_vec(),
b"( 2.5.21.9 NAME 'structuralObjectClass' DESC 'RFC4512: structural object class of entry' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )".to_vec(),
b"( 10.0 NAME 'String' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )".to_vec(),
b"( 10.1 NAME 'Integer' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )".to_vec(),
b"( 10.2 NAME 'JpegPhoto' SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 )".to_vec(),
b"( 10.3 NAME 'DateTime' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )".to_vec(),
]; ];
let num_hardcoded_attributes = hardcoded_attributes.len(); let num_hardcoded_attributes = hardcoded_attributes.len();
hardcoded_attributes.into_iter().chain( hardcoded_attributes.into_iter().chain(
ldap_schema_description ldap_schema_description
.formatted_attribute_list(num_hardcoded_attributes) .formatted_attribute_list(
num_hardcoded_attributes,
vec!["creation_date", "display_name", "last_name", "user_id", "uuid"]
)
).collect() ).collect()
} }
}, },
@@ -369,7 +410,7 @@ pub async fn do_search(
) -> LdapResult<Vec<LdapOp>> { ) -> LdapResult<Vec<LdapOp>> {
let schema = PublicSchema::from(backend_handler.get_schema().await.map_err(|e| LdapError { let schema = PublicSchema::from(backend_handler.get_schema().await.map_err(|e| LdapError {
code: LdapResultCode::OperationsError, code: LdapResultCode::OperationsError,
message: format!("Unable to get schema: {:#}", e), message: format!("Unable to get schema: {e:#}"),
})?); })?);
let search_results = do_search_internal(ldap_info, backend_handler, request, &schema).await?; let search_results = do_search_internal(ldap_info, backend_handler, request, &schema).await?;
let mut results = match search_results { let mut results = match search_results {
@@ -485,7 +526,7 @@ mod tests {
}; };
let attrs = &search_result_entry.attributes; let attrs = &search_result_entry.attributes;
assert_eq!(attrs.len(), 9); assert_eq!(attrs.len(), 10);
assert_eq!(search_result_entry.dn, "cn=Subschema".to_owned()); assert_eq!(search_result_entry.dn, "cn=Subschema".to_owned());
assert_eq!( assert_eq!(
@@ -530,47 +571,77 @@ mod tests {
LdapPartialAttribute { LdapPartialAttribute {
atype: "ldapSyntaxes".to_owned(), atype: "ldapSyntaxes".to_owned(),
vals: vec![ vals: vec![
b"( 1.3.6.1.1.16.1 DESC 'UUID' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.3 DESC 'Attribute Type Description' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.12 DESC 'Distinguished Name' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' )".to_vec(), b"( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.24 DESC 'Generalized Time' )".to_vec(), b"( 1.3.6.1.4.1.1466.115.121.1.24 DESC 'Generalized Time' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.27 DESC 'Integer' )".to_vec(), b"( 1.3.6.1.4.1.1466.115.121.1.27 DESC 'Integer' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.28 DESC 'JPEG' X-NOT-HUMAN-READABLE 'TRUE' )" b"( 1.3.6.1.4.1.1466.115.121.1.28 DESC 'JPEG' X-NOT-HUMAN-READABLE 'TRUE' )"
.to_vec() .to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.34 DESC 'Name And Optional UID' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.37 DESC 'Object Class Description' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.38 DESC 'OID' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.54 DESC 'LDAP Syntax Description' )".to_vec(),
b"( 1.3.6.1.4.1.1466.115.121.1.58 DESC 'Substring Assertion' )".to_vec(),
] ]
} }
); );
assert_eq!( assert_eq!(
attrs[6], attrs[6],
LdapPartialAttribute {
atype: "matchingRules".to_string(),
vals: vec![
b"( 1.3.6.1.1.16.2 NAME 'UUIDMatch' SYNTAX 1.3.6.1.1.16.1 )".to_vec(),
b"( 1.3.6.1.1.16.3 NAME 'UUIDOrderingMatch' SYNTAX 1.3.6.1.1.16.1 )".to_vec(),
b"( 2.5.13.0 NAME 'objectIdentifierMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )".to_vec(),
b"( 2.5.13.1 NAME 'distinguishedNameMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )".to_vec(),
b"( 2.5.13.2 NAME 'caseIgnoreMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )".to_vec(),
b"( 2.5.13.4 NAME 'caseIgnoreSubstringsMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.58 )".to_vec(),
b"( 2.5.13.23 NAME 'uniqueMemberMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 )".to_vec(),
b"( 2.5.13.27 NAME 'generalizedTimeMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )".to_vec(),
b"( 2.5.13.28 NAME 'generalizedTimeOrderingMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )".to_vec(),
b"( 2.5.13.30 NAME 'objectIdentifierFirstComponentMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )".to_vec(),
]
}
);
assert_eq!(
attrs[7],
LdapPartialAttribute { LdapPartialAttribute {
atype: "attributeTypes".to_owned(), atype: "attributeTypes".to_owned(),
vals: vec![ vals: vec![
b"( 2.0 NAME 'String' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )".to_vec(), b"( 0.9.2342.19200300.100.1.1 NAME ( 'uid' 'userid' 'user_id' ) DESC 'RFC4519: user identifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} SINGLE-VALUE NO-USER-MODIFICATION )".to_vec(),
b"( 2.1 NAME 'Integer' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )".to_vec(), b"( 1.3.6.1.1.16.4 NAME ( 'entryUUID' 'uuid' ) DESC 'UUID of the entry' EQUALITY UUIDMatch ORDERING UUIDOrderingMatch SYNTAX 1.3.6.1.1.16.1 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )".to_vec(),
b"( 2.2 NAME 'JpegPhoto' SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 )".to_vec(), b"( 1.3.6.1.4.1.1466.101.120.16 NAME 'ldapSyntaxes' DESC 'RFC4512: LDAP syntaxes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.54 USAGE directoryOperation )".to_vec(),
b"( 2.3 NAME 'DateTime' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )".to_vec(), b"( 2.5.4.0 NAME 'objectClass' DESC 'RFC4512: object classes of the entity' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )".to_vec(),
b"( 2.4 NAME 'avatar' DESC 'LLDAP: builtin attribute' SUP JpegPhoto )".to_vec(), b"( 2.5.4.3 NAME ( 'cn' 'commonName' 'display_name' ) DESC 'RFC4519: common name(s) for which the entity is known by' SUP name SINGLE-VALUE )".to_vec(),
b"( 2.5 NAME 'creation_date' DESC 'LLDAP: builtin attribute' SUP DateTime )" b"( 2.5.4.4 NAME ( 'sn' 'surname' 'last_name' ) DESC 'RFC2256: last (family) name(s) for which the entity is known by' SUP name SINGLE-VALUE )".to_vec(),
b"( 2.5.4.11 NAME ( 'ou' 'organizationalUnitName' ) DESC 'RFC2256: organizational unit this object belongs to' SUP name )".to_vec(),
b"( 2.5.4.41 NAME 'name' DESC 'RFC4519: common supertype of name attributes' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )".to_vec(),
b"( 2.5.4.49 NAME 'distinguishedName' DESC 'RFC4519: common supertype of DN attributes' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )".to_vec(),
b"( 2.5.4.50 NAME ( 'uniqueMember' 'member' ) DESC 'RFC2256: unique member of a group' EQUALITY uniqueMemberMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 )".to_vec(),
b"( 2.5.18.1 NAME ( 'createTimestamp' 'creation_date' ) DESC 'RFC4512: time which object was created' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )".to_vec(),
b"( 2.5.18.2 NAME 'modifyTimestamp' DESC 'RFC4512: time which object was last modified' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )".to_vec(),
b"( 2.5.21.5 NAME 'attributeTypes' DESC 'RFC4512: attribute types' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.3 USAGE directoryOperation )".to_vec(),
b"( 2.5.21.6 NAME 'objectClasses' DESC 'RFC4512: object classes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.37 USAGE directoryOperation )".to_vec(),
b"( 2.5.21.9 NAME 'structuralObjectClass' DESC 'RFC4512: structural object class of entry' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )".to_vec(),
b"( 10.0 NAME 'String' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )".to_vec(),
b"( 10.1 NAME 'Integer' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )".to_vec(),
b"( 10.2 NAME 'JpegPhoto' SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 )".to_vec(),
b"( 10.3 NAME 'DateTime' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )".to_vec(),
b"( 10.19 NAME 'avatar' DESC 'LLDAP: builtin attribute' SUP JpegPhoto )".to_vec(),
b"( 10.20 NAME 'first_name' DESC 'LLDAP: builtin attribute' SUP String )"
.to_vec(), .to_vec(),
b"( 2.6 NAME 'display_name' DESC 'LLDAP: builtin attribute' SUP String )" b"( 10.21 NAME 'mail' DESC 'LLDAP: builtin attribute' SUP String )".to_vec(),
b"( 10.22 NAME 'group_id' DESC 'LLDAP: builtin attribute' SUP Integer )"
.to_vec(), .to_vec(),
b"( 2.7 NAME 'first_name' DESC 'LLDAP: builtin attribute' SUP String )"
.to_vec(),
b"( 2.8 NAME 'last_name' DESC 'LLDAP: builtin attribute' SUP String )".to_vec(),
b"( 2.9 NAME 'mail' DESC 'LLDAP: builtin attribute' SUP String )".to_vec(),
b"( 2.10 NAME 'user_id' DESC 'LLDAP: builtin attribute' SUP String )".to_vec(),
b"( 2.11 NAME 'uuid' DESC 'LLDAP: builtin attribute' SUP String )".to_vec(),
b"( 2.12 NAME 'creation_date' DESC 'LLDAP: builtin attribute' SUP DateTime )"
.to_vec(),
b"( 2.13 NAME 'display_name' DESC 'LLDAP: builtin attribute' SUP String )"
.to_vec(),
b"( 2.14 NAME 'group_id' DESC 'LLDAP: builtin attribute' SUP Integer )"
.to_vec(),
b"( 2.15 NAME 'uuid' DESC 'LLDAP: builtin attribute' SUP String )".to_vec()
] ]
} }
); );
assert_eq!(attrs[7], assert_eq!(attrs[8],
LdapPartialAttribute { LdapPartialAttribute {
atype: "objectClasses".to_owned(), atype: "objectClasses".to_owned(),
vals: vec![ vals: vec![
@@ -581,7 +652,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
attrs[8], attrs[9],
LdapPartialAttribute { LdapPartialAttribute {
atype: "subschemaSubentry".to_owned(), atype: "subschemaSubentry".to_owned(),
vals: vec![b"cn=Subschema".to_vec()] vals: vec![b"cn=Subschema".to_vec()]
@@ -91,7 +91,7 @@ pub mod tests {
handler handler
.create_user(CreateUserRequest { .create_user(CreateUserRequest {
user_id: UserId::new(name), user_id: UserId::new(name),
email: format!("{}@bob.bob", name).into(), email: format!("{name}@bob.bob").into(),
display_name: Some("display ".to_string() + name), display_name: Some("display ".to_string() + name),
attributes: vec![ attributes: vec![
DomainAttribute { DomainAttribute {
@@ -164,7 +164,7 @@ impl GroupBackendHandler for SqlBackendHandler {
.one(&self.sql_pool) .one(&self.sql_pool)
.await? .await?
.map(Into::<GroupDetails>::into) .map(Into::<GroupDetails>::into)
.ok_or_else(|| DomainError::EntityNotFound(format!("{:?}", group_id)))?; .ok_or_else(|| DomainError::EntityNotFound(format!("{group_id:?}")))?;
let attributes = model::GroupAttributes::find() let attributes = model::GroupAttributes::find()
.filter(model::GroupAttributesColumn::GroupId.eq(group_details.group_id)) .filter(model::GroupAttributesColumn::GroupId.eq(group_details.group_id))
.order_by_asc(model::GroupAttributesColumn::AttributeName) .order_by_asc(model::GroupAttributesColumn::AttributeName)
@@ -252,8 +252,7 @@ impl GroupBackendHandler for SqlBackendHandler {
.await?; .await?;
if res.rows_affected == 0 { if res.rows_affected == 0 {
return Err(DomainError::EntityNotFound(format!( return Err(DomainError::EntityNotFound(format!(
"No such group: '{:?}'", "No such group: '{group_id:?}'"
group_id
))); )));
} }
Ok(()) Ok(())
@@ -306,8 +305,7 @@ impl SqlBackendHandler {
remove_group_attributes.push(attribute); remove_group_attributes.push(attribute);
} else { } else {
return Err(DomainError::InternalError(format!( return Err(DomainError::InternalError(format!(
"Group attribute name {} doesn't exist in the schema, yet was attempted to be removed from the database", "Group attribute name {attribute} doesn't exist in the schema, yet was attempted to be removed from the database"
attribute
))); )));
} }
} }
@@ -240,8 +240,7 @@ impl SqlBackendHandler {
remove_user_attributes.push(attribute); remove_user_attributes.push(attribute);
} else { } else {
return Err(DomainError::InternalError(format!( return Err(DomainError::InternalError(format!(
"User attribute name {} doesn't exist in the schema, yet was attempted to be removed from the database", "User attribute name {attribute} doesn't exist in the schema, yet was attempted to be removed from the database"
attribute
))); )));
} }
} }
@@ -384,8 +383,7 @@ impl UserBackendHandler for SqlBackendHandler {
.await?; .await?;
if res.rows_affected == 0 { if res.rows_affected == 0 {
return Err(DomainError::EntityNotFound(format!( return Err(DomainError::EntityNotFound(format!(
"No such user: '{}'", "No such user: '{user_id}'"
user_id
))); )));
} }
Ok(()) Ok(())
@@ -408,8 +406,7 @@ impl UserBackendHandler for SqlBackendHandler {
.await?; .await?;
if res.rows_affected == 0 { if res.rows_affected == 0 {
return Err(DomainError::EntityNotFound(format!( return Err(DomainError::EntityNotFound(format!(
"No such membership: '{}' -> {:?}", "No such membership: '{user_id}' -> {group_id:?}"
user_id, group_id
))); )));
} }
Ok(()) Ok(())
+2 -2
View File
@@ -55,8 +55,8 @@ Then you'll receive a JSON response with:
``` ```
{ {
"token": "eYbat...", "token": "Yh6RJV...",
"refreshToken": "3bCka...", "refreshToken": "dww5jwU...",
} }
``` ```
+1 -1
View File
@@ -6,7 +6,7 @@ configuration files:
- [Airsonic Advanced](airsonic-advanced.md) - [Airsonic Advanced](airsonic-advanced.md)
- [Apache Guacamole](apacheguacamole.md) - [Apache Guacamole](apacheguacamole.md)
- [Apereo CAS Server](apereo_cas_server.md) - [Apereo CAS Server](apereo_cas_server.md)
- [Authelia](authelia_config.yml) - [Authelia](authelia.md)
- [Authentik](authentik.md) - [Authentik](authentik.md)
- [Bookstack](bookstack.env.example) - [Bookstack](bookstack.env.example)
- [Calibre-Web](calibre_web.md) - [Calibre-Web](calibre_web.md)
+39
View File
@@ -0,0 +1,39 @@
# Configuration for Authelia
## Authelia LDAP configuration
For all configuration options see the [Authelia LDAP Documentation](https://www.authelia.com/configuration/first-factor/ldap/).
The following example configuration uses the LLDAP implementation template, the default values are documented in the
[Authelia LLDAP Integration Guide](https://www.authelia.com/integration/ldap/lldap/).
Users will be able to sign in using their username or email address.
```yaml
authentication_backend:
# How often authelia should check if there is a user update in LDAP
refresh_interval: '1m'
ldap:
implementation: 'lldap'
# Format is [<scheme>://]<hostname>[:<port>]
# ldap port for LLDAP is 3890 and ldaps 6360
address: 'ldap://lldap:3890'
# Set base dn that you configured in LLDAP
base_dn: 'DC=example,DC=com'
# The username and password of the bind user.
# "bind_user" should be the username you created for authentication with the "lldap_strict_readonly" permission. It is not recommended to use an actual admin account here.
# If you are configuring Authelia to change user passwords, then the account used here needs the "lldap_password_manager" permission instead.
user: 'UID=bind_user,OU=people,DC=example,DC=com'
# Password can also be set using a secret: https://www.authelia.com/configuration/methods/secrets/.
password: 'REPLACE_ME'
# Optional: Setup TLS if you've enabled LDAPS
# tls:
# skip_verify: false
# minimum_version: TLS1.2
# Disable the authelia password change and reset functionality if the "bind_user" does not have the "lldap_password_manager" permission.
password_reset:
disable: false
password_change:
disable: false
```
-35
View File
@@ -1,35 +0,0 @@
###############################################################
# Authelia configuration #
###############################################################
# This is just the LDAP part of the Authelia configuration!
# See Authelia docs at https://www.authelia.com/configuration/first-factor/ldap/ for more info
authentication_backend:
# Password reset through authelia works normally.
password_reset:
disable: false
# How often authelia should check if there is a user update in LDAP
refresh_interval: 1m
ldap:
implementation: lldap
# Pattern is ldap://HOSTNAME-OR-IP:PORT
# Normal ldap port is 389, standard in LLDAP is 3890
address: ldap://lldap:3890
# Set base dn that you configured in LLDAP
base_dn: dc=example,dc=com
# The username and password of the bind user.
# "bind_user" should be the username you created for authentication with the "lldap_strict_readonly" permission. It is not recommended to use an actual admin account here.
# If you are configuring Authelia to change user passwords, then the account used here needs the "lldap_password_manager" permission instead.
user: uid=bind_user,ou=people,dc=example,dc=com
additional_users_dn: ou=people
# Password can also be set using a secret: https://www.authelia.com/configuration/methods/secrets/
password: "REPLACE_ME"
# Optional: Setup TLS if you've enabled LDAPS
# tls:
# skip_verify: false
# minimum_version: TLS1.2
# Optional: To allow sign in with BOTH username and email, you can change the users_filter to this
# users_filter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person))"
+3
View File
@@ -36,6 +36,9 @@ The script can:
- `GROUP_SCHEMAS_DIR` (default value: `/bootstrap/group-schemas`) - directory where the group schema JSON configs could be found - `GROUP_SCHEMAS_DIR` (default value: `/bootstrap/group-schemas`) - directory where the group schema JSON configs could be found
- `LLDAP_SET_PASSWORD_PATH` - path to the `lldap_set_password` utility (default value: `/app/lldap_set_password`) - `LLDAP_SET_PASSWORD_PATH` - path to the `lldap_set_password` utility (default value: `/app/lldap_set_password`)
- `DO_CLEANUP` (default value: `false`) - delete groups and users not specified in config files, also remove users from groups that they do not belong to - `DO_CLEANUP` (default value: `false`) - delete groups and users not specified in config files, also remove users from groups that they do not belong to
- `DO_CLEANUP_USERS` (default value: `false`) - same as `DO_CLEANUP` but only for users.
- `DO_CLEANUP_GROUP_MEMBERSHIP` (default value: `false`) - same as `DO_CLEANUP` but only for group membership.
- `DO_CLEANUP_GROUPS` (default value: `false`) - same as `DO_CLEANUP` but only for groups.
## Config files ## Config files
+2 -2
View File
@@ -56,7 +56,7 @@ FILTER = memberOf=cn=seafile_user,ou=groups,dc=example,dc=com
## Configuring Seafile to use LLDAP with Authelia as an intermediary ## Configuring Seafile to use LLDAP with Authelia as an intermediary
Authelia is an open-source authentication and authorization server that can use LLDAP as a backend and act as an OpenID Connect Provider. We're going to assume that you have already set up Authelia and configured it with LLDAP. Authelia is an open-source authentication and authorization server that can use LLDAP as a backend and act as an OpenID Connect Provider. We're going to assume that you have already set up Authelia and configured it with LLDAP.
If not, you can find an example configuration [here](authelia_config.yml). If not, you can find an example configuration [here](authelia.md).
1. Add the following to Authelia's `configuration.yml`: 1. Add the following to Authelia's `configuration.yml`:
``` ```
@@ -117,4 +117,4 @@ OAUTH_ATTRIBUTE_MAP = {
} }
``` ```
Restart both your Authelia and Seafile server. You should see a "Single Sign-On" button on Seafile's login page. Clicking it should redirect you to Authelia. If you use the [example config for Authelia](authelia_config.yml), you should be able to log in using your LLDAP User ID. Restart both your Authelia and Seafile server. You should see a "Single Sign-On" button on Seafile's login page. Clicking it should redirect you to Authelia. If you use the [example config for Authelia](authelia.md), you should be able to log in using your LLDAP User ID.
+23 -12
View File
@@ -12,6 +12,9 @@ USER_CONFIGS_DIR="${USER_CONFIGS_DIR:-/bootstrap/user-configs}"
GROUP_CONFIGS_DIR="${GROUP_CONFIGS_DIR:-/bootstrap/group-configs}" GROUP_CONFIGS_DIR="${GROUP_CONFIGS_DIR:-/bootstrap/group-configs}"
LLDAP_SET_PASSWORD_PATH="${LLDAP_SET_PASSWORD_PATH:-/app/lldap_set_password}" LLDAP_SET_PASSWORD_PATH="${LLDAP_SET_PASSWORD_PATH:-/app/lldap_set_password}"
DO_CLEANUP="${DO_CLEANUP:-false}" DO_CLEANUP="${DO_CLEANUP:-false}"
DO_CLEANUP_USERS="${DO_CLEANUP_USERS:-$DO_CLEANUP}"
DO_CLEANUP_GROUP_MEMBERSHIP="${DO_CLEANUP_GROUP_MEMBERSHIP:-$DO_CLEANUP}"
DO_CLEANUP_GROUPS="${DO_CLEANUP_GROUPS:-$DO_CLEANUP}"
# Fallback to support legacy defaults # Fallback to support legacy defaults
if [[ ! -d $USER_CONFIGS_DIR ]] && [[ -d "/user-configs" ]]; then if [[ ! -d $USER_CONFIGS_DIR ]] && [[ -d "/user-configs" ]]; then
@@ -440,7 +443,7 @@ extract_custom_group_attributes() {
} }
extract_custom_user_attributes() { extract_custom_user_attributes() {
extract_custom_attributes "$1" '"id","email","password","displayName","firstName","lastName","groups","avatar_file","avatar_url","gravatar_avatar","weserv_avatar"' extract_custom_attributes "$1" '"id","email","password","password_file","displayName","firstName","lastName","groups","avatar_file","avatar_url","gravatar_avatar","weserv_avatar"'
} }
extract_custom_attributes() { extract_custom_attributes() {
@@ -596,12 +599,18 @@ main() {
check_install_dependencies check_install_dependencies
check_required_env_vars check_required_env_vars
local user_config_files=("${USER_CONFIGS_DIR}"/*.json) local user_config_files=()
local group_config_files=("${GROUP_CONFIGS_DIR}"/*.json) local group_config_files=()
local user_schema_files=() local user_schema_files=()
local group_schema_files=() local group_schema_files=()
local file='' local file=''
[[ -d "$USER_CONFIGS_DIR" ]] && for file in "${USER_CONFIGS_DIR}"/*.json; do
user_config_files+=("$file")
done
[[ -d "$GROUP_CONFIGS_DIR" ]] && for file in "${GROUP_CONFIGS_DIR}"/*.json; do
group_config_files+=("$file")
done
[[ -d "$USER_SCHEMAS_DIR" ]] && for file in "${USER_SCHEMAS_DIR}"/*.json; do [[ -d "$USER_SCHEMAS_DIR" ]] && for file in "${USER_SCHEMAS_DIR}"/*.json; do
user_schema_files+=("$file") user_schema_files+=("$file")
done done
@@ -647,7 +656,7 @@ main() {
printf -- '\n--- groups ---\n' printf -- '\n--- groups ---\n'
local group_config='' local group_config=''
while read -r group_config; do [[ ${#group_config_files[@]} -gt 0 ]] && while read -r group_config; do
local group_name='' local group_name=''
group_name="$(printf '%s' "$group_config" | jq --raw-output '.name')" group_name="$(printf '%s' "$group_config" | jq --raw-output '.name')"
create_group "$group_name" create_group "$group_name"
@@ -675,7 +684,7 @@ main() {
else else
local group_name='' local group_name=''
while read -r group_name; do while read -r group_name; do
if [[ "$DO_CLEANUP" == 'true' ]]; then if [[ "$DO_CLEANUP_GROUPS" == 'true' ]]; then
delete_group "$group_name" delete_group "$group_name"
else else
printf '[WARNING] Group "%s" is not declared in config files\n' "$group_name" printf '[WARNING] Group "%s" is not declared in config files\n' "$group_name"
@@ -690,9 +699,9 @@ main() {
TMP_AVATAR_DIR="$(mktemp -d)" TMP_AVATAR_DIR="$(mktemp -d)"
local user_config='' local user_config=''
while read -r user_config; do [[ ${#user_config_files[@]} -gt 0 ]] && while read -r user_config; do
local field='' id='' email='' displayName='' firstName='' lastName='' avatar_file='' avatar_url='' gravatar_avatar='' weserv_avatar='' password='' local field='' id='' email='' displayName='' firstName='' lastName='' avatar_file='' avatar_url='' gravatar_avatar='' weserv_avatar='' password='' password_file=''
for field in 'id' 'email' 'displayName' 'firstName' 'lastName' 'avatar_file' 'avatar_url' 'gravatar_avatar' 'weserv_avatar' 'password'; do for field in 'id' 'email' 'displayName' 'firstName' 'lastName' 'avatar_file' 'avatar_url' 'gravatar_avatar' 'weserv_avatar' 'password' 'password_file'; do
declare "$field"="$(printf '%s' "$user_config" | jq --raw-output --arg field "$field" '.[$field]')" declare "$field"="$(printf '%s' "$user_config" | jq --raw-output --arg field "$field" '.[$field]')"
done done
printf -- '\n--- %s ---\n' "$id" printf -- '\n--- %s ---\n' "$id"
@@ -700,8 +709,10 @@ main() {
create_update_user "$id" "$email" "$displayName" "$firstName" "$lastName" "$avatar_file" "$avatar_url" "$gravatar_avatar" "$weserv_avatar" create_update_user "$id" "$email" "$displayName" "$firstName" "$lastName" "$avatar_file" "$avatar_url" "$gravatar_avatar" "$weserv_avatar"
redundant_users="$(printf '%s' "$redundant_users" | jq --compact-output --arg id "$id" '. - [$id]')" redundant_users="$(printf '%s' "$redundant_users" | jq --compact-output --arg id "$id" '. - [$id]')"
if [[ "$password" != 'null' ]] && [[ "$password" != '""' ]]; then if [[ "$password_file" != 'null' ]] && [[ "$password_file" != '""' ]]; then
"$LLDAP_SET_PASSWORD_PATH" --base-url "$LLDAP_URL" --token "$TOKEN" --username "$id" --password "$password" LLDAP_USER_PASSWORD="$(cat $password_file)" "$LLDAP_SET_PASSWORD_PATH" --base-url "$LLDAP_URL" --token "$TOKEN" --username "$id"
elif [[ "$password" != 'null' ]] && [[ "$password" != '""' ]]; then
LLDAP_USER_PASSWORD="$password" "$LLDAP_SET_PASSWORD_PATH" --base-url "$LLDAP_URL" --token "$TOKEN" --username "$id"
fi fi
# Process custom attributes # Process custom attributes
@@ -728,7 +739,7 @@ main() {
local user_group_name='' local user_group_name=''
while read -r user_group_name; do while read -r user_group_name; do
if [[ "$DO_CLEANUP" == 'true' ]]; then if [[ "$DO_CLEANUP_GROUP_MEMBERSHIP" == 'true' ]]; then
remove_user_from_group "$id" "$user_group_name" remove_user_from_group "$id" "$user_group_name"
else else
printf '[WARNING] User "%s" is not declared as member of the "%s" group in the config files\n' "$id" "$user_group_name" printf '[WARNING] User "%s" is not declared as member of the "%s" group in the config files\n' "$id" "$user_group_name"
@@ -745,7 +756,7 @@ main() {
else else
local id='' local id=''
while read -r id; do while read -r id; do
if [[ "$DO_CLEANUP" == 'true' ]]; then if [[ "$DO_CLEANUP_USERS" == 'true' ]]; then
delete_user "$id" delete_user "$id"
else else
printf '[WARNING] User "%s" is not declared in config files\n' "$id" printf '[WARNING] User "%s" is not declared in config files\n' "$id"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "lldap" name = "lldap"
version = "0.6.2-alpha" version = "0.6.2"
description = "Super-simple and lightweight LDAP server" description = "Super-simple and lightweight LDAP server"
categories = ["authentication", "command-line-utilities"] categories = ["authentication", "command-line-utilities"]
edition.workspace = true edition.workspace = true
+5 -6
View File
@@ -199,8 +199,7 @@ where
warn!("Error sending email: {:#?}", e); warn!("Error sending email: {:#?}", e);
info!("Reset token: {}", token); info!("Reset token: {}", token);
return Err(TcpError::InternalServerError(format!( return Err(TcpError::InternalServerError(format!(
"Could not send email: {}", "Could not send email: {e}"
e
))); )));
} }
Ok(()) Ok(())
@@ -254,7 +253,7 @@ where
Cookie::build("token", token.as_str()) Cookie::build("token", token.as_str())
.max_age(5.minutes()) .max_age(5.minutes())
// Cookie is only valid to reset the password. // Cookie is only valid to reset the password.
.path(format!("{}auth", path)) .path(format!("{path}auth"))
.http_only(true) .http_only(true)
.same_site(SameSite::Strict) .same_site(SameSite::Strict)
.finish(), .finish(),
@@ -310,7 +309,7 @@ where
.cookie( .cookie(
Cookie::build("refresh_token", "") Cookie::build("refresh_token", "")
.max_age(0.days()) .max_age(0.days())
.path(format!("{}auth", path)) .path(format!("{path}auth"))
.http_only(true) .http_only(true)
.same_site(SameSite::Strict) .same_site(SameSite::Strict)
.finish(), .finish(),
@@ -381,7 +380,7 @@ where
.cookie( .cookie(
Cookie::build("refresh_token", refresh_token_plus_name.clone()) Cookie::build("refresh_token", refresh_token_plus_name.clone())
.max_age(max_age.num_days().days()) .max_age(max_age.num_days().days())
.path(format!("{}auth", path)) .path(format!("{path}auth"))
.http_only(true) .http_only(true)
.same_site(SameSite::Strict) .same_site(SameSite::Strict)
.finish(), .finish(),
@@ -475,7 +474,7 @@ where
inner_payload, inner_payload,
) )
.await .await
.map_err(|e| TcpError::BadRequest(format!("{:#?}", e)))? .map_err(|e| TcpError::BadRequest(format!("{e:#?}")))?
.into_inner(); .into_inner();
let user_id = &registration_start_request.username; let user_id = &registration_start_request.username;
let user_is_admin = data let user_is_admin = data
+8 -10
View File
@@ -299,14 +299,14 @@ impl PrivateKeyLocationOrFigment {
source: Some(figment::Source::Code(_)), source: Some(figment::Source::Code(_)),
.. ..
}) => PrivateKeyLocation::Default, }) => PrivateKeyLocation::Default,
other => panic!("Unexpected config location: {:?}", other), other => panic!("Unexpected config location: {other:?}"),
} }
} }
PrivateKeyLocationOrFigment::PrivateKeyLocation(PrivateKeyLocation::KeyFile( PrivateKeyLocationOrFigment::PrivateKeyLocation(PrivateKeyLocation::KeyFile(
config_location, config_location,
_, _,
)) => { )) => {
panic!("Unexpected location: {:?}", config_location) panic!("Unexpected location: {config_location:?}")
} }
PrivateKeyLocationOrFigment::PrivateKeyLocation(location) => location.clone(), PrivateKeyLocationOrFigment::PrivateKeyLocation(location) => location.clone(),
} }
@@ -334,11 +334,11 @@ impl PrivateKeyLocationOrFigment {
source: Some(figment::Source::Code(_)), source: Some(figment::Source::Code(_)),
.. ..
}) => PrivateKeyLocation::Default, }) => PrivateKeyLocation::Default,
other => panic!("Unexpected config location: {:?}", other), other => panic!("Unexpected config location: {other:?}"),
} }
} }
PrivateKeyLocationOrFigment::PrivateKeyLocation(PrivateKeyLocation::KeySeed(file)) => { PrivateKeyLocationOrFigment::PrivateKeyLocation(PrivateKeyLocation::KeySeed(file)) => {
panic!("Unexpected location: {:?}", file) panic!("Unexpected location: {file:?}")
} }
PrivateKeyLocationOrFigment::PrivateKeyLocation(location) => location.clone(), PrivateKeyLocationOrFigment::PrivateKeyLocation(location) => location.clone(),
} }
@@ -373,19 +373,17 @@ fn get_server_setup<L: Into<PrivateKeyLocationOrFigment>>(
private_key_location: private_key_location.for_key_seed(), private_key_location: private_key_location.for_key_seed(),
}) })
} else if path.exists() { } else if path.exists() {
let bytes = read(file_path).context(format!("Could not read key file `{}`", file_path))?; let bytes = read(file_path).context(format!("Could not read key file `{file_path}`"))?;
Ok(ServerSetupConfig { Ok(ServerSetupConfig {
server_setup: ServerSetup::deserialize(&bytes).context(format!( server_setup: ServerSetup::deserialize(&bytes).context(format!(
"while parsing the contents of the `{}` file", "while parsing the contents of the `{file_path}` file"
file_path
))?, ))?,
private_key_location: private_key_location.for_key_file(file_path), private_key_location: private_key_location.for_key_file(file_path),
}) })
} else { } else {
let server_setup = generate_random_private_key(); let server_setup = generate_random_private_key();
write_to_readonly_file(path, &server_setup.serialize()).context(format!( write_to_readonly_file(path, &server_setup.serialize()).context(format!(
"Could not write the generated server setup to file `{}`", "Could not write the generated server setup to file `{file_path}`",
file_path,
))?; ))?;
Ok(ServerSetupConfig { Ok(ServerSetupConfig {
server_setup, server_setup,
@@ -596,7 +594,7 @@ where
.iter() .iter()
.filter(|k| !expected_keys.contains(k.as_str())) .filter(|k| !expected_keys.contains(k.as_str()))
.for_each(|k| { .for_each(|k| {
eprintln!("WARNING: Unknown environment variable: LLDAP_{}", k); eprintln!("WARNING: Unknown environment variable: LLDAP_{k}");
}); });
} }
config.server_setup = Some(get_server_setup( config.server_setup = Some(get_server_setup(
+2 -2
View File
@@ -29,7 +29,7 @@ impl std::fmt::Debug for DatabaseUrl {
let mut url = self.0.clone(); let mut url = self.0.clone();
// It can fail for URLs that cannot have a password, like "mailto:bob@example". // It can fail for URLs that cannot have a password, like "mailto:bob@example".
let _ = url.set_password(Some("***PASSWORD***")); let _ = url.set_password(Some("***PASSWORD***"));
f.write_fmt(format_args!(r#""{}""#, url)) f.write_fmt(format_args!(r#""{url}""#))
} else { } else {
f.write_fmt(format_args!(r#""{}""#, self.0)) f.write_fmt(format_args!(r#""{}""#, self.0))
} }
@@ -44,7 +44,7 @@ mod tests {
fn test_database_url_debug() { fn test_database_url_debug() {
let url = DatabaseUrl::from("postgres://user:pass@localhost:5432/dbname"); let url = DatabaseUrl::from("postgres://user:pass@localhost:5432/dbname");
assert_eq!( assert_eq!(
format!("{:?}", url), format!("{url:?}"),
r#""postgres://user:***PASSWORD***@localhost:5432/dbname""# r#""postgres://user:***PASSWORD***@localhost:5432/dbname""#
); );
assert_eq!( assert_eq!(
+2 -2
View File
@@ -71,7 +71,7 @@ where
#[instrument(level = "info", err)] #[instrument(level = "info", err)]
pub async fn check_ldap(port: u16) -> Result<()> { pub async fn check_ldap(port: u16) -> Result<()> {
check_ldap_endpoint(TcpStream::connect(format!("localhost:{}", port)).await?).await check_ldap_endpoint(TcpStream::connect(format!("localhost:{port}")).await?).await
} }
fn get_root_certificates() -> rustls::RootCertStore { fn get_root_certificates() -> rustls::RootCertStore {
@@ -152,7 +152,7 @@ pub async fn check_ldaps(ldaps_options: &LdapsOptions) -> Result<()> {
#[instrument(level = "info", err)] #[instrument(level = "info", err)]
pub async fn check_api(port: u16) -> Result<()> { pub async fn check_api(port: u16) -> Result<()> {
reqwest::get(format!("http://localhost:{}/health", port)) reqwest::get(format!("http://localhost:{port}/health"))
.await? .await?
.error_for_status()?; .error_for_status()?;
info!("Success"); info!("Success");
+1 -4
View File
@@ -132,10 +132,7 @@ fn read_private_key(key_file: &str) -> Result<PrivateKey> {
.and_then(|keys| keys.into_iter().next().ok_or_else(|| anyhow!("No EC key"))) .and_then(|keys| keys.into_iter().next().ok_or_else(|| anyhow!("No EC key")))
}) })
.with_context(|| { .with_context(|| {
format!( format!("Cannot read either PKCS1, PKCS8 or EC private key from {key_file}")
"Cannot read either PKCS1, PKCS8 or EC private key from {}",
key_file
)
}) })
.map(rustls::PrivateKey) .map(rustls::PrivateKey)
} }
+3 -4
View File
@@ -93,15 +93,14 @@ pub async fn send_password_reset_email(
.unwrap() .unwrap()
.extend(["reset-password", "step2", token]); .extend(["reset-password", "step2", token]);
let body = format!( let body = format!(
"Hello {}, "Hello {username},
This email has been sent to you in order to validate your identity. This email has been sent to you in order to validate your identity.
If you did not initiate the process your credentials might have been If you did not initiate the process your credentials might have been
compromised. You should reset your password and contact an administrator. compromised. You should reset your password and contact an administrator.
To reset your password please visit the following URL: {} To reset your password please visit the following URL: {reset_url}
Please contact an administrator if you did not initiate the process.", Please contact an administrator if you did not initiate the process."
username, reset_url
); );
let res = send_email( let res = send_email(
to, to,
+2 -3
View File
@@ -55,8 +55,7 @@ async fn create_admin_user(handler: &SqlBackendHandler, config: &Configuration)
.len(); .len();
assert!( assert!(
pass_length >= 8, pass_length >= 8,
"Minimum password length is 8 characters, got {} characters", "Minimum password length is 8 characters, got {pass_length} characters"
pass_length
); );
handler handler
.create_user(CreateUserRequest { .create_user(CreateUserRequest {
@@ -97,7 +96,7 @@ async fn ensure_group_exists(handler: &SqlBackendHandler, group_name: &str) -> R
..Default::default() ..Default::default()
}) })
.await .await
.context(format!("while creating {} group", group_name))?; .context(format!("while creating {group_name} group"))?;
} }
Ok(()) Ok(())
} }
+1 -2
View File
@@ -169,8 +169,7 @@ impl TcpBackendHandler for SqlBackendHandler {
.await?; .await?;
if result.rows_affected == 0 { if result.rows_affected == 0 {
return Err(DomainError::EntityNotFound(format!( return Err(DomainError::EntityNotFound(format!(
"No such password reset token: '{}'", "No such password reset token: '{token}'"
token
))); )));
} }
Ok(()) Ok(())
+2 -2
View File
@@ -14,13 +14,13 @@ pub fn database_url() -> String {
pub fn ldap_url() -> String { pub fn ldap_url() -> String {
let port = var("LLDAP_LDAP_PORT").ok(); let port = var("LLDAP_LDAP_PORT").ok();
let port = port.unwrap_or("3890".to_string()); let port = port.unwrap_or("3890".to_string());
format!("ldap://localhost:{}", port) format!("ldap://localhost:{port}")
} }
pub fn http_url() -> String { pub fn http_url() -> String {
let port = var("LLDAP_HTTP_PORT").ok(); let port = var("LLDAP_HTTP_PORT").ok();
let port = port.unwrap_or("17170".to_string()); let port = port.unwrap_or("17170".to_string());
format!("http://localhost:{}", port) format!("http://localhost:{port}")
} }
pub fn admin_dn() -> String { pub fn admin_dn() -> String {
+7 -10
View File
@@ -102,7 +102,7 @@ impl LLDAPFixture {
create_user::Variables { create_user::Variables {
user: create_user::CreateUserInput { user: create_user::CreateUserInput {
id: user.clone(), id: user.clone(),
email: Some(format!("{}@lldap.test", user)), email: Some(format!("{user}@lldap.test")),
avatar: None, avatar: None,
display_name: None, display_name: None,
first_name: None, first_name: None,
@@ -181,11 +181,11 @@ impl Drop for LLDAPFixture {
Signal::SIGTERM, Signal::SIGTERM,
); );
if let Err(err) = result { if let Err(err) = result {
println!("Failed to send kill signal: {:?}", err); println!("Failed to send kill signal: {err:?}");
let _ = self let _ = self
.child .child
.kill() .kill()
.map_err(|err| println!("Failed to kill LLDAP: {:?}", err)); .map_err(|err| println!("Failed to kill LLDAP: {err:?}"));
return; return;
} }
@@ -193,10 +193,7 @@ impl Drop for LLDAPFixture {
let status = self.child.try_wait(); let status = self.child.try_wait();
match status { match status {
Err(e) => { Err(e) => {
println!( println!("Failed to get status while waiting for graceful exit: {e}");
"Failed to get status while waiting for graceful exit: {}",
e
);
break; break;
} }
Ok(None) => { Ok(None) => {
@@ -204,7 +201,7 @@ impl Drop for LLDAPFixture {
} }
Ok(Some(status)) => { Ok(Some(status)) => {
if !status.success() { if !status.success() {
println!("LLDAP exited with status {}", status) println!("LLDAP exited with status {status}")
} }
return; return;
} }
@@ -215,7 +212,7 @@ impl Drop for LLDAPFixture {
let _ = self let _ = self
.child .child
.kill() .kill()
.map_err(|err| println!("Failed to kill LLDAP: {:?}", err)); .map_err(|err| println!("Failed to kill LLDAP: {err:?}"));
} }
} }
@@ -223,7 +220,7 @@ pub fn new_id(prefix: Option<&str>) -> String {
let id = Uuid::new_v4(); let id = Uuid::new_v4();
let id = format!("{}-lldap-test", id.simple()); let id = format!("{}-lldap-test", id.simple());
match prefix { match prefix {
Some(prefix) => format!("{}{}", prefix, id), Some(prefix) => format!("{prefix}{id}"),
None => id, None => id,
} }
} }
+1 -1
View File
@@ -103,7 +103,7 @@ where
}) })
}; };
let url = env::http_url() + "/api/graphql"; let url = env::http_url() + "/api/graphql";
let auth_header = format!("Bearer {}", token); let auth_header = format!("Bearer {token}");
client client
.post(url) .post(url)
.header(reqwest::header::AUTHORIZATION, auth_header) .header(reqwest::header::AUTHORIZATION, auth_header)
+2 -2
View File
@@ -31,13 +31,13 @@ fn gitea() {
ldap.simple_bind(bind_dn.as_str(), env::admin_password().as_str()) ldap.simple_bind(bind_dn.as_str(), env::admin_password().as_str())
.expect("failed to bind to ldap"); .expect("failed to bind to ldap");
let user_base = format!("ou=people,{}", base_dn); let user_base = format!("ou=people,{base_dn}");
let attrs = vec!["uid", "givenName", "sn", "mail", "jpegPhoto"]; let attrs = vec!["uid", "givenName", "sn", "mail", "jpegPhoto"];
let results = ldap let results = ldap
.search( .search(
user_base.as_str(), user_base.as_str(),
Scope::Subtree, Scope::Subtree,
format!("(memberof=cn={},ou=groups,{})", gitea_user_group, base_dn).as_str(), format!("(memberof=cn={gitea_user_group},ou=groups,{base_dn})").as_str(),
attrs, attrs,
) )
.expect("failed to find gitea users") .expect("failed to find gitea users")
+2 -2
View File
@@ -86,7 +86,7 @@ fn admin_search() {
ldap.search( ldap.search(
env::base_dn().as_str(), env::base_dn().as_str(),
Scope::Subtree, Scope::Subtree,
format!("(&(objectclass=person)(uid={}))", admin_name).as_str(), format!("(&(objectclass=person)(uid={admin_name}))").as_str(),
attrs, attrs,
) )
.expect("failed to find admin"), .expect("failed to find admin"),
@@ -97,7 +97,7 @@ fn admin_search() {
found_users found_users
.get(&admin_name) .get(&admin_name)
.unwrap() .unwrap()
.contains(format!("cn={},ou=groups,{}", admin_group_name, base_dn).as_str()) .contains(format!("cn={admin_group_name},ou=groups,{base_dn}").as_str())
); );
ldap.unbind().expect("failed to unbind ldap connection"); ldap.unbind().expect("failed to unbind ldap connection");
} }
+12 -4
View File
@@ -1,3 +1,5 @@
use std::env;
use anyhow::{Context, Result, bail, ensure}; use anyhow::{Context, Result, bail, ensure};
use clap::Parser; use clap::Parser;
use lldap_auth::{opaque, registration}; use lldap_auth::{opaque, registration};
@@ -27,9 +29,9 @@ pub struct CliOpts {
#[clap(short, long)] #[clap(short, long)]
pub username: String, pub username: String,
/// New password for the user. /// New password for the user. Can also be passed as the environment variable LLDAP_USER_PASSWORD.
#[clap(short, long)] #[clap(short, long)]
pub password: String, pub password: Option<String>,
/// Bypass password requirements such as minimum length. Unsafe. /// Bypass password requirements such as minimum length. Unsafe.
#[clap(long)] #[clap(long)]
@@ -100,8 +102,14 @@ pub fn register_finish(
fn main() -> Result<()> { fn main() -> Result<()> {
let opts = CliOpts::parse(); let opts = CliOpts::parse();
let password = match opts.password {
Some(v) => v,
None => env::var("LLDAP_USER_PASSWORD").unwrap_or_default(),
};
ensure!( ensure!(
opts.bypass_password_policy || opts.password.len() >= 8, opts.bypass_password_policy || password.len() >= 8,
"New password is too short, expected at least 8 characters" "New password is too short, expected at least 8 characters"
); );
ensure!( ensure!(
@@ -118,7 +126,7 @@ fn main() -> Result<()> {
let mut rng = rand::rngs::OsRng; let mut rng = rand::rngs::OsRng;
let registration_start_request = let registration_start_request =
opaque::client::registration::start_registration(opts.password.as_bytes(), &mut rng) opaque::client::registration::start_registration(password.as_bytes(), &mut rng)
.context("Could not initiate password change")?; .context("Could not initiate password change")?;
let start_request = registration::ClientRegistrationStartRequest { let start_request = registration::ClientRegistrationStartRequest {
username: opts.username.clone().into(), username: opts.username.clone().into(),