Compare commits

...

23 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
02572bdf45 Switch to action-rs/toolchain for Rust installation in CI
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-08-21 00:01:39 +00:00
copilot-swe-agent[bot]
30cb4ceae0 Use variable for Rust version in CI workflow
Replace hardcoded "1.85.0" references with RUST_VERSION environment variable for better maintainability. Now the Rust version only needs to be updated in one place.

Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-08-20 23:58:18 +00:00
copilot-swe-agent[bot]
2e2a67c13b Pin Rust version to 1.85.0 in CI workflows and Docker images
Co-authored-by: nitnelave <796633+nitnelave@users.noreply.github.com>
2025-08-20 23:52:12 +00:00
copilot-swe-agent[bot]
4d8fccb502 Initial plan 2025-08-20 23:43:32 +00:00
copilot-swe-agent[bot]
cd2694d7dc Add comprehensive GitHub Copilot instructions for LLDAP repository
Add copilot-setup-steps.yml for GitHub Copilot agent environment setup
2025-08-21 01:22:31 +02:00
Valentin Tolmer
5e83ed8eb0 release: v0.6.2 2025-08-18 00:06:44 +02:00
Kirill Zhuravlev
c69957690e docs: avoid bad-sounding words in secrets example 2025-08-17 23:10:45 +02:00
Linus Astel
7ef2af8beb devcontainer: Bump Rust version 2025-08-14 22:38:45 +02:00
Toby
5c9897b156 ldap: Add missing subschema entries 2025-08-14 16:04:28 +02:00
ibizaman
0b720aa082 bootstrap: fine grained cleanup 2025-08-13 09:36:21 +02:00
dependabot[bot]
3e7277e77d build(deps): bump actions/checkout from 4.2.2 to 5.0.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.2 to 5.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4.2.2...v5.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-12 08:02:59 +02:00
ibizaman
5241626a3a bootstrap: make password_file a standard custom attribute
Otherwise the bootstrap script tries to create the password_file
as a custom attribute which fails since it's not in the schema.
And anyway, it shouldn't be in the schema.
2025-08-06 22:13:22 +02:00
Valentin Tolmer
363ef106e2 app: Fix attribute type parsing 2025-07-30 01:02:47 +02:00
ibizaman
3c7e4c3dec bootstrap: do not leak password in process list 2025-07-22 08:51:35 +02:00
Valentin Tolmer
fa196a9fd9 docker: try several GPG server
Sometimes the docker build fails because the gpg server is intermittently unavailable
2025-07-22 01:10:25 +02:00
ibizaman
f02b365478 bootstrap: do not fail if no user or group defined 2025-07-21 23:35:49 +02:00
Valentin Tolmer
0b0e6ae2cd github: Fix warnings about Dockerfile syntax 2025-07-21 23:23:37 +02:00
Valentin Tolmer
da525fc99b app: simplify attribute_type handling, display creation time in user details
In the user table it's still only the date, but that makes sense for an overview
2025-07-21 23:15:46 +02:00
ibizaman
78337bce72 bootstrap: allow to give password from a file 2025-07-16 23:51:21 +02:00
selfhoster1312
87e9311a44 meta: Fix cargo clippy failures (format strings) 2025-07-16 23:23:08 +02:00
Hendrik Sievers
53e62ecf5a docs: move authelia configuration to markdown file (#1205) 2025-07-13 22:29:09 +02:00
core
10d33a7537 readme: fix broken Iink 2025-07-11 00:52:03 +02:00
copilot-swe-agent[bot]
ada438398e set-password: load system certificates
Fixes #1206
2025-07-08 22:46:13 +02:00
64 changed files with 701 additions and 352 deletions

View File

@@ -1,4 +1,4 @@
FROM rust:1.74
FROM rust:1.85.0
ARG USERNAME=lldapdev
# We need to keep the user as 1001 to match the GitHub runner's UID.

159
.github/copilot-instructions.md vendored Normal file
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
.github/copilot-setup-steps.yml vendored Normal file
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

View File

@@ -1,6 +1,6 @@
FROM localhost:5000/lldap/lldap:alpine-base
# Taken directly from https://github.com/tianon/gosu/blob/master/INSTALL.md
ENV GOSU_VERSION 1.17
ENV GOSU_VERSION=1.17
RUN set -eux; \
\
apk add --no-cache --virtual .gosu-deps \
@@ -15,7 +15,18 @@ RUN set -eux; \
\
# verify the signature
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
for server in \
hkps://keys.openpgp.org \
ha.pool.sks-keyservers.net \
hkp://p80.pool.sks-keyservers.net:80 \
keyserver.ubuntu.com \
hkp://keyserver.ubuntu.com:80 \
pgp.mit.edu \
; do \
if gpg --batch --keyserver "$server" --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; then \
break; \
fi; \
done; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
gpgconf --kill all; \
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \

View File

@@ -1,12 +1,15 @@
FROM localhost:5000/lldap/lldap:debian-base
# Taken directly from https://github.com/tianon/gosu/blob/master/INSTALL.md
ENV GOSU_VERSION 1.17
ENV GOSU_VERSION=1.17
RUN set -eux; \
# save list of currently installed packages for later so we can clean up
savedAptMark="$(apt-mark showmanual)"; \
apt-get update; \
apt-get install -y --no-install-recommends ca-certificates gnupg wget; \
rm -rf /var/lib/apt/lists/*; \
for i in 1 2 3; do \
apt-get update && \
apt-get install -y --no-install-recommends wget ca-certificates gnupg && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && break || sleep 5; \
done; \
\
dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
@@ -14,7 +17,18 @@ RUN set -eux; \
\
# verify the signature
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
for server in \
hkps://keys.openpgp.org \
ha.pool.sks-keyservers.net \
hkp://p80.pool.sks-keyservers.net:80 \
keyserver.ubuntu.com \
hkp://keyserver.ubuntu.com:80 \
pgp.mit.edu \
; do \
if gpg --batch --keyserver "$server" --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; then \
break; \
fi; \
done; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
gpgconf --kill all; \
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \

View File

@@ -1,5 +1,5 @@
# Keep tracking base image
FROM rust:1.85-slim-bookworm
FROM rust:1.85.0-slim-bookworm
# Set needed env path
ENV PATH="/opt/armv7l-linux-musleabihf-cross/:/opt/armv7l-linux-musleabihf-cross/bin/:/opt/aarch64-linux-musl-cross/:/opt/aarch64-linux-musl-cross/bin/:/opt/x86_64-linux-musl-cross/:/opt/x86_64-linux-musl-cross/bin/:$PATH"

View File

@@ -87,7 +87,7 @@ jobs:
image: lldap/rust-dev:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.2
uses: actions/checkout@v5.0.0
- uses: actions/cache@v4
with:
path: |
@@ -132,7 +132,7 @@ jobs:
CARGO_HOME: ${GITHUB_WORKSPACE}/.cargo
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.2
uses: actions/checkout@v5.0.0
- uses: actions/cache@v4
with:
path: |
@@ -300,7 +300,7 @@ jobs:
steps:
- name: Checkout scripts
uses: actions/checkout@v4.2.2
uses: actions/checkout@v5.0.0
with:
sparse-checkout: 'scripts'
@@ -496,7 +496,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.2
uses: actions/checkout@v5.0.0
- name: Download all artifacts
uses: actions/download-artifact@v4

View File

@@ -8,6 +8,7 @@ on:
env:
CARGO_TERM_COLOR: always
RUST_VERSION: "1.85.0"
jobs:
pre_job:
@@ -33,7 +34,12 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@v4.2.2
uses: actions/checkout@v5.0.0
- name: Install Rust ${{ env.RUST_VERSION }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
override: true
- uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --verbose --workspace
@@ -52,7 +58,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4.2.2
uses: actions/checkout@v5.0.0
- name: Install Rust ${{ env.RUST_VERSION }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
override: true
components: clippy
- uses: Swatinem/rust-cache@v2
@@ -69,7 +82,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4.2.2
uses: actions/checkout@v5.0.0
- name: Install Rust ${{ env.RUST_VERSION }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
override: true
components: rustfmt
- uses: Swatinem/rust-cache@v2
@@ -88,10 +108,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4.2.2
uses: actions/checkout@v5.0.0
- 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
- name: Install Rust ${{ env.RUST_VERSION }} and nightly
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
override: true
components: llvm-tools-preview
- name: Install nightly toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
components: llvm-tools-preview
- uses: taiki-e/install-action@cargo-llvm-cov

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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.6.2] 2025-07-21
Small release, focused on LDAP improvements and ongoing maintenance.
### Added
- LDAP
- Support for searching groups by their `groupid`
- Support for `whoamiOID`
- Support for creating groups
- Support for subschema entry
- Custom assets path.
- New endpoint for requesting client settings
### Changed
- A missing JWT secret now prevents startup.
- Attributes with invalid characters (such as underscores) cannot be created anymore.
- Searching custom (string) attributes is now case insensitive.
- Using the top-level `firstName`, `lastName` and `avatar` GraphQL fields for users is now deprecated. Use the `attributes` field instead.
### Fixed
- `lldap_set_password` now uses the system's SSL certificates.
### Cleanups
- Split the main `lldap` crate into many sub-crates
- Various dependency version bumps
- Upgraded to 2024 Rust edition
- Docs/FAQ improvements
### Bootstrap script
- Custom attributes support
- Read the paswsord from a file
- Resilient to no user or group files
### New services
- Discord integration (Discord role to LLDAP user)
- HashiCorp
- Jellyfin 2FA with Duo
- Kimai
- Mailcow
- Peertube
- Penpot
- PgAdmin
- Project Quay
- Quadlet
- Snipe-IT
- SSSD
- Stalwart
- UnifiOS
## [0.6.1] 2024-11-22
Small release, mainly to fix a migration issue with Sqlite and Postgresql.

9
Cargo.lock generated
View File

@@ -2506,7 +2506,7 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "lldap"
version = "0.6.2-alpha"
version = "0.6.2"
dependencies = [
"actix",
"actix-files",
@@ -2532,7 +2532,7 @@ dependencies = [
"futures-util",
"graphql_client 0.11.0",
"hmac 0.12.1",
"http 1.1.0",
"http 0.2.12",
"juniper",
"jwt 0.16.0",
"ldap3",
@@ -2599,11 +2599,12 @@ dependencies = [
[[package]]
name = "lldap_app"
version = "0.6.2-alpha"
version = "0.6.2"
dependencies = [
"anyhow",
"base64 0.13.1",
"chrono",
"derive_more 1.0.0",
"gloo-console",
"gloo-file",
"gloo-net",
@@ -2618,6 +2619,7 @@ dependencies = [
"rand 0.8.5",
"serde",
"serde_json",
"strum 0.25.0",
"url-escape",
"validator",
"validator_derive",
@@ -3717,6 +3719,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"rustls 0.21.12",
"rustls-native-certs",
"rustls-pemfile 1.0.4",
"serde",
"serde_json",

View File

@@ -16,6 +16,7 @@ edition = "2024"
homepage = "https://github.com/lldap/lldap"
license = "GPL-3.0-only"
repository = "https://github.com/lldap/lldap"
rust-version = "1.85.0"
[profile.release]
lto = true

View File

@@ -1,5 +1,5 @@
# Build image
FROM rust:alpine3.21 AS chef
FROM rust:1.85.0-alpine3.21 AS chef
RUN set -x \
# Add user

View File

@@ -200,7 +200,7 @@ service that seems definitely incompatible with LLDAP.
- [I can't login](docs/faq.md#i-cant-log-in)
- [Discord Integration](docs/faq.md#discord-integration)
- [Migrating from SQLite](docs/faq.md#migrating-from-sqlite)
- How does lldap compare [with OpenLDAP](docs/faq.md#how-does-lldap-compare-with-openldap)? [With FreeIPA](docs/faq.md#how-does-lldap-compare-with-freeipa)? [With Kanidm]?(docs/faq.md#how-does-lldap-compare-with-kanidm)
- How does lldap compare [with OpenLDAP](docs/faq.md#how-does-lldap-compare-with-openldap)? [With FreeIPA](docs/faq.md#how-does-lldap-compare-with-freeipa)? [With Kanidm](docs/faq.md#how-does-lldap-compare-with-kanidm)?
- [Does lldap support vhosts?](docs/faq.md#does-lldap-support-vhosts)
- [Does lldap provide commercial support contracts?](docs/faq.md#does-lldap-provide-commercial-support-contracts)
- [Can I make a donation to fund development?](docs/faq.md#can-i-make-a-donation-to-fund-development)

View File

@@ -1,6 +1,6 @@
[package]
name = "lldap_app"
version = "0.6.2-alpha"
version = "0.6.2"
description = "Frontend for LLDAP"
edition.workspace = true
include = ["src/**/*", "queries/**/*", "Cargo.toml", "../schema.graphql"]
@@ -55,6 +55,11 @@ features = [
"wasmbind"
]
[dependencies.derive_more]
features = ["debug", "display", "from", "from_str"]
default-features = false
version = "1"
[dependencies.lldap_auth]
path = "../crates/auth"
features = [ "opaque_client" ]
@@ -73,6 +78,10 @@ version = "0.24"
[dependencies.serde]
workspace = true
[dependencies.strum]
features = ["derive"]
version = "0.25"
[dependencies.yew_form]
git = "https://github.com/jfbilodeau/yew_form"
rev = "4b9fabffb63393ec7626a4477fd36de12a07fac9"

View File

@@ -7,7 +7,6 @@ use crate::{
},
router::AppRoute,
},
convert_attribute_type,
infra::{
common_component::{CommonComponent, CommonComponentParts},
form_utils::{
@@ -30,7 +29,8 @@ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
schema_path = "../schema.graphql",
query_path = "queries/get_group_attributes_schema.graphql",
response_derives = "Debug,Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct GetGroupAttributesSchema;
@@ -39,8 +39,6 @@ use get_group_attributes_schema::ResponseData;
pub type Attribute =
get_group_attributes_schema::GetGroupAttributesSchemaSchemaGroupSchemaAttributes;
convert_attribute_type!(get_group_attributes_schema::AttributeType);
impl From<&Attribute> for GraphQlAttributeSchema {
fn from(attr: &Attribute) -> Self {
Self {
@@ -218,14 +216,14 @@ fn get_custom_attribute_input(attribute_schema: &Attribute) -> Html {
html! {
<ListAttributeInput
name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
attribute_type={attribute_schema.attribute_type}
/>
}
} else {
html! {
<SingleAttributeInput
name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
attribute_type={attribute_schema.attribute_type}
/>
}
}

View File

@@ -3,7 +3,6 @@ use crate::{
form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit},
router::AppRoute,
},
convert_attribute_type,
infra::{
common_component::{CommonComponent, CommonComponentParts},
schema::{AttributeType, validate_attribute_type},
@@ -23,12 +22,11 @@ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
schema_path = "../schema.graphql",
query_path = "queries/create_group_attribute.graphql",
response_derives = "Debug",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct CreateGroupAttribute;
convert_attribute_type!(create_group_attribute::AttributeType);
pub struct CreateGroupAttributeForm {
common: CommonComponentParts<Self>,
form: yew_form::Form<CreateGroupAttributeModel>,
@@ -70,10 +68,11 @@ impl CommonComponent<CreateGroupAttributeForm> for CreateGroupAttributeForm {
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 {
name: model.attribute_name,
attribute_type: create_group_attribute::AttributeType::from(attribute_type),
attribute_type,
is_list: model.is_list,
is_visible: model.is_visible,
};
@@ -145,7 +144,7 @@ impl Component for CreateGroupAttributeForm {
oninput={link.callback(|_| Msg::Update)}>
<option selected=true value="String">{"String"}</option>
<option value="Integer">{"Integer"}</option>
<option value="Jpeg">{"Jpeg"}</option>
<option value="JpegPhoto">{"Jpeg"}</option>
<option value="DateTime">{"DateTime"}</option>
</Select<CreateGroupAttributeModel>>
<CheckBox<CreateGroupAttributeModel>

View File

@@ -7,7 +7,6 @@ use crate::{
},
router::AppRoute,
},
convert_attribute_type,
infra::{
api::HostService,
common_component::{CommonComponent, CommonComponentParts},
@@ -32,7 +31,8 @@ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
schema_path = "../schema.graphql",
query_path = "queries/get_user_attributes_schema.graphql",
response_derives = "Debug,Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct GetUserAttributesSchema;
@@ -40,8 +40,6 @@ use get_user_attributes_schema::ResponseData;
pub type Attribute = get_user_attributes_schema::GetUserAttributesSchemaSchemaUserSchemaAttributes;
convert_attribute_type!(get_user_attributes_schema::AttributeType);
impl From<&Attribute> for GraphQlAttributeSchema {
fn from(attr: &Attribute) -> Self {
Self {
@@ -310,14 +308,14 @@ fn get_custom_attribute_input(attribute_schema: &Attribute) -> Html {
html! {
<ListAttributeInput
name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
attribute_type={attribute_schema.attribute_type}
/>
}
} else {
html! {
<SingleAttributeInput
name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
attribute_type={attribute_schema.attribute_type}
/>
}
}

View File

@@ -3,7 +3,6 @@ use crate::{
form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit},
router::AppRoute,
},
convert_attribute_type,
infra::{
common_component::{CommonComponent, CommonComponentParts},
schema::{AttributeType, validate_attribute_type},
@@ -23,12 +22,11 @@ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
schema_path = "../schema.graphql",
query_path = "queries/create_user_attribute.graphql",
response_derives = "Debug",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct CreateUserAttribute;
convert_attribute_type!(create_user_attribute::AttributeType);
pub struct CreateUserAttributeForm {
common: CommonComponentParts<Self>,
form: yew_form::Form<CreateUserAttributeModel>,
@@ -74,10 +72,11 @@ impl CommonComponent<CreateUserAttributeForm> for CreateUserAttributeForm {
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 {
name: model.attribute_name,
attribute_type: create_user_attribute::AttributeType::from(attribute_type),
attribute_type,
is_editable: model.is_editable,
is_list: model.is_list,
is_visible: model.is_visible,
@@ -147,7 +146,7 @@ impl Component for CreateUserAttributeForm {
oninput={link.callback(|_| Msg::Update)}>
<option selected=true value="String">{"String"}</option>
<option value="Integer">{"Integer"}</option>
<option value="Jpeg">{"Jpeg"}</option>
<option value="JpegPhoto">{"Jpeg"}</option>
<option value="DateTime">{"DateTime"}</option>
</Select<CreateUserAttributeModel>>
<CheckBox<CreateUserAttributeModel>

View File

@@ -26,7 +26,7 @@ fn attribute_input(props: &AttributeInputProps) -> Html {
<DateTimeInput name={props.name.clone()} value={props.value.clone()} />
};
}
AttributeType::Jpeg => {
AttributeType::JpegPhoto => {
return html! {
<JpegFileInput name={props.name.clone()} value={props.value.clone()} />
};
@@ -82,7 +82,7 @@ fn attribute_label(props: &AttributeLabelProps) -> Html {
#[derive(Properties, PartialEq)]
pub struct SingleAttributeInputProps {
pub name: String,
pub attribute_type: AttributeType,
pub(crate) attribute_type: AttributeType,
#[prop_or(None)]
pub value: Option<String>,
}
@@ -94,7 +94,7 @@ pub fn single_attribute_input(props: &SingleAttributeInputProps) -> Html {
<AttributeLabel name={props.name.clone()} />
<div class="col-8">
<AttributeInput
attribute_type={props.attribute_type.clone()}
attribute_type={props.attribute_type}
name={props.name.clone()}
value={props.value.clone()} />
</div>
@@ -105,7 +105,7 @@ pub fn single_attribute_input(props: &SingleAttributeInputProps) -> Html {
#[derive(Properties, PartialEq)]
pub struct ListAttributeInputProps {
pub name: String,
pub attribute_type: AttributeType,
pub(crate) attribute_type: AttributeType,
#[prop_or(vec!())]
pub values: Vec<String>,
}
@@ -165,7 +165,7 @@ impl Component for ListAttributeInput {
{self.indices.iter().map(|&i| html! {
<div class="input-group mb-2" key={i}>
<AttributeInput
attribute_type={props.attribute_type.clone()}
attribute_type={props.attribute_type}
name={props.name.clone()}
value={props.values.get(i).cloned().unwrap_or_default()} />
<button

View File

@@ -5,10 +5,10 @@ use crate::{
remove_user_from_group::RemoveUserFromGroupComponent,
router::{AppRoute, Link},
},
convert_attribute_type,
infra::{
common_component::{CommonComponent, CommonComponentParts},
form_utils::GraphQlAttributeSchema,
schema::AttributeType,
},
};
use anyhow::{Error, Result, bail};
@@ -20,7 +20,8 @@ use yew::prelude::*;
schema_path = "../schema.graphql",
query_path = "queries/get_group_details.graphql",
response_derives = "Debug, Hash, PartialEq, Eq, Clone",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct GetGroupDetails;
@@ -29,9 +30,6 @@ pub type User = get_group_details::GetGroupDetailsGroupUsers;
pub type AddGroupMemberUser = add_group_member::User;
pub type Attribute = get_group_details::GetGroupDetailsGroupAttributes;
pub type AttributeSchema = get_group_details::GetGroupDetailsSchemaGroupSchemaAttributes;
pub type AttributeType = get_group_details::AttributeType;
convert_attribute_type!(AttributeType);
impl From<&AttributeSchema> for GraphQlAttributeSchema {
fn from(attr: &AttributeSchema) -> Self {

View File

@@ -10,7 +10,6 @@ use crate::{
infra::{
common_component::{CommonComponent, CommonComponentParts},
form_utils::{AttributeValue, EmailIsRequired, IsAdmin, read_all_form_attributes},
schema::AttributeType,
},
};
use anyhow::{Ok, Result};
@@ -174,7 +173,7 @@ fn get_custom_attribute_input(
html! {
<ListAttributeInput
name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
attribute_type={attribute_schema.attribute_type}
values={values}
/>
}
@@ -182,7 +181,7 @@ fn get_custom_attribute_input(
html! {
<SingleAttributeInput
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()}
/>
}

View File

@@ -4,7 +4,6 @@ use crate::{
fragments::attribute_schema::render_attribute_name,
router::{AppRoute, Link},
},
convert_attribute_type,
infra::{
attributes::group,
common_component::{CommonComponent, CommonComponentParts},
@@ -21,7 +20,8 @@ use yew::prelude::*;
schema_path = "../schema.graphql",
query_path = "queries/get_group_attributes_schema.graphql",
response_derives = "Debug,Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct GetGroupAttributesSchema;
@@ -30,8 +30,6 @@ use get_group_attributes_schema::ResponseData;
pub type Attribute =
get_group_attributes_schema::GetGroupAttributesSchemaSchemaGroupSchemaAttributes;
convert_attribute_type!(get_group_attributes_schema::AttributeType);
#[derive(yew::Properties, Clone, PartialEq, Eq)]
pub struct Props {
pub hardcoded: bool,
@@ -147,7 +145,7 @@ impl GroupSchemaTable {
fn view_attribute(&self, ctx: &Context<Self>, attribute: &Attribute) -> Html {
let link = ctx.link();
let attribute_type = AttributeType::from(attribute.attribute_type.clone());
let attribute_type = attribute.attribute_type;
let checkmark = html! {
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z"></path>

View File

@@ -5,10 +5,10 @@ use crate::{
router::{AppRoute, Link},
user_details_form::UserDetailsForm,
},
convert_attribute_type,
infra::{
common_component::{CommonComponent, CommonComponentParts},
form_utils::GraphQlAttributeSchema,
schema::AttributeType,
},
};
use anyhow::{Error, Result, bail};
@@ -20,7 +20,8 @@ use yew::prelude::*;
schema_path = "../schema.graphql",
query_path = "queries/get_user_details.graphql",
response_derives = "Debug, Hash, PartialEq, Eq, Clone",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct GetUserDetails;
@@ -28,9 +29,6 @@ pub type User = get_user_details::GetUserDetailsUser;
pub type Group = get_user_details::GetUserDetailsUserGroups;
pub type Attribute = get_user_details::GetUserDetailsUserAttributes;
pub type AttributeSchema = get_user_details::GetUserDetailsSchemaUserSchemaAttributes;
pub type AttributeType = get_user_details::AttributeType;
convert_attribute_type!(AttributeType);
impl From<&AttributeSchema> for GraphQlAttributeSchema {
fn from(attr: &AttributeSchema) -> Self {

View File

@@ -14,6 +14,7 @@ use crate::{
},
};
use anyhow::{Ok, Result};
use gloo_console::console;
use graphql_client::GraphQLQuery;
use yew::prelude::*;
@@ -168,7 +169,7 @@ fn get_custom_attribute_input(
html! {
<ListAttributeInput
name={attribute_schema.name.clone()}
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
attribute_type={attribute_schema.attribute_type}
values={values}
/>
}
@@ -176,7 +177,7 @@ fn get_custom_attribute_input(
html! {
<SingleAttributeInput
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()}
/>
}
@@ -192,9 +193,19 @@ fn get_custom_attribute_static(
.find(|a| a.name == attribute_schema.name)
.map(|attribute| attribute.value.clone())
.unwrap_or_default();
let value_to_str = match attribute_schema.attribute_type {
AttributeType::String | AttributeType::Integer => |v: String| v,
AttributeType::DateTime => |v: String| {
console!(format!("Parsing date: {}", &v));
chrono::DateTime::parse_from_rfc3339(&v)
.map(|dt| dt.naive_utc().to_string())
.unwrap_or_else(|_| "Invalid date".to_string())
},
AttributeType::JpegPhoto => |_: String| "Unimplemented JPEG display".to_string(),
};
html! {
<StaticValue label={attribute_schema.name.clone()} id={attribute_schema.name.clone()}>
{values.into_iter().map(|x| html!{<div>{x}</div>}).collect::<Vec<_>>()}
{values.into_iter().map(|x| html!{<div>{value_to_str(x)}</div>}).collect::<Vec<_>>()}
</StaticValue>
}
}

View File

@@ -4,7 +4,6 @@ use crate::{
fragments::attribute_schema::render_attribute_name,
router::{AppRoute, Link},
},
convert_attribute_type,
infra::{
attributes::user,
common_component::{CommonComponent, CommonComponentParts},
@@ -21,7 +20,8 @@ use yew::prelude::*;
schema_path = "../schema.graphql",
query_path = "queries/get_user_attributes_schema.graphql",
response_derives = "Debug,Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql"
custom_scalars_module = "crate::infra::graphql",
extern_enums("AttributeType")
)]
pub struct GetUserAttributesSchema;
@@ -29,8 +29,6 @@ use get_user_attributes_schema::ResponseData;
pub type Attribute = get_user_attributes_schema::GetUserAttributesSchemaSchemaUserSchemaAttributes;
convert_attribute_type!(get_user_attributes_schema::AttributeType);
#[derive(yew::Properties, Clone, PartialEq, Eq)]
pub struct Props {
pub hardcoded: bool,
@@ -146,7 +144,7 @@ impl UserSchemaTable {
fn view_attribute(&self, ctx: &Context<Self>, attribute: &Attribute) -> Html {
let link = ctx.link();
let attribute_type = AttributeType::from(attribute.attribute_type.clone());
let attribute_type = attribute.attribute_type;
let checkmark = html! {
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z"></path>

View File

@@ -1,66 +1,42 @@
use anyhow::Result;
use std::{fmt::Display, str::FromStr};
use derive_more::Display;
use serde::{Deserialize, Serialize};
use strum::EnumString;
use validator::ValidationError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AttributeType {
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash, EnumString, Display)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[strum(ascii_case_insensitive)]
pub(crate) enum AttributeType {
String,
Integer,
#[strum(serialize = "DATE_TIME", serialize = "DATETIME")]
DateTime,
Jpeg,
}
impl Display for AttributeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl FromStr for AttributeType {
type Err = ();
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"String" => Ok(AttributeType::String),
"Integer" => Ok(AttributeType::Integer),
"DateTime" => Ok(AttributeType::DateTime),
"Jpeg" => Ok(AttributeType::Jpeg),
_ => Err(()),
}
}
}
// Macro to generate traits for converting between AttributeType and the
// graphql generated equivalents.
#[macro_export]
macro_rules! convert_attribute_type {
($source_type:ty) => {
impl From<$source_type> for $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,
}
}
}
};
#[strum(serialize = "JPEG_PHOTO", serialize = "JPEGPHOTO")]
JpegPhoto,
}
pub fn validate_attribute_type(attribute_type: &str) -> Result<(), ValidationError> {
AttributeType::from_str(attribute_type)
AttributeType::try_from(attribute_type)
.map_err(|_| ValidationError::new("Invalid attribute type"))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deserialize_attribute_type() {
let attr_type: AttributeType = "STRING".try_into().unwrap();
assert_eq!(attr_type, AttributeType::String);
let attr_type: AttributeType = "Integer".try_into().unwrap();
assert_eq!(attr_type, AttributeType::Integer);
let attr_type: AttributeType = "DATE_TIME".try_into().unwrap();
assert_eq!(attr_type, AttributeType::DateTime);
let attr_type: AttributeType = "JpegPhoto".try_into().unwrap();
assert_eq!(attr_type, AttributeType::JpegPhoto);
}
}

View File

@@ -12,11 +12,11 @@ pub fn deserialize_attribute_value(
let parse_int = |value: &String| -> Result<i64> {
value
.parse::<i64>()
.with_context(|| format!("Invalid integer value {}", value))
.with_context(|| format!("Invalid integer value {value}"))
};
let parse_date = |value: &String| -> Result<chrono::NaiveDateTime> {
Ok(chrono::DateTime::parse_from_rfc3339(value)
.with_context(|| format!("Invalid date value {}", value))?
.with_context(|| format!("Invalid date value {value}"))?
.naive_utc())
};
let parse_photo = |value: &String| -> Result<JpegPhoto> {

View File

@@ -377,7 +377,7 @@ impl std::fmt::Debug for JpegPhoto {
encoded.push_str(" ...");
};
f.debug_tuple("JpegPhoto")
.field(&format!("b64[{}]", encoded))
.field(&format!("b64[{encoded}]"))
.finish()
}
}

View File

@@ -75,7 +75,7 @@ pub fn export_schema(output_file: Option<String>) -> anyhow::Result<()> {
use lldap_sql_backend_handler::SqlBackendHandler;
let output = schema::<SqlBackendHandler>().as_schema_language();
match output_file {
None => println!("{}", output),
None => println!("{output}"),
Some(path) => {
use std::fs::File;
use std::io::prelude::*;

View File

@@ -76,7 +76,7 @@ pub fn get_group_attribute(
.users
.iter()
.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(),
GroupFieldType::Uuid => vec![group.uuid.to_string().into_bytes()],
GroupFieldType::Attribute(attr, _, _) => get_custom_attribute(&group.attributes, &attr)?,
@@ -86,8 +86,7 @@ pub fn get_group_attribute(
"+" => return None,
"*" => {
panic!(
"Matched {}, * should have been expanded into attribute list and * removed",
attribute
"Matched {attribute}, * should have been expanded into attribute list and * removed"
)
}
_ => {
@@ -211,7 +210,7 @@ fn convert_group_filter(
.map(GroupRequestFilter::Uuid)
.map_err(|e| LdapError {
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(
&value_lc,
@@ -290,15 +289,14 @@ fn convert_group_filter(
_ => Err(LdapError {
code: LdapResultCode::UnwillingToPerform,
message: format!(
"Unsupported group attribute for substring filter: \"{}\"",
field
"Unsupported group attribute for substring filter: \"{field}\""
),
}),
}
}
_ => Err(LdapError {
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
.map_err(|e| LdapError {
code: LdapResultCode::Other,
message: format!(r#"Error while listing groups "{}": {:#}"#, base, e),
message: format!(r#"Error while listing groups "{base}": {e:#}"#),
})
}

View File

@@ -100,8 +100,7 @@ pub fn get_user_attribute(
"+" => return None,
"*" => {
panic!(
"Matched {}, * should have been expanded into attribute list and * removed",
attribute
"Matched {attribute}, * should have been expanded into attribute list and * removed"
)
}
_ => {
@@ -298,10 +297,7 @@ fn convert_user_filter(
| UserFieldType::PrimaryField(UserColumn::CreationDate)
| UserFieldType::PrimaryField(UserColumn::Uuid) => Err(LdapError {
code: LdapResultCode::UnwillingToPerform,
message: format!(
"Unsupported user attribute for substring filter: {:?}",
field
),
message: format!("Unsupported user attribute for substring filter: {field:?}"),
}),
UserFieldType::NoMatch => Ok(UserRequestFilter::from(false)),
UserFieldType::PrimaryField(UserColumn::Email) => Ok(UserRequestFilter::SubString(
@@ -316,7 +312,7 @@ fn convert_user_filter(
}
_ => Err(LdapError {
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
.map_err(|e| LdapError {
code: LdapResultCode::Other,
message: format!(r#"Error while searching user "{}": {:#}"#, base, e),
message: format!(r#"Error while searching user "{base}": {e:#}"#),
})
}

View File

@@ -66,10 +66,9 @@ impl UserOrGroupName {
UserOrGroupName::InvalidSyntax(err) => return err,
UserOrGroupName::UnexpectedFormat
| UserOrGroupName::User(_)
| UserOrGroupName::Group(_) => format!(
r#"Unexpected DN format. Got "{}", expected: {}"#,
input, expected_format
),
| UserOrGroupName::Group(_) => {
format!(r#"Unexpected DN format. Got "{input}", expected: {expected_format}"#)
}
},
}
}
@@ -105,7 +104,7 @@ pub fn get_user_id_from_distinguished_name(
) -> LdapResult<UserId> {
match get_user_or_group_id_from_distinguished_name(dn, base_tree) {
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> {
match get_user_or_group_id_from_distinguished_name(dn, base_tree) {
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"
impl ObjectClassList {
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"
// Parameter 'index_offset' is an offset for the enumeration of this list of 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();
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(
format!(
"( 2.{} NAME '{}' DESC 'LLDAP: {}' SUP {:?} )",
"( 10.{} NAME '{}' DESC 'LLDAP: {}' SUP {:?} )",
(index + index_offset),
attribute.name,
if attribute.is_hardcoded {

View File

@@ -33,10 +33,7 @@ pub(crate) async fn create_user_or_group(
}
err => Err(err.into_ldap_error(
&request.dn,
format!(
r#""uid=id,ou=people,{}" or "uid=id,ou=groups,{}""#,
base_dn_str, base_dn_str
),
format!(r#""uid=id,ou=people,{base_dn_str}" or "uid=id,ou=groups,{base_dn_str}""#),
)),
}
}
@@ -73,10 +70,7 @@ async fn create_user(
std::str::from_utf8(val)
.map_err(|e| LdapError {
code: LdapResultCode::ConstraintViolation,
message: format!(
"Attribute value is invalid UTF-8: {:#?} (value {:?})",
e, val
),
message: format!("Attribute value is invalid UTF-8: {e:#?} (value {val:?})"),
})
.map(str::to_owned)
}
@@ -92,7 +86,7 @@ async fn create_user(
value: deserialize::deserialize_attribute_value(&[value], typ, false).map_err(|e| {
LdapError {
code: LdapResultCode::ConstraintViolation,
message: format!("Invalid attribute value: {}", e),
message: format!("Invalid attribute value: {e}"),
}
})?,
})
@@ -134,7 +128,7 @@ async fn create_user(
.await
.map_err(|e| LdapError {
code: LdapResultCode::OperationsError,
message: format!("Could not create user: {:#?}", e),
message: format!("Could not create user: {e:#?}"),
})?;
Ok(vec![make_add_response(
LdapResultCode::Success,
@@ -156,7 +150,7 @@ async fn create_group(
.await
.map_err(|e| LdapError {
code: LdapResultCode::OperationsError,
message: format!("Could not create group: {:#?}", e),
message: format!("Could not create group: {e:#?}"),
})?;
Ok(vec![make_add_response(
LdapResultCode::Success,

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,
err => Err(err.into_ldap_error(
&request,
format!(
r#""uid=id,ou=people,{}" or "uid=id,ou=groups,{}""#,
base_dn_str, base_dn_str
),
format!(r#""uid=id,ou=people,{base_dn_str}" or "uid=id,ou=groups,{base_dn_str}""#),
)),
}
}
@@ -53,7 +50,7 @@ async fn delete_user(
},
e => LdapError {
code: LdapResultCode::OperationsError,
message: format!("Error while finding user: {:?}", e),
message: format!("Error while finding user: {e:?}"),
},
})?;
backend_handler
@@ -61,7 +58,7 @@ async fn delete_user(
.await
.map_err(|e| LdapError {
code: LdapResultCode::OperationsError,
message: format!("Error while deleting user: {:?}", e),
message: format!("Error while deleting user: {e:?}"),
})?;
Ok(vec![make_del_response(
LdapResultCode::Success,
@@ -79,7 +76,7 @@ async fn delete_group(
.await
.map_err(|e| LdapError {
code: LdapResultCode::OperationsError,
message: format!("Error while finding group: {:?}", e),
message: format!("Error while finding group: {e:?}"),
})?;
let group_id = groups
.iter()
@@ -94,7 +91,7 @@ async fn delete_group(
.await
.map_err(|e| LdapError {
code: LdapResultCode::OperationsError,
message: format!("Error while deleting group: {:?}", e),
message: format!("Error while deleting group: {e:?}"),
})?;
Ok(vec![make_del_response(
LdapResultCode::Success,

View File

@@ -100,10 +100,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
backend_handler,
ldap_info: LdapInfo {
base_dn: parse_distinguished_name(&ldap_base_dn).unwrap_or_else(|_| {
panic!(
"Invalid value for ldap_base_dn in configuration: {}",
ldap_base_dn
)
panic!("Invalid value for ldap_base_dn in configuration: {ldap_base_dn}")
}),
base_dn_str: ldap_base_dn,
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 {
code: LdapResultCode::OperationsError,
message: format!("Unable to get schema: {:#}", e),
message: format!("Unable to get schema: {e:#}"),
})?;
return Ok(vec![
make_ldap_subschema_entry(PublicSchema::from(schema)),
@@ -224,7 +221,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
}
Err(e) => vec![make_extended_response(
LdapResultCode::ProtocolError,
format!("Error while parsing password modify request: {:#?}", e),
format!("Error while parsing password modify request: {e:#?}"),
)],
},
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)]),
op => vec![make_extended_response(
LdapResultCode::UnwillingToPerform,
format!("Unsupported operation: {:#?}", op),
format!("Unsupported operation: {op:#?}"),
)],
})
}

View File

@@ -47,7 +47,7 @@ async fn handle_modify_change(
.await
.map_err(|e| LdapError {
code: LdapResultCode::Other,
message: format!("Error while changing the password: {:#?}", e),
message: format!("Error while changing the password: {e:#?}"),
})?;
} else {
return Err(LdapError {
@@ -94,7 +94,7 @@ where
.await
.map_err(|e| LdapError {
code: LdapResultCode::OperationsError,
message: format!("Internal error while requesting user's groups: {:#?}", e),
message: format!("Internal error while requesting user's groups: {e:#?}"),
})?
.iter()
.any(|g| g.display_name == "lldap_admin".into());
@@ -115,7 +115,7 @@ where
}
Err(e) => Err(LdapError {
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 {
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 {
operation: LdapModifyType::Replace,
modification: ldap3_proto::LdapPartialAttribute {
@@ -284,7 +284,7 @@ mod tests {
let request = {
let target_user = "bob";
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 {
operation: LdapModifyType::Replace,
modification: ldap3_proto::LdapPartialAttribute {

View File

@@ -112,8 +112,7 @@ pub(crate) async fn do_password_modification<Handler: BackendHandler>(
.map_err(|e| LdapError {
code: LdapResultCode::OperationsError,
message: format!(
"Internal error while requesting user's groups: {:#?}",
e
"Internal error while requesting user's groups: {e:#?}"
),
})?
.iter()
@@ -131,7 +130,7 @@ pub(crate) async fn do_password_modification<Handler: BackendHandler>(
{
Err(LdapError {
code: LdapResultCode::Other,
message: format!("Error while changing the password: {:#?}", e),
message: format!("Error while changing the password: {e:#?}"),
})
} else {
Ok(vec![make_extended_response(
@@ -142,7 +141,7 @@ pub(crate) async fn do_password_modification<Handler: BackendHandler>(
}
Err(e) => Err(LdapError {
code: LdapResultCode::InvalidDNSyntax,
message: format!("Invalid username: {}", e),
message: format!("Invalid username: {e}"),
}),
}
}

View File

@@ -202,25 +202,66 @@ pub fn make_ldap_subschema_entry(schema: PublicSchema) -> LdapOp {
LdapPartialAttribute {
atype: "ldapSyntaxes".to_string(),
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.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.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 {
atype: "attributeTypes".to_string(),
vals: {
let hardcoded_attributes = [
b"( 2.0 NAME 'String' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )".to_vec(),
b"( 2.1 NAME 'Integer' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )".to_vec(),
b"( 2.2 NAME 'JpegPhoto' SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 )".to_vec(),
b"( 2.3 NAME 'DateTime' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )".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"( 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"( 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.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();
hardcoded_attributes.into_iter().chain(
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()
}
},
@@ -369,7 +410,7 @@ pub async fn do_search(
) -> LdapResult<Vec<LdapOp>> {
let schema = PublicSchema::from(backend_handler.get_schema().await.map_err(|e| LdapError {
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 mut results = match search_results {
@@ -485,7 +526,7 @@ mod tests {
};
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!(
@@ -530,47 +571,77 @@ mod tests {
LdapPartialAttribute {
atype: "ldapSyntaxes".to_owned(),
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.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.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!(
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 {
atype: "attributeTypes".to_owned(),
vals: vec![
b"( 2.0 NAME 'String' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )".to_vec(),
b"( 2.1 NAME 'Integer' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )".to_vec(),
b"( 2.2 NAME 'JpegPhoto' SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 )".to_vec(),
b"( 2.3 NAME 'DateTime' SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )".to_vec(),
b"( 2.4 NAME 'avatar' DESC 'LLDAP: builtin attribute' SUP JpegPhoto )".to_vec(),
b"( 2.5 NAME 'creation_date' DESC 'LLDAP: builtin attribute' SUP DateTime )"
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"( 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"( 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.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(),
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(),
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(),
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 {
atype: "objectClasses".to_owned(),
vals: vec![
@@ -581,7 +652,7 @@ mod tests {
);
assert_eq!(
attrs[8],
attrs[9],
LdapPartialAttribute {
atype: "subschemaSubentry".to_owned(),
vals: vec![b"cn=Subschema".to_vec()]

View File

@@ -91,7 +91,7 @@ pub mod tests {
handler
.create_user(CreateUserRequest {
user_id: UserId::new(name),
email: format!("{}@bob.bob", name).into(),
email: format!("{name}@bob.bob").into(),
display_name: Some("display ".to_string() + name),
attributes: vec![
DomainAttribute {

View File

@@ -164,7 +164,7 @@ impl GroupBackendHandler for SqlBackendHandler {
.one(&self.sql_pool)
.await?
.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()
.filter(model::GroupAttributesColumn::GroupId.eq(group_details.group_id))
.order_by_asc(model::GroupAttributesColumn::AttributeName)
@@ -252,8 +252,7 @@ impl GroupBackendHandler for SqlBackendHandler {
.await?;
if res.rows_affected == 0 {
return Err(DomainError::EntityNotFound(format!(
"No such group: '{:?}'",
group_id
"No such group: '{group_id:?}'"
)));
}
Ok(())
@@ -306,8 +305,7 @@ impl SqlBackendHandler {
remove_group_attributes.push(attribute);
} else {
return Err(DomainError::InternalError(format!(
"Group attribute name {} doesn't exist in the schema, yet was attempted to be removed from the database",
attribute
"Group attribute name {attribute} doesn't exist in the schema, yet was attempted to be removed from the database"
)));
}
}

View File

@@ -240,8 +240,7 @@ impl SqlBackendHandler {
remove_user_attributes.push(attribute);
} else {
return Err(DomainError::InternalError(format!(
"User attribute name {} doesn't exist in the schema, yet was attempted to be removed from the database",
attribute
"User attribute name {attribute} doesn't exist in the schema, yet was attempted to be removed from the database"
)));
}
}
@@ -384,8 +383,7 @@ impl UserBackendHandler for SqlBackendHandler {
.await?;
if res.rows_affected == 0 {
return Err(DomainError::EntityNotFound(format!(
"No such user: '{}'",
user_id
"No such user: '{user_id}'"
)));
}
Ok(())
@@ -408,8 +406,7 @@ impl UserBackendHandler for SqlBackendHandler {
.await?;
if res.rows_affected == 0 {
return Err(DomainError::EntityNotFound(format!(
"No such membership: '{}' -> {:?}",
user_id, group_id
"No such membership: '{user_id}' -> {group_id:?}"
)));
}
Ok(())

View File

@@ -55,8 +55,8 @@ Then you'll receive a JSON response with:
```
{
"token": "eYbat...",
"refreshToken": "3bCka...",
"token": "Yh6RJV...",
"refreshToken": "dww5jwU...",
}
```

View File

@@ -6,7 +6,7 @@ configuration files:
- [Airsonic Advanced](airsonic-advanced.md)
- [Apache Guacamole](apacheguacamole.md)
- [Apereo CAS Server](apereo_cas_server.md)
- [Authelia](authelia_config.yml)
- [Authelia](authelia.md)
- [Authentik](authentik.md)
- [Bookstack](bookstack.env.example)
- [Calibre-Web](calibre_web.md)

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

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

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
- `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_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

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

View File

@@ -12,6 +12,9 @@ USER_CONFIGS_DIR="${USER_CONFIGS_DIR:-/bootstrap/user-configs}"
GROUP_CONFIGS_DIR="${GROUP_CONFIGS_DIR:-/bootstrap/group-configs}"
LLDAP_SET_PASSWORD_PATH="${LLDAP_SET_PASSWORD_PATH:-/app/lldap_set_password}"
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
if [[ ! -d $USER_CONFIGS_DIR ]] && [[ -d "/user-configs" ]]; then
@@ -440,7 +443,7 @@ extract_custom_group_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() {
@@ -596,12 +599,18 @@ main() {
check_install_dependencies
check_required_env_vars
local user_config_files=("${USER_CONFIGS_DIR}"/*.json)
local group_config_files=("${GROUP_CONFIGS_DIR}"/*.json)
local user_config_files=()
local group_config_files=()
local user_schema_files=()
local group_schema_files=()
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
user_schema_files+=("$file")
done
@@ -647,7 +656,7 @@ main() {
printf -- '\n--- groups ---\n'
local group_config=''
while read -r group_config; do
[[ ${#group_config_files[@]} -gt 0 ]] && while read -r group_config; do
local group_name=''
group_name="$(printf '%s' "$group_config" | jq --raw-output '.name')"
create_group "$group_name"
@@ -675,7 +684,7 @@ main() {
else
local group_name=''
while read -r group_name; do
if [[ "$DO_CLEANUP" == 'true' ]]; then
if [[ "$DO_CLEANUP_GROUPS" == 'true' ]]; then
delete_group "$group_name"
else
printf '[WARNING] Group "%s" is not declared in config files\n' "$group_name"
@@ -690,9 +699,9 @@ main() {
TMP_AVATAR_DIR="$(mktemp -d)"
local user_config=''
while read -r user_config; do
local field='' id='' email='' displayName='' firstName='' lastName='' avatar_file='' avatar_url='' gravatar_avatar='' weserv_avatar='' password=''
for field in 'id' 'email' 'displayName' 'firstName' 'lastName' 'avatar_file' 'avatar_url' 'gravatar_avatar' 'weserv_avatar' 'password'; do
[[ ${#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='' password_file=''
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]')"
done
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"
redundant_users="$(printf '%s' "$redundant_users" | jq --compact-output --arg id "$id" '. - [$id]')"
if [[ "$password" != 'null' ]] && [[ "$password" != '""' ]]; then
"$LLDAP_SET_PASSWORD_PATH" --base-url "$LLDAP_URL" --token "$TOKEN" --username "$id" --password "$password"
if [[ "$password_file" != 'null' ]] && [[ "$password_file" != '""' ]]; then
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
# Process custom attributes
@@ -728,7 +739,7 @@ main() {
local user_group_name=''
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"
else
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
local id=''
while read -r id; do
if [[ "$DO_CLEANUP" == 'true' ]]; then
if [[ "$DO_CLEANUP_USERS" == 'true' ]]; then
delete_user "$id"
else
printf '[WARNING] User "%s" is not declared in config files\n' "$id"

View File

@@ -1,6 +1,6 @@
[package]
name = "lldap"
version = "0.6.2-alpha"
version = "0.6.2"
description = "Super-simple and lightweight LDAP server"
categories = ["authentication", "command-line-utilities"]
edition.workspace = true

View File

@@ -199,8 +199,7 @@ where
warn!("Error sending email: {:#?}", e);
info!("Reset token: {}", token);
return Err(TcpError::InternalServerError(format!(
"Could not send email: {}",
e
"Could not send email: {e}"
)));
}
Ok(())
@@ -254,7 +253,7 @@ where
Cookie::build("token", token.as_str())
.max_age(5.minutes())
// Cookie is only valid to reset the password.
.path(format!("{}auth", path))
.path(format!("{path}auth"))
.http_only(true)
.same_site(SameSite::Strict)
.finish(),
@@ -310,7 +309,7 @@ where
.cookie(
Cookie::build("refresh_token", "")
.max_age(0.days())
.path(format!("{}auth", path))
.path(format!("{path}auth"))
.http_only(true)
.same_site(SameSite::Strict)
.finish(),
@@ -381,7 +380,7 @@ where
.cookie(
Cookie::build("refresh_token", refresh_token_plus_name.clone())
.max_age(max_age.num_days().days())
.path(format!("{}auth", path))
.path(format!("{path}auth"))
.http_only(true)
.same_site(SameSite::Strict)
.finish(),
@@ -475,7 +474,7 @@ where
inner_payload,
)
.await
.map_err(|e| TcpError::BadRequest(format!("{:#?}", e)))?
.map_err(|e| TcpError::BadRequest(format!("{e:#?}")))?
.into_inner();
let user_id = &registration_start_request.username;
let user_is_admin = data

View File

@@ -299,14 +299,14 @@ impl PrivateKeyLocationOrFigment {
source: Some(figment::Source::Code(_)),
..
}) => PrivateKeyLocation::Default,
other => panic!("Unexpected config location: {:?}", other),
other => panic!("Unexpected config location: {other:?}"),
}
}
PrivateKeyLocationOrFigment::PrivateKeyLocation(PrivateKeyLocation::KeyFile(
config_location,
_,
)) => {
panic!("Unexpected location: {:?}", config_location)
panic!("Unexpected location: {config_location:?}")
}
PrivateKeyLocationOrFigment::PrivateKeyLocation(location) => location.clone(),
}
@@ -334,11 +334,11 @@ impl PrivateKeyLocationOrFigment {
source: Some(figment::Source::Code(_)),
..
}) => PrivateKeyLocation::Default,
other => panic!("Unexpected config location: {:?}", other),
other => panic!("Unexpected config location: {other:?}"),
}
}
PrivateKeyLocationOrFigment::PrivateKeyLocation(PrivateKeyLocation::KeySeed(file)) => {
panic!("Unexpected location: {:?}", file)
panic!("Unexpected location: {file:?}")
}
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(),
})
} 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 {
server_setup: ServerSetup::deserialize(&bytes).context(format!(
"while parsing the contents of the `{}` file",
file_path
"while parsing the contents of the `{file_path}` file"
))?,
private_key_location: private_key_location.for_key_file(file_path),
})
} else {
let server_setup = generate_random_private_key();
write_to_readonly_file(path, &server_setup.serialize()).context(format!(
"Could not write the generated server setup to file `{}`",
file_path,
"Could not write the generated server setup to file `{file_path}`",
))?;
Ok(ServerSetupConfig {
server_setup,
@@ -596,7 +594,7 @@ where
.iter()
.filter(|k| !expected_keys.contains(k.as_str()))
.for_each(|k| {
eprintln!("WARNING: Unknown environment variable: LLDAP_{}", k);
eprintln!("WARNING: Unknown environment variable: LLDAP_{k}");
});
}
config.server_setup = Some(get_server_setup(

View File

@@ -29,7 +29,7 @@ impl std::fmt::Debug for DatabaseUrl {
let mut url = self.0.clone();
// It can fail for URLs that cannot have a password, like "mailto:bob@example".
let _ = url.set_password(Some("***PASSWORD***"));
f.write_fmt(format_args!(r#""{}""#, url))
f.write_fmt(format_args!(r#""{url}""#))
} else {
f.write_fmt(format_args!(r#""{}""#, self.0))
}
@@ -44,7 +44,7 @@ mod tests {
fn test_database_url_debug() {
let url = DatabaseUrl::from("postgres://user:pass@localhost:5432/dbname");
assert_eq!(
format!("{:?}", url),
format!("{url:?}"),
r#""postgres://user:***PASSWORD***@localhost:5432/dbname""#
);
assert_eq!(

View File

@@ -71,7 +71,7 @@ where
#[instrument(level = "info", err)]
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 {
@@ -152,7 +152,7 @@ pub async fn check_ldaps(ldaps_options: &LdapsOptions) -> Result<()> {
#[instrument(level = "info", err)]
pub async fn check_api(port: u16) -> Result<()> {
reqwest::get(format!("http://localhost:{}/health", port))
reqwest::get(format!("http://localhost:{port}/health"))
.await?
.error_for_status()?;
info!("Success");

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")))
})
.with_context(|| {
format!(
"Cannot read either PKCS1, PKCS8 or EC private key from {}",
key_file
)
format!("Cannot read either PKCS1, PKCS8 or EC private key from {key_file}")
})
.map(rustls::PrivateKey)
}

View File

@@ -93,15 +93,14 @@ pub async fn send_password_reset_email(
.unwrap()
.extend(["reset-password", "step2", token]);
let body = format!(
"Hello {},
"Hello {username},
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
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.",
username, reset_url
Please contact an administrator if you did not initiate the process."
);
let res = send_email(
to,

View File

@@ -55,8 +55,7 @@ async fn create_admin_user(handler: &SqlBackendHandler, config: &Configuration)
.len();
assert!(
pass_length >= 8,
"Minimum password length is 8 characters, got {} characters",
pass_length
"Minimum password length is 8 characters, got {pass_length} characters"
);
handler
.create_user(CreateUserRequest {
@@ -97,7 +96,7 @@ async fn ensure_group_exists(handler: &SqlBackendHandler, group_name: &str) -> R
..Default::default()
})
.await
.context(format!("while creating {} group", group_name))?;
.context(format!("while creating {group_name} group"))?;
}
Ok(())
}

View File

@@ -169,8 +169,7 @@ impl TcpBackendHandler for SqlBackendHandler {
.await?;
if result.rows_affected == 0 {
return Err(DomainError::EntityNotFound(format!(
"No such password reset token: '{}'",
token
"No such password reset token: '{token}'"
)));
}
Ok(())

View File

@@ -14,13 +14,13 @@ pub fn database_url() -> String {
pub fn ldap_url() -> String {
let port = var("LLDAP_LDAP_PORT").ok();
let port = port.unwrap_or("3890".to_string());
format!("ldap://localhost:{}", port)
format!("ldap://localhost:{port}")
}
pub fn http_url() -> String {
let port = var("LLDAP_HTTP_PORT").ok();
let port = port.unwrap_or("17170".to_string());
format!("http://localhost:{}", port)
format!("http://localhost:{port}")
}
pub fn admin_dn() -> String {

View File

@@ -102,7 +102,7 @@ impl LLDAPFixture {
create_user::Variables {
user: create_user::CreateUserInput {
id: user.clone(),
email: Some(format!("{}@lldap.test", user)),
email: Some(format!("{user}@lldap.test")),
avatar: None,
display_name: None,
first_name: None,
@@ -181,11 +181,11 @@ impl Drop for LLDAPFixture {
Signal::SIGTERM,
);
if let Err(err) = result {
println!("Failed to send kill signal: {:?}", err);
println!("Failed to send kill signal: {err:?}");
let _ = self
.child
.kill()
.map_err(|err| println!("Failed to kill LLDAP: {:?}", err));
.map_err(|err| println!("Failed to kill LLDAP: {err:?}"));
return;
}
@@ -193,10 +193,7 @@ impl Drop for LLDAPFixture {
let status = self.child.try_wait();
match status {
Err(e) => {
println!(
"Failed to get status while waiting for graceful exit: {}",
e
);
println!("Failed to get status while waiting for graceful exit: {e}");
break;
}
Ok(None) => {
@@ -204,7 +201,7 @@ impl Drop for LLDAPFixture {
}
Ok(Some(status)) => {
if !status.success() {
println!("LLDAP exited with status {}", status)
println!("LLDAP exited with status {status}")
}
return;
}
@@ -215,7 +212,7 @@ impl Drop for LLDAPFixture {
let _ = self
.child
.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 = format!("{}-lldap-test", id.simple());
match prefix {
Some(prefix) => format!("{}{}", prefix, id),
Some(prefix) => format!("{prefix}{id}"),
None => id,
}
}

View File

@@ -103,7 +103,7 @@ where
})
};
let url = env::http_url() + "/api/graphql";
let auth_header = format!("Bearer {}", token);
let auth_header = format!("Bearer {token}");
client
.post(url)
.header(reqwest::header::AUTHORIZATION, auth_header)

View File

@@ -31,13 +31,13 @@ fn gitea() {
ldap.simple_bind(bind_dn.as_str(), env::admin_password().as_str())
.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 results = ldap
.search(
user_base.as_str(),
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,
)
.expect("failed to find gitea users")

View File

@@ -86,7 +86,7 @@ fn admin_search() {
ldap.search(
env::base_dn().as_str(),
Scope::Subtree,
format!("(&(objectclass=person)(uid={}))", admin_name).as_str(),
format!("(&(objectclass=person)(uid={admin_name}))").as_str(),
attrs,
)
.expect("failed to find admin"),
@@ -97,7 +97,7 @@ fn admin_search() {
found_users
.get(&admin_name)
.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");
}

View File

@@ -26,7 +26,7 @@ features = ["opaque_client"]
[dependencies.reqwest]
version = "*"
default-features = false
features = ["json", "blocking", "rustls-tls"]
features = ["json", "blocking", "rustls-tls", "rustls-tls-native-roots"]
[dependencies.serde]
workspace = true

View File

@@ -1,3 +1,5 @@
use std::env;
use anyhow::{Context, Result, bail, ensure};
use clap::Parser;
use lldap_auth::{opaque, registration};
@@ -27,9 +29,9 @@ pub struct CliOpts {
#[clap(short, long)]
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)]
pub password: String,
pub password: Option<String>,
/// Bypass password requirements such as minimum length. Unsafe.
#[clap(long)]
@@ -100,8 +102,14 @@ pub fn register_finish(
fn main() -> Result<()> {
let opts = CliOpts::parse();
let password = match opts.password {
Some(v) => v,
None => env::var("LLDAP_USER_PASSWORD").unwrap_or_default(),
};
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"
);
ensure!(
@@ -118,7 +126,7 @@ fn main() -> Result<()> {
let mut rng = rand::rngs::OsRng;
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")?;
let start_request = registration::ClientRegistrationStartRequest {
username: opts.username.clone().into(),