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