mirror of
https://github.com/lldap/lldap.git
synced 2026-06-18 15:56:27 +00:00
Compare commits
23 Commits
copilot/fi
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02572bdf45 | ||
|
|
30cb4ceae0 | ||
|
|
2e2a67c13b | ||
|
|
4d8fccb502 | ||
|
|
cd2694d7dc | ||
|
|
5e83ed8eb0 | ||
|
|
c69957690e | ||
|
|
7ef2af8beb | ||
|
|
5c9897b156 | ||
|
|
0b720aa082 | ||
|
|
3e7277e77d | ||
|
|
5241626a3a | ||
|
|
363ef106e2 | ||
|
|
3c7e4c3dec | ||
|
|
fa196a9fd9 | ||
|
|
f02b365478 | ||
|
|
0b0e6ae2cd | ||
|
|
da525fc99b | ||
|
|
78337bce72 | ||
|
|
87e9311a44 | ||
|
|
53e62ecf5a | ||
|
|
10d33a7537 | ||
|
|
ada438398e |
@@ -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
159
.github/copilot-instructions.md
vendored
Normal 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
26
.github/copilot-setup-steps.yml
vendored
Normal 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
|
||||
15
.github/workflows/Dockerfile.ci.alpine
vendored
15
.github/workflows/Dockerfile.ci.alpine
vendored
@@ -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; \
|
||||
|
||||
24
.github/workflows/Dockerfile.ci.debian
vendored
24
.github/workflows/Dockerfile.ci.debian
vendored
@@ -1,12 +1,15 @@
|
||||
FROM localhost:5000/lldap/lldap:debian-base
|
||||
# Taken directly from https://github.com/tianon/gosu/blob/master/INSTALL.md
|
||||
ENV GOSU_VERSION 1.17
|
||||
ENV GOSU_VERSION=1.17
|
||||
RUN set -eux; \
|
||||
# save list of currently installed packages for later so we can clean up
|
||||
savedAptMark="$(apt-mark showmanual)"; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends ca-certificates gnupg wget; \
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
for i in 1 2 3; do \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends wget ca-certificates gnupg && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* && break || sleep 5; \
|
||||
done; \
|
||||
\
|
||||
dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
|
||||
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
|
||||
@@ -14,7 +17,18 @@ RUN set -eux; \
|
||||
\
|
||||
# verify the signature
|
||||
export GNUPGHOME="$(mktemp -d)"; \
|
||||
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
|
||||
for server in \
|
||||
hkps://keys.openpgp.org \
|
||||
ha.pool.sks-keyservers.net \
|
||||
hkp://p80.pool.sks-keyservers.net:80 \
|
||||
keyserver.ubuntu.com \
|
||||
hkp://keyserver.ubuntu.com:80 \
|
||||
pgp.mit.edu \
|
||||
; do \
|
||||
if gpg --batch --keyserver "$server" --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; then \
|
||||
break; \
|
||||
fi; \
|
||||
done; \
|
||||
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
|
||||
gpgconf --kill all; \
|
||||
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
|
||||
|
||||
2
.github/workflows/Dockerfile.dev
vendored
2
.github/workflows/Dockerfile.dev
vendored
@@ -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"
|
||||
|
||||
8
.github/workflows/docker-build-static.yml
vendored
8
.github/workflows/docker-build-static.yml
vendored
@@ -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
|
||||
|
||||
41
.github/workflows/rust.yml
vendored
41
.github/workflows/rust.yml
vendored
@@ -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
|
||||
|
||||
|
||||
55
CHANGELOG.md
55
CHANGELOG.md
@@ -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
9
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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:#}"#),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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:#}"#),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:#?}"),
|
||||
)],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}"),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()]
|
||||
|
||||
@@ -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
|
||||
- [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)
|
||||
|
||||
39
example_configs/authelia.md
Normal file
39
example_configs/authelia.md
Normal 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
|
||||
```
|
||||
@@ -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))"
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = ®istration_start_request.username;
|
||||
let user_is_admin = data
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user