2 Commits

Author SHA1 Message Date
Valentin Tolmer
8c052c091e fix hipb typo 2023-04-13 09:32:53 +02:00
Valentin Tolmer
278fb1630d server: implement haveibeenpwned endpoint
See #39.
2023-04-04 20:34:37 +02:00
58 changed files with 707 additions and 2152 deletions

View File

@@ -34,20 +34,12 @@ package.json
.vscode
.devcontainer
# Created databases
*.db
*.db-shm
*.db-wal
# These are backup files generated by rustfmt
**/*.rs.bk
# Various config files that shouldn't be tracked
.env
lldap_config.toml
server_key
users.db*
screenshot.png
recipe.json
lldap_config.toml
cert.pem
key.pem

2
.github/codecov.yml vendored
View File

@@ -10,5 +10,3 @@ ignore:
- "docs"
- "example_configs"
- "migration-tool"
- "scripts"
- "set-password"

View File

@@ -11,10 +11,10 @@ RUN mkdir -p /lldap/app
RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \
mv bin/x86_64-unknown-linux-musl-lldap-bin/lldap target/lldap && \
mv bin/x86_64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/x86_64-unknown-linux-musl-migration-tool-bin/migration-tool target/migration-tool && \
mv bin/x86_64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/migration-tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
@@ -22,10 +22,10 @@ RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \
RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
mv bin/aarch64-unknown-linux-musl-lldap-bin/lldap target/lldap && \
mv bin/aarch64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/aarch64-unknown-linux-musl-migration-tool-bin/migration-tool target/migration-tool && \
mv bin/aarch64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/migration-tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
@@ -33,10 +33,10 @@ RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
RUN if [ "${TARGETPLATFORM}" = "linux/arm/v7" ]; then \
mv bin/armv7-unknown-linux-gnueabihf-lldap-bin/lldap target/lldap && \
mv bin/armv7-unknown-linux-gnueabihf-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/armv7-unknown-linux-gnueabihf-migration-tool-bin/migration-tool target/migration-tool && \
mv bin/armv7-unknown-linux-gnueabihf-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/migration-tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
@@ -47,7 +47,7 @@ COPY docker-entrypoint.sh /docker-entrypoint.sh
COPY lldap_config.docker_template.toml /lldap/
COPY web/index_local.html web/index.html
RUN cp target/lldap /lldap/ && \
cp target/lldap_migration_tool /lldap/ && \
cp target/migration-tool /lldap/ && \
cp target/lldap_set_password /lldap/ && \
cp -R web/index.html \
web/pkg \

View File

@@ -11,10 +11,10 @@ RUN mkdir -p /lldap/app
RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \
mv bin/x86_64-unknown-linux-musl-lldap-bin/lldap target/lldap && \
mv bin/x86_64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/x86_64-unknown-linux-musl-migration-tool-bin/migration-tool target/migration-tool && \
mv bin/x86_64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/migration-tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
@@ -22,10 +22,10 @@ RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \
RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
mv bin/aarch64-unknown-linux-musl-lldap-bin/lldap target/lldap && \
mv bin/aarch64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/aarch64-unknown-linux-musl-migration-tool-bin/migration-tool target/migration-tool && \
mv bin/aarch64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/migration-tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
@@ -33,10 +33,10 @@ RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
RUN if [ "${TARGETPLATFORM}" = "linux/arm/v7" ]; then \
mv bin/armv7-unknown-linux-gnueabihf-lldap-bin/lldap target/lldap && \
mv bin/armv7-unknown-linux-gnueabihf-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/armv7-unknown-linux-gnueabihf-migration-tool-bin/migration-tool target/migration-tool && \
mv bin/armv7-unknown-linux-gnueabihf-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/migration-tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
@@ -47,7 +47,7 @@ COPY docker-entrypoint.sh /docker-entrypoint.sh
COPY lldap_config.docker_template.toml /lldap/
COPY web/index_local.html web/index.html
RUN cp target/lldap /lldap/ && \
cp target/lldap_migration_tool /lldap/ && \
cp target/migration-tool /lldap/ && \
cp target/lldap_set_password /lldap/ && \
cp -R web/index.html \
web/pkg \

View File

@@ -84,12 +84,12 @@ jobs:
build-ui:
runs-on: ubuntu-latest
needs: pre_job
if: ${{ needs.pre_job.outputs.should_skip != 'true' || github.event_name == 'release' }}
if: ${{ needs.pre_job.outputs.should_skip != 'true' }}
container:
image: nitnelave/rust-dev:latest
steps:
- name: Checkout repository
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
- uses: actions/cache@v3
with:
path: |
@@ -123,7 +123,7 @@ jobs:
build-bin:
runs-on: ubuntu-latest
needs: pre_job
if: ${{ needs.pre_job.outputs.should_skip != 'true' || github.event_name == 'release' }}
if: ${{ needs.pre_job.outputs.should_skip != 'true' }}
strategy:
matrix:
target: [armv7-unknown-linux-gnueabihf, aarch64-unknown-linux-musl, x86_64-unknown-linux-musl]
@@ -138,7 +138,7 @@ jobs:
CARGO_HOME: ${GITHUB_WORKSPACE}/.cargo
steps:
- name: Checkout repository
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
- uses: actions/cache@v3
with:
path: |
@@ -151,7 +151,7 @@ jobs:
restore-keys: |
lldap-bin-${{ matrix.target }}-
- name: Compile ${{ matrix.target }} lldap and tools
run: cargo build --target=${{ matrix.target }} --release -p lldap -p lldap_migration_tool -p lldap_set_password
run: cargo build --target=${{ matrix.target }} --release -p lldap -p migration-tool -p lldap_set_password
- name: Check path
run: ls -al target/release
- name: Upload ${{ matrix.target}} lldap artifacts
@@ -162,8 +162,8 @@ jobs:
- name: Upload ${{ matrix.target }} migration tool artifacts
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.target }}-lldap_migration_tool-bin
path: target/${{ matrix.target }}/release/lldap_migration_tool
name: ${{ matrix.target }}-migration-tool-bin
path: target/${{ matrix.target }}/release/migration-tool
- name: Upload ${{ matrix.target }} password tool artifacts
uses: actions/upload-artifact@v3
with:
@@ -180,13 +180,11 @@ jobs:
ports:
- 3306:3306
env:
MARIADB_USER: lldapuser
MARIADB_PASSWORD: lldappass
MARIADB_DATABASE: lldap
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 1
options: >-
--name mariadb
--health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
MYSQL_USER: lldapuser
MYSQL_PASSWORD: lldappass
MYSQL_DATABASE: lldap
MYSQL_ROOT_PASSWORD: rootpass
options: --name mariadb
postgresql:
image: postgres:latest
@@ -196,12 +194,7 @@ jobs:
POSTGRES_USER: lldapuser
POSTGRES_PASSWORD: lldappass
POSTGRES_DB: lldap
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
--name postgresql
options: --name postgresql
steps:
- name: Download artifacts
@@ -263,27 +256,17 @@ jobs:
POSTGRES_USER: lldapuser
POSTGRES_PASSWORD: lldappass
POSTGRES_DB: lldap
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
--name postgresql
options: --name postgresql
mariadb:
image: mariadb:latest
ports:
- 3306:3306
env:
MARIADB_USER: lldapuser
MARIADB_PASSWORD: lldappass
MARIADB_DATABASE: lldap
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 1
options: >-
--name mariadb
--health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
MYSQL_USER: lldapuser
MYSQL_PASSWORD: lldappass
MYSQL_DATABASE: lldap
MYSQL_ROOT_PASSWORD: rootpass
options: --name mariadb
mysql:
image: mysql:latest
ports:
@@ -292,10 +275,8 @@ jobs:
MYSQL_USER: lldapuser
MYSQL_PASSWORD: lldappass
MYSQL_DATABASE: lldap
MYSQL_ALLOW_EMPTY_PASSWORD: 1
options: >-
--name mysql
--health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
MYSQL_ROOT_PASSWORD: rootpass
options: --name mysql
steps:
@@ -333,7 +314,7 @@ jobs:
- name: Create dummy user
run: |
TOKEN=$(curl -X POST -H "Content-Type: application/json" -d '{"username": "admin", "password": "ldappass"}' http://localhost:17170/auth/simple/login | jq -r .token)
TOKEN=$(curl -X POST -H "Content-Type: application/json" -d '{"username": "admin", "password": "ldappass"}' http://localhost:17170/auth/simple/login | cut -c 11-277)
echo "$TOKEN"
curl 'http://localhost:17170/api/graphql' -H 'Content-Type: application/json' -H "Authorization: Bearer ${TOKEN//[$'\t\r\n ']}" --data-binary '{"query":"mutation{\n createUser(user:\n {\n id: \"dummyuser\",\n email: \"dummyuser@example.com\"\n }\n )\n {\n id\n email\n }\n}\n\n\n"}' --compressed
bin/lldap_set_password --base-url http://localhost:17170 --admin-username admin --admin-password ldappass --token $TOKEN --username dummyuser --password dummypassword
@@ -347,7 +328,7 @@ jobs:
- name: Export and Converting to Postgress
run: |
curl -L https://raw.githubusercontent.com/lldap/lldap/main/scripts/sqlite_dump_commands.sh -o helper.sh
curl -L https://raw.githubusercontent.com/nitnelave/lldap/main/scripts/sqlite_dump_commands.sh -o helper.sh
chmod +x ./helper.sh
./helper.sh | sqlite3 ./users.db > ./dump.sql
sed -i -r -e "s/X'([[:xdigit:]]+'[^'])/'\\\x\\1/g" -e '1s/^/BEGIN;\n/' -e '$aCOMMIT;' ./dump.sql
@@ -365,7 +346,7 @@ jobs:
- name: Export and Converting to mariadb
run: |
curl -L https://raw.githubusercontent.com/lldap/lldap/main/scripts/sqlite_dump_commands.sh -o helper.sh
curl -L https://raw.githubusercontent.com/nitnelave/lldap/main/scripts/sqlite_dump_commands.sh -o helper.sh
chmod +x ./helper.sh
./helper.sh | sqlite3 ./users.db > ./dump.sql
cp ./dump.sql ./dump-no-sed.sql
@@ -384,7 +365,7 @@ jobs:
- name: Export and Converting to mysql
run: |
curl -L https://raw.githubusercontent.com/lldap/lldap/main/scripts/sqlite_dump_commands.sh -o helper.sh
curl -L https://raw.githubusercontent.com/nitnelave/lldap/main/scripts/sqlite_dump_commands.sh -o helper.sh
chmod +x ./helper.sh
./helper.sh | sqlite3 ./users.db > ./dump.sql
sed -i -r -e 's/^INSERT INTO "?([a-zA-Z0-9_]+)"?/INSERT INTO `\1`/' -e '1s/^/START TRANSACTION;\n/' -e '$aCOMMIT;' ./dump.sql
@@ -444,40 +425,12 @@ jobs:
needs: [build-ui, build-bin]
name: Build Docker image
runs-on: ubuntu-latest
strategy:
matrix:
container: ["debian","alpine"]
include:
- container: alpine
platforms: linux/amd64,linux/arm64
tags: |
type=ref,event=pr
type=semver,pattern=v{{version}}
type=semver,pattern=v{{major}}
type=semver,pattern=v{{major}}.{{minor}}
type=semver,pattern=v{{version}},suffix=
type=semver,pattern=v{{major}},suffix=
type=semver,pattern=v{{major}}.{{minor}},suffix=
type=raw,value=latest,enable={{ is_default_branch }}
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }},suffix=
type=raw,value=latest,enable={{ is_default_branch }},suffix=
- container: debian
platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: |
type=ref,event=pr
type=semver,pattern=v{{version}}
type=semver,pattern=v{{major}}
type=semver,pattern=v{{major}}.{{minor}}
type=raw,value=latest,enable={{ is_default_branch }}
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
- name: Download all artifacts
uses: actions/download-artifact@v3
with:
@@ -493,66 +446,86 @@ jobs:
uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
- name: Docker ${{ matrix.container }} meta
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
# list of Docker images to use as base name for tags
images: |
nitnelave/lldap
lldap/lldap
ghcr.io/lldap/lldap
# Wanted Docker tags
# vX-alpine
# vX.Y-alpine
# vX.Y.Z-alpine
# latest
# latest-alpine
# stable
# stable-alpine
#################
# vX-debian
# vX.Y-debian
# vX.Y.Z-debian
# latest-debian
# stable-debian
#################
# Check matrix for tag list definition
flavor: |
latest=false
suffix=-${{ matrix.container }}
tags: ${{ matrix.tags }}
# generate Docker tags based on the following events/attributes
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
# Docker login to nitnelave/lldap and lldap/lldap
- name: Login to Nitnelave/LLDAP Docker Hub
- name: parse tag
uses: gacts/github-slug@v1
id: slug
- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
registry: ghcr.io
username: nitnelave
password: ${{ secrets.GITHUB_TOKEN }}
########################################
#### docker image build ####
#### docker image :latest tag build ####
########################################
- name: Build ${{ matrix.container }} Docker Image
- name: Build and push latest alpine
if: github.event_name != 'release'
uses: docker/build-push-action@v4
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: ${{ matrix.platforms }}
file: ./.github/workflows/Dockerfile.ci.${{ matrix.container }}
tags: |
${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
file: ./.github/workflows/Dockerfile.ci.alpine
tags: nitnelave/lldap:latest, nitnelave/lldap:latest-alpine
cache-from: type=gha,mode=max
cache-to: type=gha,mode=max
- name: Build and push latest debian
if: github.event_name != 'release'
uses: docker/build-push-action@v4
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
file: ./.github/workflows/Dockerfile.ci.debian
tags: nitnelave/lldap:latest-debian
cache-from: type=gha,mode=max
cache-to: type=gha,mode=max
########################################
#### docker image :semver tag build ####
########################################
- name: Build and push release alpine
if: github.event_name == 'release'
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
# Tag as latest, stable, semver, major, major.minor and major.minor.patch.
file: ./.github/workflows/Dockerfile.ci.alpine
tags: nitnelave/lldap:stable, nitnelave/lldap:stable-alpine, nitnelave/lldap:v${{ steps.slug.outputs.version-semantic }}, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}.${{ steps.slug.outputs.version-patch }}, nitnelave/lldap:v${{ steps.slug.outputs.version-semantic }}-alpine, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}-alpine, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}-alpine.${{ steps.slug.outputs.version-minor }}-alpine, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}.${{ steps.slug.outputs.version-patch }}-alpine
cache-from: type=gha,mode=max
cache-to: type=gha,mode=max
- name: Build and push release debian
if: github.event_name == 'release'
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
# Tag as latest, stable, semver, major, major.minor and major.minor.patch.
file: ./.github/workflows/Dockerfile.ci.debian
tags: nitnelave/lldap:stable-debian, nitnelave/lldap:v${{ steps.slug.outputs.version-semantic }}-debian, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}-debian, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}-debian, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}.${{ steps.slug.outputs.version-patch }}-debian
cache-from: type=gha,mode=max
cache-to: type=gha,mode=max
@@ -564,14 +537,6 @@ jobs:
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repository: nitnelave/lldap
- name: Update lldap repo description
if: github.event_name != 'pull_request'
uses: peter-evans/dockerhub-description@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repository: lldap/lldap
###############################################################
### Download artifacts, clean up ui, upload to release page ###
###############################################################
@@ -594,14 +559,14 @@ jobs:
mv bin/aarch64-unknown-linux-musl-lldap-bin/lldap bin/aarch64-lldap
mv bin/x86_64-unknown-linux-musl-lldap-bin/lldap bin/amd64-lldap
mv bin/armv7-unknown-linux-gnueabihf-lldap-bin/lldap bin/armhf-lldap
mv bin/aarch64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool bin/aarch64-lldap_migration_tool
mv bin/x86_64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool bin/amd64-lldap_migration_tool
mv bin/armv7-unknown-linux-gnueabihf-lldap_migration_tool-bin/lldap_migration_tool bin/armhf-lldap_migration_tool
mv bin/aarch64-unknown-linux-musl-migration-tool-bin/migration-tool bin/aarch64-migration-tool
mv bin/x86_64-unknown-linux-musl-migration-tool-bin/migration-tool bin/amd64-migration-tool
mv bin/armv7-unknown-linux-gnueabihf-migration-tool-bin/migration-tool bin/armhf-migration-tool
mv bin/aarch64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password bin/aarch64-lldap_set_password
mv bin/x86_64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password bin/amd64-lldap_set_password
mv bin/armv7-unknown-linux-gnueabihf-lldap_set_password-bin/lldap_set_password bin/armhf-lldap_set_password
chmod +x bin/*-lldap
chmod +x bin/*-lldap_migration_tool
chmod +x bin/*-migration-tool
chmod +x bin/*-lldap_set_password
- name: Download llap ui artifacts
@@ -627,9 +592,9 @@ jobs:
mv bin/aarch64-lldap aarch64-lldap/lldap
mv bin/amd64-lldap amd64-lldap/lldap
mv bin/armhf-lldap armhf-lldap/lldap
mv bin/aarch64-lldap_migration_tool aarch64-lldap/lldap_migration_tool
mv bin/amd64-lldap_migration_tool amd64-lldap/lldap_migration_tool
mv bin/armhf-lldap_migration_tool armhf-lldap/lldap_migration_tool
mv bin/aarch64-migration-tool aarch64-lldap/migration-tool
mv bin/amd64-migration-tool amd64-lldap/migration-tool
mv bin/armhf-migration-tool armhf-lldap/migration-tool
mv bin/aarch64-lldap_set_password aarch64-lldap/lldap_set_password
mv bin/amd64-lldap_set_password amd64-lldap/lldap_set_password
mv bin/armhf-lldap_set_password armhf-lldap/lldap_set_password

View File

@@ -33,7 +33,7 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
- uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --verbose --workspace
@@ -52,7 +52,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
- uses: Swatinem/rust-cache@v2
@@ -69,7 +69,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
- uses: Swatinem/rust-cache@v2
@@ -86,7 +86,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.0
- name: Install Rust
run: rustup toolchain install nightly --component llvm-tools-preview && rustup component add llvm-tools-preview --toolchain stable-x86_64-unknown-linux-gnu
@@ -101,39 +101,7 @@ jobs:
run: cargo llvm-cov --no-run --lcov --output-path lcov.info
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
if: github.ref != 'refs/heads/main' || github.event_name != 'push'
with:
files: lcov.info
fail_ci_if_error: true
- name: Upload coverage to Codecov (main)
uses: codecov/codecov-action@v3
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
with:
files: lcov.info
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
publish-crates:
name: Publish on crates.io
if: ${{ needs.pre_job.outputs.should_skip != 'true' || (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'release' }}
needs: pre_job
strategy:
fail-fast: false
matrix:
target: [lldap_auth, lldap, lldap_app, lldap_set_password, lldap_migration_tool]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.5.2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Publish ${{ matrix.target }} crate
uses: katyo/publish-crates@v2
with:
args: -p ${{ matrix.target }}
dry-run: ${{ github.event_name != 'release' }}
check-repo: ${{ github.event_name != 'pull_request' }}
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
ignore-unpublished-changes: ${{ github.event_name != 'release' }}

View File

@@ -5,35 +5,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.4.3] 2023-04-11
The repository has changed from `nitnelave/lldap` to `lldap/lldap`, both on GitHub
and on DockerHub (although we will keep publishing the images to
`nitnelave/lldap` for the foreseeable future). All data on GitHub has been
migrated, and the new docker images are available both on DockerHub and on the
GHCR under `lldap/lldap`.
### Added
- EC private keys are not supported for LDAPS.
### Changed
- SMTP user no longer has a default value (and instead defaults to unauthenticated).
### Fixed
- WASM payload is now delivered uncompressed to Safari due to a Safari bug.
- Password reset no longer redirects to login page.
- NextCloud config should add the "mail" attribute.
- GraphQL parameters are now urldecoded, to support special characters in usernames.
- Healthcheck correctly checks the server certificate.
### New services
- Home Assistant
- Shaarli
## [0.4.2] - 2023-03-27
### Added

381
Cargo.lock generated
View File

@@ -118,7 +118,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6"
dependencies = [
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -189,7 +189,7 @@ dependencies = [
"pin-project-lite",
"tokio-rustls",
"tokio-util",
"webpki-roots 0.22.6",
"webpki-roots",
]
[[package]]
@@ -252,7 +252,7 @@ dependencies = [
"actix-router",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -278,7 +278,7 @@ checksum = "6d44b8fee1ced9671ba043476deddef739dd0959bf77030b26b738cc591737a7"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -400,7 +400,7 @@ checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
"synstructure",
]
@@ -412,21 +412,7 @@ checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "assert_cmd"
version = "2.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9834fcc22e0874394a010230586367d4a3e9f11b560f469262678547e1d2575e"
dependencies = [
"bstr",
"doc-comment",
"predicates",
"predicates-core",
"predicates-tree",
"wait-timeout",
"syn",
]
[[package]]
@@ -448,7 +434,7 @@ checksum = "e4655ae1a7b0cdf149156f780c5bf3f1352bc53cbd9e0a361a7ef7b22947e965"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -459,7 +445,7 @@ checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -511,7 +497,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -620,18 +606,6 @@ dependencies = [
"uuid 0.8.2",
]
[[package]]
name = "bstr"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09"
dependencies = [
"memchr",
"once_cell",
"regex-automata",
"serde",
]
[[package]]
name = "bumpalo"
version = "3.12.0"
@@ -721,7 +695,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -984,7 +958,7 @@ dependencies = [
"proc-macro2",
"quote",
"scratch",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -1001,7 +975,7 @@ checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -1025,7 +999,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -1036,20 +1010,7 @@ checksum = "b36230598a2d5de7ec1c6f51f72d8a99a9208daff41de2084d06e3fd3ea56685"
dependencies = [
"darling_core",
"quote",
"syn 1.0.109",
]
[[package]]
name = "dashmap"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
dependencies = [
"cfg-if",
"hashbrown 0.12.3",
"lock_api",
"once_cell",
"parking_lot_core 0.9.7",
"syn",
]
[[package]]
@@ -1101,7 +1062,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -1111,7 +1072,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e"
dependencies = [
"derive_builder_core",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -1124,7 +1085,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -1135,7 +1096,7 @@ checksum = "532b4c15dccee12c7044f1fcad956e98410860b22231e44a3b827464797ca7bf"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -1192,7 +1153,7 @@ checksum = "adc2ab4d5a16117f9029e9a6b5e4e79f4c67f6519bc134210d4d4a04ba31f41b"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -1203,15 +1164,9 @@ checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "dotenvy"
version = "0.15.6"
@@ -1300,7 +1255,7 @@ checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
"synstructure",
]
@@ -1394,16 +1349,6 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa"
[[package]]
name = "fslock"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "futures"
version = "0.3.26"
@@ -1443,7 +1388,7 @@ checksum = "3422d14de7903a52e9dbc10ae05a7e14445ec61890100e098754e120b2bd7b1e"
dependencies = [
"derive_utils",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -1482,7 +1427,7 @@ checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -1773,7 +1718,7 @@ dependencies = [
"quote",
"serde",
"serde_json",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -1790,7 +1735,7 @@ dependencies = [
"quote",
"serde",
"serde_json",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -1801,7 +1746,7 @@ checksum = "e56b093bfda71de1da99758b036f4cc811fd2511c8a76f75680e9ffbd2bb4251"
dependencies = [
"graphql_client_codegen 0.10.0",
"proc-macro2",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -1812,7 +1757,7 @@ checksum = "a755cc59cda2641ea3037b4f9f7ef40471c329f55c1fa2db6fa0bb7ae6c1f7ce"
dependencies = [
"graphql_client_codegen 0.11.0",
"proc-macro2",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -2217,7 +2162,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -2323,7 +2268,7 @@ dependencies = [
"peg",
"tokio-util",
"tracing",
"uuid 1.3.1",
"uuid 1.3.0",
]
[[package]]
@@ -2351,7 +2296,7 @@ dependencies = [
"socket2",
"tokio",
"tokio-rustls",
"webpki-roots 0.22.6",
"webpki-roots",
]
[[package]]
@@ -2400,7 +2345,7 @@ checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
[[package]]
name = "lldap"
version = "0.5.0-alpha"
version = "0.4.3-alpha"
dependencies = [
"actix",
"actix-files",
@@ -2412,7 +2357,6 @@ dependencies = [
"actix-web",
"actix-web-httpauth",
"anyhow",
"assert_cmd",
"async-trait",
"base64 0.21.0",
"bincode",
@@ -2424,7 +2368,6 @@ dependencies = [
"figment_file_provider_adapter",
"futures",
"futures-util",
"graphql_client 0.11.0",
"hmac 0.12.1",
"http",
"image",
@@ -2432,17 +2375,14 @@ dependencies = [
"juniper",
"jwt 0.16.0",
"lber 0.4.1",
"ldap3",
"ldap3_proto",
"lettre",
"lldap_auth 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lldap_auth",
"log",
"mockall",
"nix",
"opaque-ke",
"orion",
"rand 0.8.5",
"rand_chacha 0.3.1",
"reqwest",
"rustls",
"rustls-pemfile",
@@ -2451,7 +2391,6 @@ dependencies = [
"serde",
"serde_bytes",
"serde_json",
"serial_test",
"sha2 0.10.6",
"thiserror",
"time 0.3.19",
@@ -2465,14 +2404,13 @@ dependencies = [
"tracing-forest",
"tracing-log",
"tracing-subscriber",
"urlencoding",
"uuid 1.3.1",
"webpki-roots 0.23.0",
"uuid 1.3.0",
"webpki-roots",
]
[[package]]
name = "lldap_app"
version = "0.5.0-alpha"
version = "0.4.3-alpha"
dependencies = [
"anyhow",
"base64 0.13.1",
@@ -2480,18 +2418,20 @@ dependencies = [
"gloo-console",
"gloo-file",
"gloo-net",
"gloo-timers",
"graphql_client 0.10.0",
"http",
"image",
"indexmap",
"jwt 0.13.0",
"lldap_auth 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lldap_auth",
"rand 0.8.5",
"serde",
"serde_json",
"sha1",
"url-escape",
"validator",
"validator_derive 0.16.0",
"validator_derive",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
@@ -2518,49 +2458,13 @@ dependencies = [
"thiserror",
]
[[package]]
name = "lldap_auth"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17867a28e09989643401bb7849a494b328416634a335ddf7d3dabc9806ba563"
dependencies = [
"chrono",
"curve25519-dalek",
"digest 0.9.0",
"generic-array",
"getrandom 0.2.8",
"opaque-ke",
"rand 0.8.5",
"rust-argon2",
"serde",
"sha2 0.9.9",
"thiserror",
]
[[package]]
name = "lldap_migration_tool"
version = "0.4.2"
dependencies = [
"anyhow",
"base64 0.13.1",
"graphql_client 0.11.0",
"ldap3",
"lldap_auth 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.8.5",
"requestty",
"reqwest",
"serde",
"serde_json",
"smallvec",
]
[[package]]
name = "lldap_set_password"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"lldap_auth 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lldap_auth",
"rand 0.8.5",
"reqwest",
"serde",
@@ -2635,12 +2539,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "memoffset"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
name = "migration-tool"
version = "0.4.2"
dependencies = [
"autocfg",
"anyhow",
"base64 0.13.1",
"graphql_client 0.11.0",
"ldap3",
"lldap_auth",
"rand 0.8.5",
"requestty",
"reqwest",
"serde",
"serde_json",
"smallvec",
]
[[package]]
@@ -2710,21 +2622,7 @@ dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "nix"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a"
dependencies = [
"bitflags",
"cfg-if",
"libc",
"memoffset",
"pin-utils",
"static_assertions",
"syn",
]
[[package]]
@@ -2934,7 +2832,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -3017,7 +2915,7 @@ dependencies = [
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -3079,7 +2977,7 @@ checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -3167,7 +3065,7 @@ dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
"version_check",
]
@@ -3184,9 +3082,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.54"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534"
checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
dependencies = [
"unicode-ident",
]
@@ -3199,16 +3097,16 @@ checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
"version_check",
"yansi",
]
[[package]]
name = "quote"
version = "1.0.26"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
dependencies = [
"proc-macro2",
]
@@ -3404,7 +3302,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots 0.22.6",
"webpki-roots",
"winreg",
]
@@ -3532,16 +3430,6 @@ dependencies = [
"base64 0.21.0",
]
[[package]]
name = "rustls-webpki"
version = "0.100.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.11"
@@ -3612,7 +3500,7 @@ dependencies = [
"thiserror",
"tracing",
"url",
"uuid 1.3.1",
"uuid 1.3.0",
]
[[package]]
@@ -3625,7 +3513,7 @@ dependencies = [
"heck 0.3.3",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -3636,7 +3524,7 @@ checksum = "d2fbe015dbdaa7d8829d71c1e14fb6289e928ac256b93dfda543c85cd89d6f03"
dependencies = [
"chrono",
"sea-query-derive",
"uuid 1.3.1",
"uuid 1.3.0",
]
[[package]]
@@ -3648,7 +3536,7 @@ dependencies = [
"chrono",
"sea-query",
"sqlx",
"uuid 1.3.1",
"uuid 1.3.0",
]
[[package]]
@@ -3660,7 +3548,7 @@ dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
"thiserror",
]
@@ -3683,7 +3571,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -3763,7 +3651,7 @@ checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -3790,30 +3678,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serial_test"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d"
dependencies = [
"dashmap",
"fslock",
"lazy_static",
"parking_lot 0.12.1",
"serial_test_derive",
]
[[package]]
name = "serial_test_derive"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.12",
]
[[package]]
name = "sha1"
version = "0.10.5"
@@ -4032,8 +3896,8 @@ dependencies = [
"thiserror",
"tokio-stream",
"url",
"uuid 1.3.1",
"webpki-roots 0.22.6",
"uuid 1.3.0",
"webpki-roots",
"whoami",
]
@@ -4051,7 +3915,7 @@ dependencies = [
"quote",
"sqlx-core",
"sqlx-rt",
"syn 1.0.109",
"syn",
"url",
]
@@ -4105,17 +3969,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79d9531f94112cfc3e4c8f5f02cb2b58f72c97b7efd85f70203cc6d8efda5927"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.12.6"
@@ -4124,7 +3977,7 @@ checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
"unicode-xid",
]
@@ -4185,7 +4038,7 @@ checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -4279,7 +4132,7 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -4355,7 +4208,7 @@ dependencies = [
"actix-web",
"pin-project",
"tracing",
"uuid 1.3.1",
"uuid 1.3.0",
]
[[package]]
@@ -4366,7 +4219,7 @@ checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -4542,12 +4395,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "urlencoding"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
[[package]]
name = "uuid"
version = "0.8.2"
@@ -4556,9 +4403,9 @@ checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
[[package]]
name = "uuid"
version = "1.3.1"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b55a3fef2a1e3b3a00ce878640918820d3c51081576ac657d23af9fc7928fdb"
checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
dependencies = [
"getrandom 0.2.8",
"md-5",
@@ -4577,7 +4424,7 @@ dependencies = [
"serde_derive",
"serde_json",
"url",
"validator_types 0.14.0",
"validator_types",
]
[[package]]
@@ -4592,24 +4439,8 @@ dependencies = [
"proc-macro2",
"quote",
"regex",
"syn 1.0.109",
"validator_types 0.14.0",
]
[[package]]
name = "validator_derive"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af"
dependencies = [
"if_chain",
"lazy_static",
"proc-macro-error",
"proc-macro2",
"quote",
"regex",
"syn 1.0.109",
"validator_types 0.16.0",
"syn",
"validator_types",
]
[[package]]
@@ -4619,17 +4450,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ded9d97e1d42327632f5f3bae6403c04886e2de3036261ef42deebd931a6a291"
dependencies = [
"proc-macro2",
"syn 1.0.109",
]
[[package]]
name = "validator_types"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3"
dependencies = [
"proc-macro2",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -4656,15 +4477,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "want"
version = "0.3.0"
@@ -4714,7 +4526,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
"wasm-bindgen-shared",
]
@@ -4748,7 +4560,7 @@ checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -4788,15 +4600,6 @@ dependencies = [
"webpki",
]
[[package]]
name = "webpki-roots"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa54963694b65584e170cf5dc46aeb4dcaa5584e652ff5f3952e56d66aff0125"
dependencies = [
"rustls-webpki",
]
[[package]]
name = "whoami"
version = "1.3.0"
@@ -4982,7 +4785,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -5013,7 +4816,7 @@ checksum = "39049d193b52eaad4ffc80916bf08806d142c90b5edcebd527644de438a7e19a"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
]
[[package]]
@@ -5023,7 +4826,7 @@ source = "git+https://github.com/jfbilodeau/yew_form?rev=4b9fabffb63393ec7626a44
dependencies = [
"gloo-console",
"validator",
"validator_derive 0.14.0",
"validator_derive",
"wasm-bindgen",
"web-sys",
"yew",
@@ -5036,7 +4839,7 @@ source = "git+https://github.com/jfbilodeau/yew_form?rev=4b9fabffb63393ec7626a44
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
"yew_form",
]
@@ -5057,7 +4860,7 @@ checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn",
"synstructure",
]

View File

@@ -31,12 +31,12 @@ FROM chef AS builder
COPY --from=planner /tmp/recipe.json recipe.json
RUN cargo chef cook --release -p lldap_app --target wasm32-unknown-unknown \
&& cargo chef cook --release -p lldap \
&& cargo chef cook --release -p lldap_migration_tool \
&& cargo chef cook --release -p migration-tool \
&& cargo chef cook --release -p lldap_set_password
# Copy the source and build the app and server.
COPY --chown=app:app . .
RUN cargo build --release -p lldap -p lldap_migration_tool -p lldap_set_password \
RUN cargo build --release -p lldap -p migration-tool -p lldap_set_password \
# Build the frontend.
&& ./app/build.sh
@@ -78,7 +78,7 @@ WORKDIR /app
COPY --from=builder /app/app/index_local.html app/index.html
COPY --from=builder /app/app/static app/static
COPY --from=builder /app/app/pkg app/pkg
COPY --from=builder /app/target/release/lldap /app/target/release/lldap_migration_tool /app/target/release/lldap_set_password ./
COPY --from=builder /app/target/release/lldap /app/target/release/migration-tool /app/target/release/lldap_set_password ./
COPY docker-entrypoint.sh lldap_config.docker_template.toml ./
RUN set -x \

View File

@@ -13,7 +13,6 @@
<a href="https://discord.gg/h5PEdRMNyP">
<img alt="Discord" src="https://img.shields.io/discord/898492935446876200?label=discord&logo=discord" />
</a>
<a href="https://twitter.com/nitnelave1?ref_src=twsrc%5Etfw">
<img
src="https://img.shields.io/twitter/follow/nitnelave1?style=social"
@@ -24,12 +23,8 @@
src="https://img.shields.io/badge/unsafe-forbidden-success.svg"
alt="Unsafe forbidden"/>
</a>
<a href="https://app.codecov.io/gh/lldap/lldap">
<img alt="Codecov" src="https://img.shields.io/codecov/c/github/lldap/lldap" />
</a>
<br/>
<a href="https://www.buymeacoffee.com/nitnelave" target="_blank">
<img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" >
<a href="https://app.codecov.io/gh/nitnelave/lldap">
<img alt="Codecov" src="https://img.shields.io/codecov/c/github/nitnelave/lldap" />
</a>
</p>
@@ -163,7 +158,7 @@ To compile the project, you'll need:
Then you can compile the server (and the migration tool if you want):
```shell
cargo build --release -p lldap -p lldap_migration_tool
cargo build --release -p lldap -p migration-tool
```
The resulting binaries will be in `./target/release/`. Alternatively, you can
@@ -262,7 +257,6 @@ folder for help with:
- [Dex](example_configs/dex_config.yml)
- [Dokuwiki](example_configs/dokuwiki.md)
- [Dolibarr](example_configs/dolibarr.md)
- [Ejabberd](example_configs/ejabberd.md)
- [Emby](example_configs/emby.md)
- [Gitea](example_configs/gitea.md)
- [Grafana](example_configs/grafana_ldap_config.toml)

View File

@@ -1,12 +1,8 @@
[package]
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
description = "Frontend for LLDAP"
edition = "2021"
homepage = "https://github.com/lldap/lldap"
license = "GPL-3.0-only"
name = "lldap_app"
repository = "https://github.com/lldap/lldap"
version = "0.5.0-alpha"
version = "0.4.3-alpha"
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
edition = "2021"
include = ["src/**/*", "queries/**/*", "Cargo.toml", "../schema.graphql"]
[dependencies]
@@ -14,7 +10,7 @@ anyhow = "1"
base64 = "0.13"
gloo-console = "0.2.3"
gloo-file = "0.2.3"
gloo-net = "0.2"
gloo-net = "*"
graphql_client = "0.10"
http = "0.2"
jwt = "0.13"
@@ -23,14 +19,16 @@ serde = "1"
serde_json = "1"
url-escape = "0.1.1"
validator = "=0.14"
validator_derive = "0.16"
sha1 = "*"
validator_derive = "*"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
wasm-bindgen-futures = "*"
yew = "0.19.3"
yew-router = "0.16"
# Needed because of https://github.com/tkaitchuck/aHash/issues/95
indexmap = "=1.6.2"
gloo-timers = "0.2.6"
[dependencies.web-sys]
version = "0.3"
@@ -47,13 +45,13 @@ features = [
]
[dependencies.chrono]
version = "0.4"
version = "*"
features = [
"wasmbind"
]
[dependencies.lldap_auth]
version = "0.3"
path = "../auth"
features = [ "opaque_client" ]
[dependencies.image]

View File

@@ -1,4 +1,5 @@
use crate::{
components::password_field::PasswordField,
components::router::{AppRoute, Link},
infra::{
api::HostService,
@@ -254,14 +255,12 @@ impl Component for ChangePasswordForm {
{":"}
</label>
<div class="col-sm-10">
<Field
<PasswordField<FormModel>
form={&self.form}
field_name="password"
input_type="password"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="new-password"
oninput={link.callback(|_| Msg::FormUpdate)} />
<div class="invalid-feedback">
{&self.form.field_message("password")}

View File

@@ -149,9 +149,9 @@ impl Component for LoginForm {
let link = &ctx.link();
if self.refreshing {
html! {
<div>
<img src={"spinner.gif"} alt={"Loading"} />
</div>
<div class="spinner-border" role="status">
<span class="sr-only">{"Loading..."}</span>
</div>
}
} else {
html! {

View File

@@ -10,6 +10,7 @@ pub mod group_details;
pub mod group_table;
pub mod login;
pub mod logout;
pub mod password_field;
pub mod remove_user_from_group;
pub mod reset_password_step1;
pub mod reset_password_step2;

View File

@@ -0,0 +1,152 @@
use crate::infra::{
api::{hash_password, HostService, PasswordHash, PasswordWasLeaked},
common_component::{CommonComponent, CommonComponentParts},
};
use anyhow::Result;
use gloo_timers::callback::Timeout;
use web_sys::{HtmlInputElement, InputEvent};
use yew::{html, Callback, Classes, Component, Context, Properties};
use yew_form::{Field, Form, Model};
pub enum PasswordFieldMsg {
OnInput(String),
OnInputIdle,
PasswordCheckResult(Result<(Option<PasswordWasLeaked>, PasswordHash)>),
}
#[derive(PartialEq)]
pub enum PasswordState {
// Whether the password was found in a leak.
Checked(PasswordWasLeaked),
// Server doesn't support checking passwords (TODO: move to config).
NotSupported,
// Requested a check, no response yet from the server.
Loading,
// User is still actively typing.
Typing,
}
pub struct PasswordField<FormModel: Model> {
common: CommonComponentParts<Self>,
timeout_task: Option<Timeout>,
password: String,
password_check_state: PasswordState,
_marker: std::marker::PhantomData<FormModel>,
}
impl<FormModel: Model> CommonComponent<PasswordField<FormModel>> for PasswordField<FormModel> {
fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> anyhow::Result<bool> {
match msg {
PasswordFieldMsg::OnInput(password) => {
self.password = password;
if self.password_check_state != PasswordState::NotSupported {
self.password_check_state = PasswordState::Typing;
if self.password.len() >= 8 {
let link = ctx.link().clone();
self.timeout_task = Some(Timeout::new(500, move || {
link.send_message(PasswordFieldMsg::OnInputIdle)
}));
}
}
}
PasswordFieldMsg::PasswordCheckResult(result) => {
self.timeout_task = None;
// If there's an error from the backend, don't retry.
self.password_check_state = PasswordState::NotSupported;
if let (Some(check), hash) = result? {
if hash == hash_password(&self.password) {
self.password_check_state = PasswordState::Checked(check)
}
}
}
PasswordFieldMsg::OnInputIdle => {
self.timeout_task = None;
if self.password_check_state != PasswordState::NotSupported {
self.password_check_state = PasswordState::Loading;
self.common.call_backend(
ctx,
HostService::check_password_haveibeenpwned(hash_password(&self.password)),
PasswordFieldMsg::PasswordCheckResult,
);
}
}
}
Ok(true)
}
fn mut_common(&mut self) -> &mut CommonComponentParts<PasswordField<FormModel>> {
&mut self.common
}
}
#[derive(Properties, PartialEq, Clone)]
pub struct PasswordFieldProperties<FormModel: Model> {
pub field_name: String,
pub form: Form<FormModel>,
#[prop_or_else(|| { "form-control".into() })]
pub class: Classes,
#[prop_or_else(|| { "is-invalid".into() })]
pub class_invalid: Classes,
#[prop_or_else(|| { "is-valid".into() })]
pub class_valid: Classes,
#[prop_or_else(Callback::noop)]
pub oninput: Callback<String>,
}
impl<FormModel: Model> Component for PasswordField<FormModel> {
type Message = PasswordFieldMsg;
type Properties = PasswordFieldProperties<FormModel>;
fn create(_: &Context<Self>) -> Self {
Self {
common: CommonComponentParts::<Self>::create(),
timeout_task: None,
password: String::new(),
password_check_state: PasswordState::Typing,
_marker: std::marker::PhantomData,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, ctx, msg)
}
fn view(&self, ctx: &Context<Self>) -> yew::Html {
let link = &ctx.link();
html! {
<div>
<Field<FormModel>
autocomplete={"new-password"}
input_type={"password"}
field_name={ctx.props().field_name.clone()}
form={ctx.props().form.clone()}
class={ctx.props().class.clone()}
class_invalid={ctx.props().class_invalid.clone()}
class_valid={ctx.props().class_valid.clone()}
oninput={link.callback(|e: InputEvent| {
use wasm_bindgen::JsCast;
let target = e.target().unwrap();
let input = target.dyn_into::<HtmlInputElement>().unwrap();
PasswordFieldMsg::OnInput(input.value())
})} />
{
match self.password_check_state {
PasswordState::Checked(PasswordWasLeaked(true)) => html! { <i class="bi bi-x"></i> },
PasswordState::Checked(PasswordWasLeaked(false)) => html! { <i class="bi bi-check"></i> },
PasswordState::NotSupported | PasswordState::Typing => html!{},
PasswordState::Loading =>
html! {
<div class="spinner-border spinner-border-sm" role="status">
<span class="sr-only">{"Loading..."}</span>
</div>
},
}
}
</div>
}
}
}

View File

@@ -1,5 +1,8 @@
use crate::{
components::router::{AppRoute, Link},
components::{
password_field::PasswordField,
router::{AppRoute, Link},
},
infra::{
api::HostService,
common_component::{CommonComponent, CommonComponentParts},
@@ -176,14 +179,12 @@ impl Component for ResetPasswordStep2Form {
{"New password*:"}
</label>
<div class="col-sm-10">
<Field
<PasswordField<FormModel>
form={&self.form}
field_name="password"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="new-password"
input_type="password"
oninput={link.callback(|_| Msg::FormUpdate)} />
<div class="invalid-feedback">
{&self.form.field_message("password")}

View File

@@ -1,4 +1,4 @@
use super::cookies::set_cookie;
use crate::infra::cookies::set_cookie;
use anyhow::{anyhow, Context, Result};
use gloo_net::http::{Method, Request};
use graphql_client::GraphQLQuery;
@@ -74,6 +74,19 @@ fn set_cookies_from_jwt(response: login::ServerLoginResponse) -> Result<(String,
.context("Error setting cookie")
}
#[derive(PartialEq)]
pub struct PasswordHash(String);
#[derive(PartialEq)]
pub struct PasswordWasLeaked(pub bool);
pub fn hash_password(password: &str) -> PasswordHash {
use sha1::{Digest, Sha1};
let mut hasher = Sha1::new();
hasher.update(password);
PasswordHash(format!("{:X}", hasher.finalize()))
}
impl HostService {
pub async fn graphql_query<QueryType>(
variables: QueryType::Variables,
@@ -194,4 +207,35 @@ impl HostService {
!= http::StatusCode::NOT_FOUND,
)
}
pub async fn check_password_haveibeenpwned(
password_hash: PasswordHash,
) -> Result<(Option<PasswordWasLeaked>, PasswordHash)> {
use lldap_auth::password_reset::*;
let hash_prefix = &password_hash.0[0..5];
match call_server_json_with_error_message::<PasswordHashList, _>(
&format!("/auth/password/check/{}", hash_prefix),
NO_BODY,
"Could not validate token",
)
.await
{
Ok(r) => {
for PasswordHashCount { hash, count } in r.hashes {
if password_hash.0[5..] == hash && count != 0 {
return Ok((Some(PasswordWasLeaked(true)), password_hash));
}
}
Ok((Some(PasswordWasLeaked(false)), password_hash))
}
Err(e) => {
if e.to_string().contains("[501]:") {
// Unimplemented, no API key.
Ok((None, password_hash))
} else {
Err(e)
}
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -1,12 +1,8 @@
[package]
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
description = "Authentication protocol for LLDAP"
edition = "2021"
homepage = "https://github.com/lldap/lldap"
license = "GPL-3.0-only"
name = "lldap_auth"
repository = "https://github.com/lldap/lldap"
version = "0.3.0"
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
edition = "2021"
[features]
default = ["opaque_server", "opaque_client"]
@@ -18,17 +14,17 @@ js = []
rust-argon2 = "0.8"
curve25519-dalek = "3"
digest = "0.9"
generic-array = "0.14"
generic-array = "*"
rand = "0.8"
serde = "1"
serde = "*"
sha2 = "0.9"
thiserror = "1"
thiserror = "*"
[dependencies.opaque-ke]
version = "0.6"
[dependencies.chrono]
version = "0.4"
version = "*"
features = [ "serde" ]
# For WASM targets, use the JS getrandom.

View File

@@ -102,6 +102,17 @@ pub mod password_reset {
pub user_id: String,
pub token: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct PasswordHashCount {
pub hash: String,
pub count: u64,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct PasswordHashList {
pub hashes: Vec<PasswordHashCount>,
}
}
#[derive(Clone, Serialize, Deserialize)]

View File

@@ -1,58 +0,0 @@
# Migration from 0.4 to 0.5
Welcome! If you're here, it's probably that the migration from 0.4.x to 0.5
didn't go smoothly for you. Don't worry, we can fix that.
## Multiple users with the same email
This is the most common case. You can see in the LLDAP logs that there are
several users with the same email, and they are listed.
This is not allowed anymore in v0.5, to prevent a user from setting their email
to someone else's email and gaining access to systems that identify by email.
The problem is that you currently have several users with the same email, so the
constraint cannot be enforced.
### Step 1: Take a note of the users with duplicate emails
In the LLDAP logs when you tried to start v0.5+, you'll see some warnings with
the list of users with the same emails. Take note of them.
### Step 2: Downgrade to v0.4.3
If using docker, switch to the `lldap/lldap:v0.4.3` image. Alternatively, grab
the binaries at https://github.com/lldap/lldap/releases/tag/v0.4.3.
This downgrade is safe and supported.
### Step 3: Remove duplicate emails
Restart LLDAP with the v0.4.3 version, and using your notes from step 1, change
the email of users with duplicate emails to make sure that each email is unique.
### Step 4: Upgrade again
You can now revert to the initial version.
## Multiple users/groups with the same UUID
This should be extremely rare. In this case, you'll need to find which users
have the same UUID, revert to v0.4.3 to be able to apply the changes, and delete
one of the duplicates.
## FAQ
### What if I want several users to be controlled by the same email?
You can use plus codes to set "the same" email to several users, while ensuring
that they can't identify as each other. For instance:
- Admin: `admin@example.com`
- Read-only admin: `admin+readonly@example.com`
- Jellyfin admin: `admin+jellyfin@example.com`
### I'm upgrading to a higher version than v0.5.
This guide is still relevant: you can use whatever later version in place of
v0.5. You'll still need to revert to v0.4.3 to apply the changes.

View File

@@ -1,30 +0,0 @@
# Basic LDAP auth for a Ejabberd XMPP server
[Main documentation here.](https://docs.ejabberd.im/admin/configuration/ldap/)
For simple user auth add this to main ejabberd.yml:
```
host_config:
xmpp.example.org:
auth_method: [ldap]
ldap_servers:
- 127.0.0.1 #IP or hostname of LLDAP server
ldap_port: 3890
ldap_uids:
- uid
ldap_rootdn: "uid=lldap_readonly,ou=people,dc=example,dc=org"
ldap_password: "secret"
ldap_base: "ou=people,dc=example,dc=org"
```
## vCard from LDAP
Theoretically possible, [see the documentation.](https://docs.ejabberd.im/admin/configuration/ldap/#vcard-in-ldap)
TODO
## Shared roster groups from LDAP
Theoretically possible, [see the documentation.](https://docs.ejabberd.im/admin/configuration/ldap/#shared-roster-in-ldap)
TODO

View File

@@ -44,6 +44,6 @@ username = "uid"
# If you want to map your ldap groups to grafana's groups, see: https://grafana.com/docs/grafana/latest/auth/ldap/#group-mappings
# As a quick example, here is how you would map lldap's admin group to grafana's admin
# [[servers.group_mappings]]
# group_dn = "cn=lldap_admin,ou=groups,dc=example,dc=org"
# group_dn = "uid=lldap_admin,ou=groups,dc=example,dc=org"
# org_role = "Admin"
# grafana_admin = true

View File

@@ -1,24 +0,0 @@
# Home Assistant Configuration
Home Assistant configures ldap auth via the [Command Line Auth Provider](https://www.home-assistant.io/docs/authentication/providers/#command-line). The wiki mentions a script that can be used for LDAP authentication, but it doesn't work in the container version (it is lacking both `ldapsearch` and `curl` ldap protocol support). Thankfully LLDAP has a graphql API to save the day!
## Graphql-based Auth Script
The [auth script](lldap-ha-auth.sh) attempts to authenticate a user against an LLDAP server, using credentials provided via `username` and `password` environment variables. The first argument must be the URL of your LLDAP server, accessible from Home Assistant. You can provide an additional optional argument to confine allowed logins to a single group. The script will output the user's display name as the `name` variable, if not empty.
1. Copy the [auth script](lldap-ha-auth.sh) to your home assistant instance. In this example, we use `/config/lldap-auth.sh`.
- Set the script as executable by running `chmod +x /config/lldap-auth-sh`
2. Add the following to your configuration.yaml in Home assistant:
```yaml
homeassistant:
auth_providers:
# Ensure you have the homeassistant provider enabled if you want to continue using your existing accounts
- type: homeassistant
- type: command_line
command: /config/lldap-auth.sh
# Only allow users in the 'homeassistant_user' group to login.
# Change to ["https://lldap.example.com"] to allow all users
args: ["https://lldap.example.com", "homeassistant_user"]
meta: true
```
3. Reload your config or restart Home Assistant

View File

@@ -1,70 +0,0 @@
#!/bin/bash
# Usernames should be validated using a regular expression to be of
# a known format. Special characters will be escaped anyway, but it is
# generally not recommended to allow more than necessary.
# This pattern is set by default. In your config file, you can either
# overwrite it with a different one or use "unset USERNAME_PATTERN" to
# disable validation completely.
USERNAME_PATTERN='^[a-z|A-Z|0-9|_|-|.]+$'
# When the timeout (in seconds) is exceeded (e.g. due to slow networking),
# authentication fails.
TIMEOUT=3
# Log messages to stderr.
log() {
echo "$1" >&2
}
# Get server address
if [ -z "$1" ]; then
log "Usage: lldap-auth.sh <LLDAP server address> <Optional group to filter>"
exit 2
fi
SERVER_URL="${1%/}"
# Check username and password are present and not malformed.
if [ -z "$username" ] || [ -z "$password" ]; then
log "Need username and password environment variables."
exit 2
elif [ ! -z "$USERNAME_PATTERN" ]; then
username_match=$(echo "$username" | sed -r "s/$USERNAME_PATTERN/x/")
if [ "$username_match" != "x" ]; then
log "Username '$username' has an invalid format."
exit 2
fi
fi
RESPONSE=$(curl -f -s -X POST -m "$TIMEOUT" -H "Content-type: application/json" -d '{"username":"'"$username"'","password":"'"$password"'"}' "$SERVER_URL/auth/simple/login")
if [[ $? -ne 0 ]]; then
log "Auth failed"
exit 1
fi
TOKEN=$(jq -e -r .token <<< $RESPONSE)
if [[ $? -ne 0 ]]; then
log "Failed to parse token"
exit 1
fi
RESPONSE=$(curl -f -s -m "$TIMEOUT" -H "Content-type: application/json" -H "Authorization: Bearer ${TOKEN}" -d '{"variables":{"id":"'"$username"'"},"query":"query($id:String!){user(userId:$id){displayName groups{displayName}}}"}' "$SERVER_URL/api/graphql")
if [[ $? -ne 0 ]]; then
log "Failed to get user"
exit 1
fi
USER_JSON=$(jq -e .data.user <<< $RESPONSE)
if [[ $? -ne 0 ]]; then
log "Failed to parse user json"
exit 1
fi
if [[ ! -z "$2" ]] && ! jq -e '.groups|map(.displayName)|index("'"$2"'")' <<< $USER_JSON > /dev/null 2>&1; then
log "User is not in group '$2'"
exit 1
fi
DISPLAY_NAME=$(jq -r .displayName <<< $USER_JSON)
[[ ! -z "$DISPLAY_NAME" ]] && echo "name = $DISPLAY_NAME"

View File

@@ -1,113 +0,0 @@
# Configuration for OPNsense
## Create a LDAP Server
- Login to OPNsense
- Navigate to: `System > Access > Servers`
- Create a new server by clicking on the `+` icon
## Server Config
- Descriptive Name: `A Descriptive Name`
- Type: `LDAP`
- Hostname or IP address: `Hostname or IP for your LLDAP host`
- Port value: `Your LLDAP port`
- Default: `3890`
- Transport: `TCP - Standard`
- Protocol version: `3`
Make sure the host running LLDAP is accessible to OPNsense and that you mapped the LLDAP port to the LLDAP host.
## LDAP Config
### Bind credentials
#### User DN
```
uid=admin,ou=people,dc=example,dc=com
```
It is recommended that you create a separate user account (e.g, `bind_user`) instead of `admin` for sharing Bind credentials with other services. The `bind_user` should be a member of the `lldap_strict_readonly` group to limit access to your LDAP configuration in LLDAP.
#### Password
```
xxx
```
Enter the password that you set for the user specified in the User DN field.
### Search Scope
```
One Level
```
### Base DN
```
dc=example,dc=com
```
This is the same LDAP Base DN that you set via the *LLDAP_LDAP_BASE_DN* environment variable or in `lldap_config.toml`.
### Authentication containers
```
ou=people,dc=example,dc=com
```
Note: The `Select` box may not work for selecting containers. You can just enter the `Authentication containers` directly into the text field.
### Extended Query
```
&(objectClass=person)(memberof=cn=lldap_admin,ou=groups,dc=example,dc=com)
```
It is recommended that you create a unique LDAP group (e.g., `lldap_opnsense`) in LLDAP and use that group in this query instead of `lldap_admin`. This will limit OPNsense access to users in the `lldap_opnsense` group and make it easier to synchronize LLDAP groups with OPNsense groups for managing OPNsense access.
### Initial Template
```
OpenLDAP
```
### User naming attribute
```
uid
```
## Optional Configuration
The above configuration will connect OPNsense to LLDAP. This optional configuration will synchronize groups between LLDAP and OPNsense and automate user creation when an authorized LLDAP user logs into OPNsense.
### Remaining Server Configuration
Enable the following options on the OPNsense configuration page for your LLDAP server (the same page where you entered the prior configuration):
- Read Properties: `Checked`
- Synchronize groups: `Checked`
- Automatic user creation: `Checked`
### Create OPNsense Group
Go to `System > Access > Groups` and create a new group with the **same** name as the LLDAP group used to authenticate users for OPNsense.
By default, you would name your OPNsense group `lldap_admin` unless you followed the recommended advice in this guide and created a separate `lldap_opnsense` group for managing OPNsense users.
If you want your LLDAP users to have full administrator access in OPNsense, then you need to edit the `Assigned Privileges` for the group and add the `GUI - All pages` system privilege.
### Enable LLDAP as an Authentication Option
Go to `System > Settings > Administration` page and scroll down to the `Authentication` section. Add your LLDAP server configuration to the `Server` field.
## Testing LLDAP
OPNsense includes a built-in feature for testing user authentication at `System > Access > Tester`. Select your LLDAP server configuration in the `Authentication Server` to test logins for your LLDAP users.
## More Information
Please read the [OPNsense docs](https://docs.opnsense.org/manual/how-tos/user-ldap.html) for more information on LDAP configuration and managing access to OPNsense.

View File

@@ -1,117 +0,0 @@
# Configuration for pfSense
## Create a LDAP Server
- Login to pfSense
- Navigate to: `System > User Manager > Authentication Servers`
- Create a new server by clicking on the `+ Add` button
## LDAP Server Settings
- Descriptive Name: `A Descriptive Name`
- Type: `LDAP`
- Hostname or IP address: `Hostname or IP for your LLDAP host`
- Port value: `Your LLDAP port`
- Transport: `TCP - Standard`
- Protocol version: `3`
- Server Timeout: `25`
(Make sure the host running LLDAP is accessible to pfSense and that you mapped the LLDAP port to the LLDAP host)
### Search Scope
```
Entire Subtree
```
### Base DN
```
dc=example,dc=com
```
This is the same LDAP Base DN that you set via the *LLDAP_LDAP_BASE_DN* environment variable or in `lldap_config.toml`.
### Authentication containers
```
ou=people
```
Note: The `Select a container` box may not work for selecting containers. You can just enter the `Authentication containers` directly into the text field.
### Extended Query
Enable extended query: `Checked`
### Query:
```
&(objectClass=person)(|(memberof=cn=pfsense_admin,ou=groups,dc=example,dc=com)(memberof=cn=pfsense_guest,ou=groups,dc=example,dc=com))
```
This example gives you two groups in LLDAP, one for pfSense admin access (`pfsense_admin`) and one for guest access (`pfsense_guest`). You **must** create these exact same groups in both LLDAP and pfSense, then give them the correct permissions in pfSense.
### Bind Anonymous
`Unchecked`
### Bind credentials
#### User DN
```
uid=yourbinduser,ou=people,dc=example,dc=com
```
It is recommended that you create a separate read-only user account (e.g, `readonly`) instead of `admin` for sharing Bind credentials with other services. The `readonly` should be a member of the `lldap_strict_readonly` group to limit access to your LDAP configuration in LLDAP.
#### Password
```
LLDAPPasswordForBindUser
```
### User naming attribute
```
uid
```
### Group naming attribute
```
cn
```
### Group member attribute
```
memberof
```
### RFC 2307 Groups
`Unchecked`
### Group Object Class
`groupOfUniqueNames`
### Shell Authentication Group DN
`cn=pfsense_admin,ou=groups,dc=example,dc=com`
(This is only if you want to give a group shell access through LDAP. Leave blank and only the pfSense admin user will have shell access.
### Remaining Server Configuration
Enable the following options on the pfSense configuration page for your LLDAP server (the same page where you entered the prior configuration):
- UTF8 Encodes: `Checked`
- Username Alterations: `Unchecked`
- Allow unauthenticated bind: `Unchecked`
### Create pfSense Groups
Go to `System > User Manager > Groups` and create a new group(s) with the **same exact** name as the LLDAP group(s) used to authenticate users for pfSense (`pfsense_admin` and `pfsense_guest` in this example).
If you want your LLDAP users to have full administrator access in pfSense, then you need to edit the `Assigned Privileges` for the group and add the `WebCfg - All pages` system privilege. If you do not give any permissions to a group, you will be able to log in but only see an empty webUI.
### Enable LLDAP as an Authentication Option
Go to `System > User Manager > Settings` page. Add your LLDAP server configuration to the `Authentication Server` field. **The "Save & Test" Button will fail the test results at step 3. No clue why.**
## Testing LLDAP
pfSense includes a built-in feature for testing user authentication at `Diagnostics > Authentication`. Select your LLDAP server configuration in the `Authentication Server` to test logins for your LLDAP users. The groups (only the ones you added to pfSense) should show up when tested.
## More Information
Please read the [pfSense docs](https://docs.netgate.com/pfsense/en/latest/usermanager/ldap.html) for more information on LDAP configuration and managing access to pfSense.

View File

@@ -13,6 +13,6 @@ You setup https://zend.to/ for using LDAP by editing `/opt/zendto/config/prefere
'authLDAPUsernameAttr' => 'uid',
'authLDAPEmailAttr' => 'mail',
'authLDAPMemberKey' => 'memberOf',
'authLDAPMemberRole' => 'cn=zendto,ou=groups,dc=example,dc=com',
'authLDAPMemberRole' => 'uid=zendto,ou=groups,dc=example,dc=com',
```
Every user of the group `zendto` is allowed to login.

View File

@@ -93,17 +93,8 @@ database_url = "sqlite:///data/users.db?mode=rwc"
## would still have to perform an (expensive) brute force attack to find
## each password.
## Randomly generated on first run if it doesn't exist.
## Alternatively, you can use key_seed to override this instead of relying on
## a file.
## Env variable: LLDAP_KEY_FILE
key_file = "/data/private_key"
## Seed to generate the server private key, see key_file above.
## This can be any random string, the recommendation is that it's at least 12
## characters long.
## Env variable: LLDAP_KEY_SEED
#key_seed = "RanD0m STR1ng"
## Ignored attributes.
## Some services will request attributes that are not present in LLDAP. When it
## is the case, LLDAP will warn about the attribute being unknown. If you want
@@ -115,7 +106,7 @@ key_file = "/data/private_key"
## Options to configure SMTP parameters, to send password reset emails.
## To set these options from environment variables, use the following format
## (example with "password"): LLDAP_SMTP_OPTIONS__PASSWORD
[smtp_options]
#[smtp_options]
## Whether to enabled password reset via email, from LLDAP.
#enable_password_reset=true
## The SMTP server.
@@ -137,7 +128,7 @@ key_file = "/data/private_key"
## Options to configure LDAPS.
## To set these options from environment variables, use the following format
## (example with "port"): LLDAP_LDAPS_OPTIONS__PORT
[ldaps_options]
#[ldaps_options]
## Whether to enable LDAPS.
#enabled=true
## Port on which to listen.

View File

@@ -1,24 +1,20 @@
[package]
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
description = "CLI migration tool to go from OpenLDAP to LLDAP"
edition = "2021"
homepage = "https://github.com/lldap/lldap"
license = "GPL-3.0-only"
name = "lldap_migration_tool"
repository = "https://github.com/lldap/lldap"
name = "migration-tool"
version = "0.4.2"
edition = "2021"
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
[dependencies]
anyhow = "1"
anyhow = "*"
base64 = "0.13"
rand = "0.8"
requestty = "0.4.1"
serde = "1"
serde_json = "1"
smallvec = "1"
smallvec = "*"
[dependencies.lldap_auth]
version = "0.3"
path = "../auth"
features = ["opaque_client"]
[dependencies.graphql_client]
@@ -27,11 +23,11 @@ default-features = false
version = "0.11"
[dependencies.reqwest]
version = "0.11"
version = "*"
default-features = false
features = ["json", "blocking", "rustls-tls"]
[dependencies.ldap3]
version = "0.11"
version = "*"
default-features = false
features = ["sync", "tls-rustls"]

View File

@@ -1,14 +1,8 @@
[package]
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
categories = ["authentication", "command-line-utilities"]
description = "Super-simple and lightweight LDAP server"
edition = "2021"
homepage = "https://github.com/lldap/lldap"
keywords = ["cli", "ldap", "graphql", "server", "authentication"]
license = "GPL-3.0-only"
name = "lldap"
repository = "https://github.com/lldap/lldap"
version = "0.5.0-alpha"
version = "0.4.3-alpha"
[dependencies]
actix = "0.13"
@@ -19,45 +13,44 @@ actix-server = "2"
actix-service = "2"
actix-web = "4.3"
actix-web-httpauth = "0.8"
anyhow = "1"
anyhow = "*"
async-trait = "0.1"
base64 = "0.21"
bincode = "1.3"
cron = "0.12"
cron = "*"
derive_builder = "0.12"
figment_file_provider_adapter = "0.1"
futures = "0.3"
futures-util = "0.3"
futures = "*"
futures-util = "*"
hmac = "0.12"
http = "0.2"
http = "*"
itertools = "0.10"
juniper = "0.15"
jwt = "0.16"
lber = "0.4.1"
ldap3_proto = ">=0.3.1"
log = "0.4"
log = "*"
orion = "0.17"
rand_chacha = "0.3"
rustls-pemfile = "1"
serde = "1"
serde_bytes = "0.11"
rustls = "0.20"
serde = "*"
serde_json = "1"
sha2 = "0.10"
thiserror = "1"
thiserror = "*"
time = "0.3"
tokio-rustls = "0.23"
tokio-stream = "0.1"
tokio-stream = "*"
tokio-util = "0.7"
tracing = "0.1"
tracing = "*"
tracing-actix-web = "0.7"
tracing-attributes = "^0.1.21"
tracing-log = "0.1"
urlencoding = "2"
webpki-roots = "0.23"
tracing-log = "*"
rustls-pemfile = "1"
serde_bytes = "0.11"
webpki-roots = "*"
[dependencies.chrono]
features = ["serde"]
version = "0.4"
version = "*"
[dependencies.clap]
features = ["std", "color", "suggestions", "derive", "env"]
@@ -65,8 +58,7 @@ version = "4"
[dependencies.figment]
features = ["env", "toml"]
version = "0.10"
version = "*"
[dependencies.tracing-subscriber]
version = "0.3"
features = ["env-filter", "tracing-log"]
@@ -77,7 +69,7 @@ default-features = false
version = "0.10.1"
[dependencies.lldap_auth]
version = "0.3"
path = "../auth"
[dependencies.opaque-ke]
version = "0.6"
@@ -88,7 +80,7 @@ version = "0.8"
[dependencies.secstr]
features = ["serde"]
version = "0.5"
version = "*"
[dependencies.tokio]
features = ["full"]
@@ -96,7 +88,7 @@ version = "1.25"
[dependencies.uuid]
features = ["v3"]
version = "1"
version = "*"
[dependencies.tracing-forest]
features = ["smallvec", "chrono", "tokio"]
@@ -121,35 +113,5 @@ version = "0.11"
default-features = false
features = ["rustls-tls-webpki-roots"]
[dependencies.rustls]
version = "0.20"
features = ["dangerous_configuration"]
[dev-dependencies]
assert_cmd = "2.0"
mockall = "0.11"
nix = "0.26.2"
[dev-dependencies.graphql_client]
features = ["graphql_query_derive", "reqwest-rustls"]
default-features = false
version = "0.11"
[dev-dependencies.ldap3]
version = "0.11"
default-features = false
features = ["sync", "tls-rustls"]
[dev-dependencies.reqwest]
version = "0.11"
default-features = false
features = ["json", "blocking", "rustls-tls"]
[dev-dependencies.serial_test]
version = "2.0.0"
default-features = false
features = ["file_locks"]
[dev-dependencies.uuid]
version = "1"
features = ["v4"]

View File

@@ -86,7 +86,7 @@ pub mod tests {
handler
.create_user(CreateUserRequest {
user_id: UserId::new(name),
email: format!("{}@bob.bob", name),
email: "bob@bob.bob".to_string(),
display_name: Some("display ".to_string() + name),
first_name: Some("first ".to_string() + name),
last_name: Some("last ".to_string() + name),

View File

@@ -2,17 +2,15 @@ use crate::domain::{
sql_tables::{DbConnection, SchemaVersion},
types::{GroupId, UserId, Uuid},
};
use anyhow::Context;
use itertools::Itertools;
use sea_orm::{
sea_query::{
self, all, ColumnDef, Expr, ForeignKey, ForeignKeyAction, Func, Index, Query, Table, Value,
},
ConnectionTrait, FromQueryResult, Iden, Order, Statement, TransactionTrait,
sea_query::{self, ColumnDef, Expr, ForeignKey, ForeignKeyAction, Query, Table, Value},
ConnectionTrait, FromQueryResult, Iden, Statement, TransactionTrait,
};
use serde::{Deserialize, Serialize};
use tracing::{info, instrument, warn};
use super::sql_tables::LAST_SCHEMA_VERSION;
#[derive(Iden, PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Copy)]
pub enum Users {
Table,
@@ -462,136 +460,30 @@ async fn migrate_to_v3(pool: &DbConnection) -> anyhow::Result<()> {
Ok(())
}
async fn migrate_to_v4(pool: &DbConnection) -> anyhow::Result<()> {
let builder = pool.get_database_backend();
// Make emails and UUIDs unique.
if let Err(e) = pool
.execute(
builder.build(
Index::create()
.if_not_exists()
.name("unique-user-email")
.table(Users::Table)
.col(Users::Email)
.unique(),
),
)
.await
.context(
r#"while enforcing unicity on emails (2 users have the same email).
See https://github.com/lldap/lldap/blob/main/docs/migration_guides/v0.5.md for details.
"#,
)
{
warn!("Found several users with the same email:");
for (email, users) in &pool
.query_all(
builder.build(
Query::select()
.from(Users::Table)
.columns([Users::Email, Users::UserId])
.order_by_columns([(Users::Email, Order::Asc), (Users::UserId, Order::Asc)])
.and_where(
Expr::col(Users::Email).in_subquery(
Query::select()
.from(Users::Table)
.column(Users::Email)
.group_by_col(Users::Email)
.cond_having(all![Expr::gt(
Expr::expr(Func::count(Expr::col(Users::Email))),
1
)])
.take(),
),
),
),
)
.await
.expect("Could not check duplicate users")
.into_iter()
.map(|row| {
(
row.try_get::<UserId>("", &Users::UserId.to_string())
.unwrap(),
row.try_get::<String>("", &Users::Email.to_string())
.unwrap(),
)
})
.group_by(|(_user, email)| email.to_owned())
{
warn!("Email: {email}");
for (user, _email) in users {
warn!(" User: {}", user.as_str());
}
}
return Err(e);
}
pool.execute(
builder.build(
Index::create()
.if_not_exists()
.name("unique-user-uuid")
.table(Users::Table)
.col(Users::Uuid)
.unique(),
),
)
.await
.context("while enforcing unicity on user UUIDs (2 users have the same UUID)")?;
pool.execute(
builder.build(
Index::create()
.if_not_exists()
.name("unique-group-uuid")
.table(Groups::Table)
.col(Groups::Uuid)
.unique(),
),
)
.await
.context("while enforcing unicity on group UUIDs (2 groups have the same UUID)")?;
Ok(())
}
// This is needed to make an array of async functions.
macro_rules! to_sync {
($l:ident) => {
|pool| -> std::pin::Pin<Box<dyn std::future::Future<Output = anyhow::Result<()>>>> {
Box::pin($l(pool))
}
};
}
pub async fn migrate_from_version(
pool: &DbConnection,
version: SchemaVersion,
last_version: SchemaVersion,
) -> anyhow::Result<()> {
match version.cmp(&last_version) {
std::cmp::Ordering::Less => (),
match version.cmp(&LAST_SCHEMA_VERSION) {
std::cmp::Ordering::Less => info!(
"Upgrading DB schema from {} to {}",
version.0, LAST_SCHEMA_VERSION.0
),
std::cmp::Ordering::Equal => return Ok(()),
std::cmp::Ordering::Greater => anyhow::bail!("DB version downgrading is not supported"),
}
info!("Upgrading DB schema from version {}", version.0);
let migrations = [
to_sync!(migrate_to_v2),
to_sync!(migrate_to_v3),
to_sync!(migrate_to_v4),
];
for migration in 2..=4 {
if version < SchemaVersion(migration) && SchemaVersion(migration) <= last_version {
info!("Upgrading DB schema to version {}", migration);
migrations[(migration - 2) as usize](pool).await?;
}
if version < SchemaVersion(2) {
migrate_to_v2(pool).await?;
}
if version < SchemaVersion(3) {
migrate_to_v3(pool).await?;
}
let builder = pool.get_database_backend();
pool.execute(
builder.build(
Query::update()
.table(Metadata::Table)
.value(Metadata::Version, Value::from(last_version)),
.value(Metadata::Version, Value::from(LAST_SCHEMA_VERSION)),
),
)
.await?;

View File

@@ -21,7 +21,7 @@ impl From<SchemaVersion> for Value {
}
}
const LAST_SCHEMA_VERSION: SchemaVersion = SchemaVersion(4);
pub const LAST_SCHEMA_VERSION: SchemaVersion = SchemaVersion(3);
pub async fn init_table(pool: &DbConnection) -> anyhow::Result<()> {
let version = {
@@ -32,7 +32,7 @@ pub async fn init_table(pool: &DbConnection) -> anyhow::Result<()> {
SchemaVersion(1)
}
};
migrate_from_version(pool, version, LAST_SCHEMA_VERSION).await?;
migrate_from_version(pool, version).await?;
Ok(())
}
@@ -46,7 +46,6 @@ mod tests {
use super::*;
use chrono::prelude::*;
use sea_orm::{ConnectionTrait, Database, DbBackend, FromQueryResult};
use tracing::error;
async fn get_in_memory_db() -> DbConnection {
let mut sql_opt = sea_orm::ConnectOptions::new("sqlite::memory:".to_owned());
@@ -101,21 +100,21 @@ mod tests {
let sql_pool = get_in_memory_db().await;
sql_pool
.execute(raw_statement(
r#"CREATE TABLE users ( user_id TEXT, display_name TEXT, first_name TEXT NOT NULL, last_name TEXT, avatar BLOB, creation_date TEXT, email TEXT);"#,
r#"CREATE TABLE users ( user_id TEXT, display_name TEXT, first_name TEXT NOT NULL, last_name TEXT, avatar BLOB, creation_date TEXT);"#,
))
.await
.unwrap();
sql_pool
.execute(raw_statement(
r#"INSERT INTO users (user_id, display_name, first_name, creation_date, email)
VALUES ("bôb", "", "", "1970-01-01 00:00:00", "bob@bob.com")"#,
r#"INSERT INTO users (user_id, display_name, first_name, creation_date)
VALUES ("bôb", "", "", "1970-01-01 00:00:00")"#,
))
.await
.unwrap();
sql_pool
.execute(raw_statement(
r#"INSERT INTO users (user_id, display_name, first_name, creation_date, email)
VALUES ("john", "John Doe", "John", "1971-01-01 00:00:00", "bob2@bob.com")"#,
r#"INSERT INTO users (user_id, display_name, first_name, creation_date)
VALUES ("john", "John Doe", "John", "1971-01-01 00:00:00")"#,
))
.await
.unwrap();
@@ -207,69 +206,6 @@ mod tests {
);
}
#[tokio::test]
async fn test_migration_to_v4() {
crate::infra::logging::init_for_tests();
let sql_pool = get_in_memory_db().await;
upgrade_to_v1(&sql_pool).await.unwrap();
migrate_from_version(&sql_pool, SchemaVersion(1), SchemaVersion(3))
.await
.unwrap();
sql_pool
.execute(raw_statement(
r#"INSERT INTO users (user_id, email, display_name, first_name, creation_date, uuid)
VALUES ("bob", "bob@bob.com", "", "", "1970-01-01 00:00:00", "a02eaf13-48a7-30f6-a3d4-040ff7c52b04")"#,
))
.await
.unwrap();
sql_pool
.execute(raw_statement(
r#"INSERT INTO users (user_id, email, display_name, first_name, creation_date, uuid)
VALUES ("bob2", "bob@bob.com", "", "", "1970-01-01 00:00:00", "986765a5-3f03-389e-b47b-536b2d6e1bec")"#,
))
.await
.unwrap();
error!(
"{}",
migrate_from_version(&sql_pool, SchemaVersion(3), SchemaVersion(4))
.await
.expect_err("migration should fail")
);
assert_eq!(
sql_migrations::JustSchemaVersion::find_by_statement(raw_statement(
r#"SELECT version FROM metadata"#
))
.one(&sql_pool)
.await
.unwrap()
.unwrap(),
sql_migrations::JustSchemaVersion {
version: SchemaVersion(3)
}
);
sql_pool
.execute(raw_statement(
r#"UPDATE users SET email = "new@bob.com" WHERE user_id = "bob2""#,
))
.await
.unwrap();
migrate_from_version(&sql_pool, SchemaVersion(3), SchemaVersion(4))
.await
.unwrap();
assert_eq!(
sql_migrations::JustSchemaVersion::find_by_statement(raw_statement(
r#"SELECT version FROM metadata"#
))
.one(&sql_pool)
.await
.unwrap()
.unwrap(),
sql_migrations::JustSchemaVersion {
version: SchemaVersion(4)
}
);
}
#[tokio::test]
async fn test_too_high_version() {
let sql_pool = get_in_memory_db().await;

View File

@@ -1,21 +1,22 @@
use std::collections::{hash_map::DefaultHasher, HashSet};
use std::hash::{Hash, Hasher};
use std::pin::Pin;
use std::task::{Context, Poll};
use std::task::Poll;
use actix_web::{
cookie::{Cookie, SameSite},
dev::{Service, ServiceRequest, ServiceResponse, Transform},
error::{ErrorBadRequest, ErrorUnauthorized},
web, HttpRequest, HttpResponse,
web, FromRequest, HttpRequest, HttpResponse,
};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use anyhow::Result;
use anyhow::{bail, Context, Result};
use chrono::prelude::*;
use futures::future::{ok, Ready};
use futures_util::FutureExt;
use hmac::Hmac;
use jwt::{SignWithKey, VerifyWithKey};
use secstr::SecUtf8;
use sha2::Sha512;
use time::ext::NumericalDuration;
use tracing::{debug, info, instrument, warn};
@@ -205,6 +206,24 @@ where
.unwrap_or_else(error_to_http_response)
}
async fn check_password_reset_token<'a, Backend>(
backend_handler: &Backend,
token: &Option<&'a str>,
) -> TcpResult<Option<(&'a str, UserId)>>
where
Backend: TcpBackendHandler + 'static,
{
let token = match token {
None => return Ok(None),
Some(token) => token,
};
let user_id = backend_handler
.get_user_id_for_password_reset_token(token)
.await
.map_err(|_| TcpError::UnauthorizedError("Invalid or expired token".to_string()))?;
Ok(Some((token, user_id)))
}
#[instrument(skip_all, level = "debug")]
async fn get_password_reset_step2<Backend>(
data: web::Data<AppState<Backend>>,
@@ -213,22 +232,12 @@ async fn get_password_reset_step2<Backend>(
where
Backend: TcpBackendHandler + BackendHandler + 'static,
{
let token = request
.match_info()
.get("token")
.ok_or_else(|| TcpError::BadRequest("Missing reset token".to_owned()))?;
let user_id = data
.get_tcp_handler()
.get_user_id_for_password_reset_token(token)
.await
.map_err(|e| {
debug!("Reset token error: {e:#}");
TcpError::NotFoundError("Wrong or expired reset token".to_owned())
})?;
let _ = data
.get_tcp_handler()
.delete_password_reset_token(token)
.await;
let tcp_handler = data.get_tcp_handler();
let (token, user_id) =
check_password_reset_token(tcp_handler, &request.match_info().get("token"))
.await?
.ok_or_else(|| TcpError::BadRequest("Missing token".to_string()))?;
let _ = tcp_handler.delete_password_reset_token(token).await;
let groups = HashSet::new();
let token = create_jwt(&data.jwt_key, user_id.to_string(), groups);
Ok(HttpResponse::Ok()
@@ -403,6 +412,7 @@ where
Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + LoginHandler + 'static,
{
let user_id = UserId::new(&request.username);
debug!(?user_id);
let bind_request = BindRequest {
name: user_id.clone(),
password: request.password.clone(),
@@ -449,6 +459,115 @@ where
.unwrap_or_else(error_to_http_response)
}
// Parse the response from the HaveIBeenPwned API. Sample response:
//
// 0018A45C4D1DEF81644B54AB7F969B88D65:1
// 00D4F6E8FA6EECAD2A3AA415EEC418D38EC:2
// 011053FD0102E94D6AE2F8B83D76FAF94F6:13
fn parse_hash_list(response: &str) -> Result<password_reset::PasswordHashList> {
use password_reset::*;
let parse_line = |line: &str| -> Result<PasswordHashCount> {
let split = line.trim().split(':').collect::<Vec<_>>();
if let [hash, count] = &split[..] {
if hash.len() == 35 {
if let Ok(count) = str::parse::<u64>(count) {
return Ok(PasswordHashCount {
hash: hash.to_string(),
count,
});
}
}
}
bail!("Invalid password hash from API: {}", line)
};
Ok(PasswordHashList {
hashes: response
.split('\n')
.map(parse_line)
.collect::<Result<Vec<_>>>()?,
})
}
// TODO: Refactor that for testing.
async fn get_password_hash_list(
hash: &str,
api_key: &SecUtf8,
) -> Result<password_reset::PasswordHashList> {
use reqwest::*;
let client = Client::new();
let resp = client
.get(format!("https://api.pwnedpasswords.com/range/{}", hash))
.header(header::USER_AGENT, "LLDAP")
.header("hibp-api-key", api_key.unsecure())
.send()
.await
.context("Could not get response from HIBP")?
.text()
.await?;
parse_hash_list(&resp).context("Invalid HIBP response")
}
async fn check_password_pwned<Backend>(
data: web::Data<AppState<Backend>>,
request: HttpRequest,
payload: web::Payload,
) -> TcpResult<HttpResponse>
where
Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + 'static,
{
let has_reset_token = check_password_reset_token(
data.get_tcp_handler(),
&request
.headers()
.get("reset-token")
.map(|v| v.to_str().unwrap()),
)
.await?
.is_some();
let inner_payload = &mut payload.into_inner();
if !has_reset_token
&& BearerAuth::from_request(&request, inner_payload)
.await
.ok()
.and_then(|bearer| check_if_token_is_valid(&data, bearer.token()).ok())
.is_none()
{
return Err(TcpError::UnauthorizedError(
"No token or invalid token".to_string(),
));
}
if data.hibp_api_key.unsecure().is_empty() {
return Err(TcpError::NotImplemented("No HIBP API key".to_string()));
}
let hash = request
.match_info()
.get("hash")
.ok_or_else(|| TcpError::BadRequest("Missing hash".to_string()))?;
if hash.len() != 5 || !hash.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(TcpError::BadRequest(format!(
"Bad request: invalid hash format \"{}\"",
hash
)));
}
get_password_hash_list(hash, &data.hibp_api_key)
.await
.map(|hashes| HttpResponse::Ok().json(hashes))
.map_err(|e| TcpError::InternalServerError(e.to_string()))
}
async fn check_password_pwned_handler<Backend>(
data: web::Data<AppState<Backend>>,
request: HttpRequest,
payload: web::Payload,
) -> HttpResponse
where
Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + 'static,
{
check_password_pwned(data, request, payload)
.await
.unwrap_or_else(error_to_http_response)
}
#[instrument(skip_all, level = "debug")]
async fn opaque_register_start<Backend>(
request: actix_web::HttpRequest,
@@ -565,7 +684,7 @@ where
#[allow(clippy::type_complexity)]
type Future = Pin<Box<dyn core::future::Future<Output = Result<Self::Response, Self::Error>>>>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
fn poll_ready(&self, cx: &mut std::task::Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
@@ -636,6 +755,11 @@ where
web::resource("/simple/login").route(web::post().to(simple_login_handler::<Backend>)),
)
.service(web::resource("/refresh").route(web::get().to(get_refresh_handler::<Backend>)))
.service(
web::resource("/password/check/{hash}")
.wrap(CookieToHeaderTranslatorFactory)
.route(web::get().to(check_password_pwned_handler::<Backend>)),
)
.service(web::resource("/logout").route(web::get().to(get_logout_handler::<Backend>)))
.service(
web::scope("/opaque/register")

View File

@@ -54,16 +54,9 @@ pub struct RunOpts {
/// Path to the file that contains the private server key.
/// It will be created if it doesn't exist.
/// Alternatively, you can set `server_key_seed`. If `server_key_seed` is given,
/// `server_key_file` will be ignored.
#[clap(long, env = "LLDAP_SERVER_KEY_FILE")]
pub server_key_file: Option<String>,
/// Seed used to generate the private server key.
/// Takes precedence over `server_key_file`.
#[clap(long, env = "LLDAP_SERVER_KEY_SEED")]
pub server_key_seed: Option<String>,
/// Change ldap host. Default: "0.0.0.0"
#[clap(long, env = "LLDAP_LDAP_HOST")]
pub ldap_host: Option<String>,
@@ -88,6 +81,10 @@ pub struct RunOpts {
#[clap(short, long, env = "LLDAP_DATABASE_URL")]
pub database_url: Option<String>,
/// HaveIBeenPwned API key, to check passwords against leaks.
#[clap(long, env = "LLDAP_HIBP_API_KEY")]
pub hibp_api_key: Option<String>,
#[clap(flatten)]
pub smtp_opts: SmtpOpts,

View File

@@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize};
pub struct MailOptions {
#[builder(default = "false")]
pub enable_password_reset: bool,
#[builder(default)]
#[builder(default = "None")]
pub from: Option<Mailbox>,
#[builder(default = "None")]
pub reply_to: Option<Mailbox>,
@@ -25,7 +25,7 @@ pub struct MailOptions {
pub server: String,
#[builder(default = "587")]
pub port: u16,
#[builder(default)]
#[builder(default = r#"String::default()"#)]
pub user: String,
#[builder(default = r#"SecUtf8::from("")"#)]
pub password: SecUtf8,
@@ -78,7 +78,7 @@ pub struct Configuration {
pub ldap_base_dn: String,
#[builder(default = r#"UserId::new("admin")"#)]
pub ldap_user_dn: UserId,
#[builder(default)]
#[builder(default = r#"String::default()"#)]
pub ldap_user_email: String,
#[builder(default = r#"SecUtf8::from("password")"#)]
pub ldap_user_pass: SecUtf8,
@@ -92,16 +92,14 @@ pub struct Configuration {
pub verbose: bool,
#[builder(default = r#"String::from("server_key")"#)]
pub key_file: String,
// We want an Option to see whether there is a value or not, since the value is printed as
// "***SECRET***".
#[builder(default)]
pub key_seed: Option<SecUtf8>,
#[builder(default)]
pub smtp_options: MailOptions,
#[builder(default)]
pub ldaps_options: LdapsOptions,
#[builder(default = r#"String::from("http://localhost")"#)]
pub http_url: String,
#[builder(default = r#"SecUtf8::from("")"#)]
pub hibp_api_key: SecUtf8,
#[serde(skip)]
#[builder(field(private), default = "None")]
server_setup: Option<ServerSetup>,
@@ -115,14 +113,7 @@ impl std::default::Default for Configuration {
impl ConfigurationBuilder {
pub fn build(self) -> Result<Configuration> {
let server_setup = get_server_setup(
self.key_file.as_deref().unwrap_or("server_key"),
self.key_seed
.as_ref()
.and_then(|o| o.as_ref())
.map(SecUtf8::unsecure)
.unwrap_or_default(),
)?;
let server_setup = get_server_setup(self.key_file.as_deref().unwrap_or("server_key"))?;
Ok(self.server_setup(Some(server_setup)).private_build()?)
}
@@ -165,25 +156,10 @@ fn write_to_readonly_file(path: &std::path::Path, buffer: &[u8]) -> Result<()> {
Ok(file.write_all(buffer)?)
}
fn get_server_setup(file_path: &str, key_seed: &str) -> Result<ServerSetup> {
fn get_server_setup(file_path: &str) -> Result<ServerSetup> {
use std::fs::read;
let path = std::path::Path::new(file_path);
if !key_seed.is_empty() {
if file_path != "server_key" || path.exists() {
eprintln!("WARNING: A key_seed was given, we will ignore the server_key and generate one from the seed!");
} else {
println!("Got a key_seed, ignoring key_file");
}
let hash = |val: &[u8]| -> [u8; 32] {
use sha2::{Digest, Sha256};
let mut seed_hasher = Sha256::new();
seed_hasher.update(val);
seed_hasher.finalize().into()
};
use rand::SeedableRng;
let mut rng = rand_chacha::ChaCha20Rng::from_seed(hash(key_seed.as_bytes()));
Ok(ServerSetup::new(&mut rng))
} else if path.exists() {
if path.exists() {
let bytes = read(file_path).context(format!("Could not read key file `{}`", file_path))?;
Ok(ServerSetup::deserialize(&bytes)?)
} else {
@@ -224,10 +200,6 @@ impl ConfigOverrider for RunOpts {
config.key_file = path.to_string();
}
if let Some(seed) = self.server_key_seed.as_ref() {
config.key_seed = Some(SecUtf8::from(seed));
}
if let Some(port) = self.ldap_port {
config.ldap_port = port;
}
@@ -243,6 +215,10 @@ impl ConfigOverrider for RunOpts {
if let Some(database_url) = self.database_url.as_ref() {
config.database_url = database_url.to_string();
}
if let Some(api_key) = self.hibp_api_key.as_ref() {
config.hibp_api_key = SecUtf8::from(api_key.clone());
}
self.smtp_opts.override_config(config);
self.ldaps_opts.override_config(config);
}
@@ -336,14 +312,7 @@ where
if config.verbose {
println!("Configuration: {:#?}", &config);
}
config.server_setup = Some(get_server_setup(
&config.key_file,
config
.key_seed
.as_ref()
.map(SecUtf8::unsecure)
.unwrap_or_default(),
)?);
config.server_setup = Some(get_server_setup(&config.key_file)?);
if config.jwt_secret == SecUtf8::from("secretjwtsecret") {
println!("WARNING: Default JWT secret used! This is highly unsafe and can allow attackers to log in as admin.");
}
@@ -355,29 +324,3 @@ where
}
Ok(config)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_generated_server_key() {
assert_eq!(
bincode::serialize(&get_server_setup("/doesnt/exist", "key seed").unwrap()).unwrap(),
[
255, 206, 202, 50, 247, 13, 59, 191, 69, 244, 148, 187, 150, 227, 12, 250, 20, 207,
211, 151, 147, 33, 107, 132, 2, 252, 121, 94, 97, 6, 97, 232, 163, 168, 86, 246,
249, 186, 31, 204, 59, 75, 65, 134, 108, 159, 15, 70, 246, 250, 150, 195, 54, 197,
195, 176, 150, 200, 157, 119, 13, 173, 119, 8, 32, 0, 0, 0, 0, 0, 0, 0, 248, 123,
35, 91, 194, 51, 52, 57, 191, 210, 68, 227, 107, 166, 232, 37, 195, 244, 100, 84,
88, 212, 190, 12, 195, 57, 83, 72, 127, 189, 179, 16, 32, 0, 0, 0, 0, 0, 0, 0, 128,
112, 60, 207, 205, 69, 67, 73, 24, 175, 187, 62, 16, 45, 59, 136, 78, 40, 187, 54,
159, 94, 116, 33, 133, 119, 231, 43, 199, 164, 141, 7, 32, 0, 0, 0, 0, 0, 0, 0,
212, 134, 53, 203, 131, 24, 138, 211, 162, 28, 23, 233, 251, 82, 34, 66, 98, 12,
249, 205, 35, 208, 241, 50, 128, 131, 46, 189, 211, 51, 56, 109, 32, 0, 0, 0, 0, 0,
0, 0, 84, 20, 147, 25, 50, 5, 243, 203, 216, 180, 175, 121, 159, 96, 123, 183, 146,
251, 22, 44, 98, 168, 67, 224, 255, 139, 159, 25, 24, 254, 88, 3
]
);
}
}

View File

@@ -124,12 +124,10 @@ impl<Handler: BackendHandler> Query<Handler> {
}
pub async fn user(context: &Context<Handler>, user_id: String) -> FieldResult<User<Handler>> {
use anyhow::Context;
let span = debug_span!("[GraphQL query] user");
span.in_scope(|| {
debug!(?user_id);
});
let user_id = urlencoding::decode(&user_id).context("Invalid user parameter")?;
let user_id = UserId::new(&user_id);
let handler = context
.get_readable_handler(&user_id)

View File

@@ -1,4 +1,4 @@
use crate::infra::{configuration::LdapsOptions, ldap_server::read_certificates};
use crate::infra::configuration::LdapsOptions;
use anyhow::{anyhow, bail, ensure, Context, Result};
use futures_util::SinkExt;
use ldap3_proto::{
@@ -65,7 +65,6 @@ where
invalid_answer
);
info!("Success");
resp.close().await?;
Ok(())
}
@@ -86,44 +85,15 @@ fn get_root_certificates() -> rustls::RootCertStore {
root_store
}
fn get_tls_connector(ldaps_options: &LdapsOptions) -> Result<RustlsTlsConnector> {
let mut client_config = rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(get_root_certificates())
.with_no_client_auth();
let (certs, _private_key) = read_certificates(ldaps_options)?;
// Check that the server cert is the one in the config file.
struct CertificateVerifier {
certificate: rustls::Certificate,
certificate_path: String,
}
impl rustls::client::ServerCertVerifier for CertificateVerifier {
fn verify_server_cert(
&self,
end_entity: &rustls::Certificate,
_intermediates: &[rustls::Certificate],
_server_name: &rustls::ServerName,
_scts: &mut dyn Iterator<Item = &[u8]>,
_ocsp_response: &[u8],
_now: std::time::SystemTime,
) -> std::result::Result<rustls::client::ServerCertVerified, rustls::Error> {
if end_entity != &self.certificate {
return Err(rustls::Error::InvalidCertificateData(format!(
"Server certificate doesn't match the one in the config file {}",
&self.certificate_path
)));
}
Ok(rustls::client::ServerCertVerified::assertion())
}
}
let mut dangerous_config = rustls::client::DangerousClientConfig {
cfg: &mut client_config,
};
dangerous_config.set_certificate_verifier(std::sync::Arc::new(CertificateVerifier {
certificate: certs.first().expect("empty certificate chain").clone(),
certificate_path: ldaps_options.cert_file.clone(),
}));
Ok(std::sync::Arc::new(client_config).into())
fn get_tls_connector() -> Result<RustlsTlsConnector> {
use rustls::ClientConfig;
let client_config = std::sync::Arc::new(
ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(get_root_certificates())
.with_no_client_auth(),
);
Ok(client_config.into())
}
#[instrument(skip_all, level = "info", err)]
@@ -132,20 +102,15 @@ pub async fn check_ldaps(ldaps_options: &LdapsOptions) -> Result<()> {
info!("LDAPS not enabled");
return Ok(());
};
let tls_connector =
get_tls_connector(ldaps_options).context("while preparing the tls connection")?;
let tls_connector = get_tls_connector()?;
let url = format!("localhost:{}", ldaps_options.port);
check_ldap_endpoint(
tls_connector
.connect(
rustls::ServerName::try_from("localhost")
.context("while parsing the server name")?,
TcpStream::connect(&url)
.await
.context("while connecting TCP")?,
rustls::ServerName::try_from(url.as_str())?,
TcpStream::connect(&url).await?,
)
.await
.context("while connecting TLS")?,
.await?,
)
.await
}

View File

@@ -4,8 +4,7 @@ use crate::{
opaque_handler::OpaqueHandler,
},
infra::{
access_control::AccessControlledBackendHandler,
configuration::{Configuration, LdapsOptions},
access_control::AccessControlledBackendHandler, configuration::Configuration,
ldap_handler::LdapHandler,
},
};
@@ -95,7 +94,7 @@ where
}
fn read_private_key(key_file: &str) -> Result<PrivateKey> {
use rustls_pemfile::{ec_private_keys, pkcs8_private_keys, rsa_private_keys};
use rustls_pemfile::{pkcs8_private_keys, rsa_private_keys};
use std::{fs::File, io::BufReader};
pkcs8_private_keys(&mut BufReader::new(File::open(key_file)?))
.map_err(anyhow::Error::from)
@@ -113,36 +112,29 @@ fn read_private_key(key_file: &str) -> Result<PrivateKey> {
.ok_or_else(|| anyhow!("No PKCS1 key"))
})
})
.or_else(|_| {
ec_private_keys(&mut BufReader::new(File::open(key_file)?))
.map_err(anyhow::Error::from)
.and_then(|keys| keys.into_iter().next().ok_or_else(|| anyhow!("No EC key")))
})
.with_context(|| {
format!(
"Cannot read either PKCS1, PKCS8 or EC private key from {}",
"Cannot read either PKCS1 or PKCS8 private key from {}",
key_file
)
})
.map(rustls::PrivateKey)
}
pub fn read_certificates(
ldaps_options: &LdapsOptions,
) -> Result<(Vec<rustls::Certificate>, rustls::PrivateKey)> {
fn get_tls_acceptor(config: &Configuration) -> Result<RustlsTlsAcceptor> {
use rustls::{Certificate, ServerConfig};
use rustls_pemfile::certs;
use std::{fs::File, io::BufReader};
let certs = rustls_pemfile::certs(&mut BufReader::new(File::open(&ldaps_options.cert_file)?))?
.into_iter()
.map(rustls::Certificate)
.collect::<Vec<_>>();
let private_key = read_private_key(&ldaps_options.key_file)?;
Ok((certs, private_key))
}
fn get_tls_acceptor(ldaps_options: &LdapsOptions) -> Result<RustlsTlsAcceptor> {
let (certs, private_key) = read_certificates(ldaps_options)?;
// Load TLS key and cert files
let certs = certs(&mut BufReader::new(File::open(
&config.ldaps_options.cert_file,
)?))?
.into_iter()
.map(Certificate)
.collect::<Vec<_>>();
let private_key = read_private_key(&config.ldaps_options.key_file)?;
let server_config = std::sync::Arc::new(
rustls::ServerConfig::builder()
ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(certs, private_key)?,
@@ -193,8 +185,7 @@ where
if config.ldaps_options.enabled {
let tls_context = (
context_for_tls,
get_tls_acceptor(&config.ldaps_options)
.context("while setting up the SSL certificate")?,
get_tls_acceptor(config).context("while setting up the SSL certificate")?,
);
let tls_binder = move || {
let tls_context = tls_context.clone();

View File

@@ -19,6 +19,7 @@ use actix_service::map_config;
use actix_web::{dev::AppConfig, guard, web, App, HttpResponse, Responder};
use anyhow::{Context, Result};
use hmac::Hmac;
use secstr::SecUtf8;
use sha2::Sha512;
use std::collections::HashSet;
use std::path::PathBuf;
@@ -38,10 +39,10 @@ pub enum TcpError {
BadRequest(String),
#[error("Internal server error: `{0}`")]
InternalServerError(String),
#[error("Not found: `{0}`")]
NotFoundError(String),
#[error("Unauthorized: `{0}`")]
UnauthorizedError(String),
#[error("Not implemented: `{0}`")]
NotImplemented(String),
}
pub type TcpResult<T> = std::result::Result<T, TcpError>;
@@ -60,9 +61,9 @@ pub(crate) fn error_to_http_response(error: TcpError) -> HttpResponse {
| DomainError::EntityNotFound(_) => HttpResponse::BadRequest(),
},
TcpError::BadRequest(_) => HttpResponse::BadRequest(),
TcpError::NotFoundError(_) => HttpResponse::NotFound(),
TcpError::InternalServerError(_) => HttpResponse::InternalServerError(),
TcpError::UnauthorizedError(_) => HttpResponse::Unauthorized(),
TcpError::NotImplemented(_) => HttpResponse::NotImplemented(),
}
.body(error.to_string())
}
@@ -88,6 +89,7 @@ fn http_config<Backend>(
jwt_blacklist: HashSet<u64>,
server_url: String,
mail_options: MailOptions,
hibp_api_key: SecUtf8,
) where
Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Clone + 'static,
{
@@ -98,6 +100,7 @@ fn http_config<Backend>(
jwt_blacklist: RwLock::new(jwt_blacklist),
server_url,
mail_options,
hibp_api_key,
}))
.route(
"/health",
@@ -133,6 +136,7 @@ pub(crate) struct AppState<Backend> {
pub jwt_blacklist: RwLock<HashSet<u64>>,
pub server_url: String,
pub mail_options: MailOptions,
pub hibp_api_key: SecUtf8,
}
impl<Backend: BackendHandler> AppState<Backend> {
@@ -173,6 +177,7 @@ where
let mail_options = config.smtp_options.clone();
let verbose = config.verbose;
info!("Starting the API/web server on port {}", config.http_port);
let hibp_api_key = config.hibp_api_key.clone();
server_builder
.bind(
"http",
@@ -183,6 +188,7 @@ where
let jwt_blacklist = jwt_blacklist.clone();
let server_url = server_url.clone();
let mail_options = mail_options.clone();
let hibp_api_key = hibp_api_key.clone();
HttpServiceBuilder::default()
.finish(map_config(
App::new()
@@ -198,6 +204,7 @@ where
jwt_blacklist,
server_url,
mail_options,
hibp_api_key,
)
}),
|_| AppConfig::default(),

View File

@@ -1,27 +0,0 @@
use crate::common::env;
use reqwest::blocking::Client;
pub fn get_token(client: &Client) -> String {
let username = env::admin_dn();
let password = env::admin_password();
let base_url = env::http_url();
let response = client
.post(format!("{base_url}/auth/simple/login"))
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(
serde_json::to_string(&lldap_auth::login::ClientSimpleLoginRequest {
username,
password,
})
.expect("Failed to encode the username/password as json to log in"),
)
.send()
.expect("Failed to send auth request")
.error_for_status()
.expect("Auth attempt failed");
serde_json::from_str::<lldap_auth::login::ServerLoginResponse>(
&response.text().expect("Failed to get response text"),
)
.expect("Failed to parse json")
.token
}

View File

@@ -1,36 +0,0 @@
#![allow(dead_code)]
use std::env::var;
pub const DB_KEY: &str = "LLDAP_DATABASE_URL";
pub fn database_url() -> String {
let url = var(DB_KEY).ok();
url.unwrap_or("sqlite://e2e_test.db?mode=rwc".to_string())
}
pub fn ldap_url() -> String {
let port = var("LLDAP_LDAP_PORT").ok();
let port = port.unwrap_or("3890".to_string());
format!("ldap://localhost:{}", port)
}
pub fn http_url() -> String {
let port = var("LLDAP_HTTP_PORT").ok();
let port = port.unwrap_or("17170".to_string());
format!("http://localhost:{}", port)
}
pub fn admin_dn() -> String {
let user = var("LLDAP_LDAP_USER_DN").ok();
user.unwrap_or("admin".to_string())
}
pub fn admin_password() -> String {
let pass = var("LLDAP_LDAP_USER_PASS").ok();
pass.unwrap_or("password".to_string())
}
pub fn base_dn() -> String {
let dn = var("LLDAP_LDAP_BASE_DN").ok();
dn.unwrap_or("dc=example,dc=com".to_string())
}

View File

@@ -1,240 +0,0 @@
use crate::common::{
auth::get_token,
env,
graphql::{
add_user_to_group, create_group, create_user, delete_group_query, delete_user_query, post,
AddUserToGroup, CreateGroup, CreateUser, DeleteGroupQuery, DeleteUserQuery,
},
};
use assert_cmd::prelude::*;
use nix::{
sys::signal::{self, Signal},
unistd::Pid,
};
use reqwest::blocking::{Client, ClientBuilder};
use std::collections::{HashMap, HashSet};
use std::process::{Child as ChildProcess, Command};
use std::{fs::canonicalize, thread, time::Duration};
use uuid::Uuid;
#[derive(Clone)]
pub struct User {
pub username: String,
pub groups: Vec<String>,
}
impl User {
pub fn new(username: &str, groups: Vec<&str>) -> Self {
let username = username.to_owned();
let groups = groups.iter().map(|username| username.to_string()).collect();
Self { username, groups }
}
}
pub struct LLDAPFixture {
token: String,
client: Client,
child: ChildProcess,
users: HashSet<String>,
groups: HashMap<String, i64>,
}
const MAX_HEALTHCHECK_ATTEMPS: u8 = 10;
impl LLDAPFixture {
pub fn new() -> Self {
let mut cmd = create_lldap_command();
cmd.arg("run");
cmd.arg("--verbose");
let child = cmd.spawn().expect("Unable to start server");
let mut started = false;
for _ in 0..MAX_HEALTHCHECK_ATTEMPS {
let status = create_lldap_command()
.arg("healthcheck")
.status()
.expect("healthcheck fail");
if status.success() {
started = true;
break;
}
thread::sleep(Duration::from_millis(1000));
}
assert!(started);
let client = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(2))
.timeout(std::time::Duration::from_secs(5))
.redirect(reqwest::redirect::Policy::none())
.build()
.expect("failed to make http client");
let token = get_token(&client);
Self {
client,
token,
child,
users: HashSet::new(),
groups: HashMap::new(),
}
}
pub fn load_state(&mut self, state: &Vec<User>) {
let mut users: HashSet<String> = HashSet::new();
let mut groups: HashSet<String> = HashSet::new();
for user in state {
users.insert(user.username.clone());
groups.extend(user.groups.clone());
}
for user in &users {
self.add_user(user);
}
for group in &groups {
self.add_group(group);
}
for User { username, groups } in state {
for group in groups {
self.add_user_to_group(username, group);
}
}
}
fn add_user(&mut self, user: &String) {
post::<CreateUser>(
&self.client,
&self.token,
create_user::Variables {
user: create_user::CreateUserInput {
id: user.clone(),
email: format!("{}@lldap.test", user),
avatar: None,
display_name: None,
first_name: None,
last_name: None,
},
},
)
.expect("failed to add user");
self.users.insert(user.clone());
}
fn add_group(&mut self, group: &str) {
let id = post::<CreateGroup>(
&self.client,
&self.token,
create_group::Variables {
name: group.to_owned(),
},
)
.expect("failed to add group")
.create_group
.id;
self.groups.insert(group.to_owned(), id);
}
fn delete_user(&mut self, user: &String) {
post::<DeleteUserQuery>(
&self.client,
&self.token,
delete_user_query::Variables { user: user.clone() },
)
.expect("failed to delete user");
self.users.remove(user);
}
fn delete_group(&mut self, group: &String) {
let group_id = self.groups.get(group).unwrap();
post::<DeleteGroupQuery>(
&self.client,
&self.token,
delete_group_query::Variables {
group_id: *group_id,
},
)
.expect("failed to delete group");
self.groups.remove(group);
}
fn add_user_to_group(&mut self, user: &str, group: &String) {
let group_id = self.groups.get(group).unwrap();
post::<AddUserToGroup>(
&self.client,
&self.token,
add_user_to_group::Variables {
user: user.to_owned(),
group: *group_id,
},
)
.expect("failed to add user to group");
}
}
impl Drop for LLDAPFixture {
fn drop(&mut self) {
let users = self.users.clone();
for user in users {
self.delete_user(&user);
}
let groups = self.groups.clone();
for group in groups.keys() {
self.delete_group(group);
}
let result = signal::kill(
Pid::from_raw(self.child.id().try_into().unwrap()),
Signal::SIGTERM,
);
if let Err(err) = result {
println!("Failed to send kill signal: {:?}", err);
let _ = self
.child
.kill()
.map_err(|err| println!("Failed to kill LLDAP: {:?}", err));
return;
}
for _ in 0..10 {
let status = self.child.try_wait();
if status.is_err() {}
match status {
Err(e) => {
println!(
"Failed to get status while waiting for graceful exit: {}",
e
);
break;
}
Ok(None) => {
println!("LLDAP still running, sleeping for 1 second.");
}
Ok(Some(status)) => {
if !status.success() {
println!("LLDAP exited with status {}", status)
}
return;
}
}
thread::sleep(Duration::from_millis(1000));
}
println!("LLDAP alive after 10 seconds, forcing exit.");
let _ = self
.child
.kill()
.map_err(|err| println!("Failed to kill LLDAP: {:?}", err));
}
}
pub fn new_id(prefix: Option<&str>) -> String {
let id = Uuid::new_v4();
let id = format!("{}-lldap-test", id.simple());
match prefix {
Some(prefix) => format!("{}{}", prefix, id),
None => id,
}
}
fn create_lldap_command() -> Command {
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).expect("cargo bin not found");
// This gives us the absolute path of the repo base instead of running it in server/
let path = canonicalize("..").expect("canonical path");
let db_url = env::database_url();
cmd.current_dir(path);
cmd.env(env::DB_KEY, db_url);
cmd
}

View File

@@ -1,121 +0,0 @@
use crate::common::env;
use anyhow::{anyhow, Context, Result};
use graphql_client::GraphQLQuery;
use reqwest::blocking::Client;
pub type DateTimeUtc = chrono::DateTime<chrono::Utc>;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "tests/queries/add_user_to_group.graphql",
response_derives = "Debug",
variables_derives = "Debug,Clone",
custom_scalars_module = "crate::common::graphql"
)]
pub struct AddUserToGroup;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "tests/queries/create_user.graphql",
response_derives = "Debug",
variables_derives = "Debug,Clone",
custom_scalars_module = "crate::common::graphql"
)]
pub struct CreateUser;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "tests/queries/create_group.graphql",
response_derives = "Debug",
variables_derives = "Debug,Clone",
custom_scalars_module = "crate::common::graphql"
)]
pub struct CreateGroup;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "tests/queries/list_users.graphql",
response_derives = "Debug",
custom_scalars_module = "crate::common::graphql"
)]
pub struct ListUsers;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "tests/queries/get_user_details.graphql",
response_derives = "Debug",
variables_derives = "Debug,Clone",
custom_scalars_module = "crate::common::graphql"
)]
pub struct GetUserDetails;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "tests/queries/list_groups.graphql",
response_derives = "Debug",
custom_scalars_module = "crate::common::graphql"
)]
pub struct ListGroups;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "tests/queries/delete_group.graphql",
response_derives = "Debug",
custom_scalars_module = "crate::common::graphql"
)]
pub struct DeleteGroupQuery;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "tests/queries/delete_user.graphql",
response_derives = "Debug",
custom_scalars_module = "crate::common::graphql"
)]
pub struct DeleteUserQuery;
pub fn post<QueryType>(
client: &Client,
token: &String,
variables: QueryType::Variables,
) -> Result<QueryType::ResponseData>
where
QueryType: GraphQLQuery + 'static,
{
let unwrap_graphql_response = |graphql_client::Response { data, errors, .. }| {
data.ok_or_else(|| {
anyhow!(
"Errors: [{}]",
errors
.unwrap_or_default()
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
)
})
};
let url = env::http_url() + "/api/graphql";
let auth_header = format!("Bearer {}", token);
client
.post(url)
.header(reqwest::header::AUTHORIZATION, auth_header)
// Request body.
.json(&QueryType::build_query(variables))
.send()
.context("while sending a request to the LLDAP server")?
.error_for_status()
.context("error from an LLDAP response")?
// Parse response as Json.
.json::<graphql_client::Response<QueryType::ResponseData>>()
.context("while parsing backend response")
.and_then(unwrap_graphql_response)
.context("GraphQL error from an LLDAP response")
}

View File

@@ -1,4 +0,0 @@
pub mod auth;
pub mod env;
pub mod fixture;
pub mod graphql;

View File

@@ -1,70 +0,0 @@
use crate::common::{
auth::get_token,
env,
fixture::{new_id, LLDAPFixture, User},
graphql::{get_user_details, list_users, post, GetUserDetails, ListUsers},
};
use reqwest::blocking::ClientBuilder;
use serial_test::file_serial;
use std::collections::HashSet;
mod common;
#[test]
#[file_serial]
fn list_users() {
let mut fixture = LLDAPFixture::new();
let prefix = "graphql-list_users-";
let user1_name = new_id(Some(prefix));
let user2_name = new_id(Some(prefix));
let user3_name = new_id(Some(prefix));
let group1_name = new_id(Some(prefix));
let group2_name = new_id(Some(prefix));
let initial_state = vec![
User::new(&user1_name, vec![&group1_name]),
User::new(&user2_name, vec![&group1_name, &group2_name]),
User::new(&user3_name, vec![]),
];
fixture.load_state(&initial_state);
let client = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(2))
.timeout(std::time::Duration::from_secs(5))
.redirect(reqwest::redirect::Policy::none())
.build()
.expect("failed to make http client");
let token = get_token(&client);
let result =
post::<ListUsers>(&client, &token, list_users::Variables {}).expect("failed to list users");
let users: HashSet<String> = result.users.iter().map(|user| user.id.clone()).collect();
assert!(users.contains(&user1_name));
assert!(users.contains(&user2_name));
assert!(users.contains(&user3_name));
}
#[test]
#[file_serial]
fn get_admin() {
let mut _fixture = LLDAPFixture::new();
let client = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(2))
.timeout(std::time::Duration::from_secs(5))
.redirect(reqwest::redirect::Policy::none())
.build()
.expect("failed to make http client");
let admin_name = env::admin_dn();
let admin_group_name = "lldap_admin";
let token = get_token(&client);
let result = post::<GetUserDetails>(
&client,
&token,
get_user_details::Variables { id: admin_name },
)
.expect("failed to get admin");
let admin_groups: HashSet<String> = result
.user
.groups
.iter()
.map(|group| group.display_name.clone())
.collect();
assert!(admin_groups.contains(admin_group_name));
}

View File

@@ -1,57 +0,0 @@
use std::collections::HashSet;
use crate::common::{
env,
fixture::{new_id, LLDAPFixture, User},
};
use ldap3::{LdapConn, Scope, SearchEntry};
use serial_test::file_serial;
mod common;
#[test]
#[file_serial]
fn gitea() {
let mut fixture = LLDAPFixture::new();
let gitea_user_group = new_id(Some("gitea_user-"));
let gitea_admin_group = new_id(Some("gitea_admin-"));
let gitea_user1 = new_id(Some("gitea1-"));
let gitea_user2 = new_id(Some("gitea2-"));
let gitea_user3 = new_id(Some("gitea3-"));
let initial_state = vec![
User::new(&gitea_user1, vec![&gitea_user_group, &gitea_admin_group]),
User::new(&gitea_user2, vec![&gitea_user_group]),
User::new(&gitea_user3, vec![]),
];
fixture.load_state(&initial_state);
let mut ldap =
LdapConn::new(env::ldap_url().as_str()).expect("failed to create ldap connection");
let base_dn = env::base_dn();
let bind_dn = format!("uid={},ou=people,{}", env::admin_dn(), base_dn);
ldap.simple_bind(bind_dn.as_str(), env::admin_password().as_str())
.expect("failed to bind to ldap");
let user_base = format!("ou=people,{}", base_dn);
let attrs = vec!["uid", "givenName", "sn", "mail", "jpegPhoto"];
let results = ldap
.search(
user_base.as_str(),
Scope::Subtree,
format!("(memberof=cn={},ou=groups,{})", gitea_user_group, base_dn).as_str(),
attrs,
)
.expect("failed to find gitea users")
.success()
.expect("failed to get gitea user results")
.0;
let mut found_users: HashSet<String> = HashSet::new();
for result in results {
let attrs = SearchEntry::construct(result).attrs;
let user = attrs.get("uid").unwrap().get(0).unwrap();
found_users.insert(user.clone());
}
assert!(found_users.contains(&gitea_user1));
assert!(found_users.contains(&gitea_user2));
assert!(!found_users.contains(&gitea_user3));
ldap.unbind().expect("failed to unbind ldap connection");
}

View File

@@ -1,112 +0,0 @@
use std::collections::{HashMap, HashSet};
use crate::common::{
env,
fixture::{new_id, LLDAPFixture, User},
};
use ldap3::{LdapConn, Scope, SearchEntry, SearchResult};
use serial_test::file_serial;
mod common;
#[test]
#[file_serial]
fn basic_users_search() {
let mut fixture = LLDAPFixture::new();
let prefix = "ldap-basic_users_search-";
let user1_name = new_id(Some(prefix));
let user2_name = new_id(Some(prefix));
let user3_name = new_id(Some(prefix));
let group1_name = new_id(Some(prefix));
let group2_name = new_id(Some(prefix));
let initial_state = vec![
User::new(&user1_name, vec![&group1_name]),
User::new(&user2_name, vec![&group1_name, &group2_name]),
User::new(&user3_name, vec![]),
];
fixture.load_state(&initial_state);
let mut ldap =
LdapConn::new(env::ldap_url().as_str()).expect("failed to create ldap connection");
let base_dn = env::base_dn();
let bind_dn = format!("uid={},ou=people,{}", env::admin_dn(), base_dn);
ldap.simple_bind(bind_dn.as_str(), env::admin_password().as_str())
.expect("failed to bind to ldap");
let attrs = vec!["uid", "memberof"];
let found_users = get_users_and_groups(
ldap.search(
env::base_dn().as_str(),
Scope::Subtree,
"(objectclass=person)",
attrs,
)
.expect("failed to find users"),
);
assert!(found_users.contains_key(&user1_name));
assert!(found_users
.get(&user1_name)
.unwrap()
.contains(format!("cn={},ou=groups,{}", &group1_name, base_dn).as_str()));
assert!(found_users.contains_key(&user2_name));
assert!(found_users
.get(&user2_name)
.unwrap()
.contains(format!("cn={},ou=groups,{}", &group1_name, base_dn).as_str()));
assert!(found_users
.get(&user2_name)
.unwrap()
.contains(format!("cn={},ou=groups,{}", &group2_name, base_dn).as_str()));
assert!(found_users.contains_key(&user3_name));
assert!(found_users.get(&user3_name).unwrap().is_empty());
ldap.unbind().expect("failed to unbind ldap connection");
}
#[test]
#[file_serial]
fn admin_search() {
let mut _fixture = LLDAPFixture::new();
let mut ldap =
LdapConn::new(env::ldap_url().as_str()).expect("failed to create ldap connection");
let base_dn = env::base_dn();
let bind_dn = format!("uid={},ou=people,{}", env::admin_dn(), base_dn);
ldap.simple_bind(bind_dn.as_str(), env::admin_password().as_str())
.expect("failed to bind to ldap");
let attrs = vec!["uid", "memberof"];
let admin_name = env::admin_dn();
let admin_group_name = "lldap_admin";
let found_users = get_users_and_groups(
ldap.search(
env::base_dn().as_str(),
Scope::Subtree,
format!("(&(objectclass=person)(uid={}))", admin_name).as_str(),
attrs,
)
.expect("failed to find admin"),
);
assert!(found_users.contains_key(&admin_name));
assert!(found_users
.get(&admin_name)
.unwrap()
.contains(format!("cn={},ou=groups,{}", admin_group_name, base_dn).as_str()));
ldap.unbind().expect("failed to unbind ldap connection");
}
fn get_users_and_groups(results: SearchResult) -> HashMap<String, HashSet<String>> {
let results = results
.success()
.expect("failed to get successful result")
.0;
let mut found_users: HashMap<String, HashSet<String>> = HashMap::new();
for result in results {
let attrs = SearchEntry::construct(result).attrs;
let user = attrs.get("uid").unwrap().get(0).unwrap();
let user_groups = attrs.get("memberof").unwrap().clone();
let mut groups: HashSet<String> = HashSet::new();
groups.extend(user_groups.clone());
found_users.insert(user.clone(), groups);
}
found_users
}

View File

@@ -1,5 +0,0 @@
mutation AddUserToGroup($user: String!, $group: Int!) {
addUserToGroup(userId: $user, groupId: $group) {
ok
}
}

View File

@@ -1,6 +0,0 @@
mutation CreateGroup($name: String!) {
createGroup(name: $name) {
id
displayName
}
}

View File

@@ -1,5 +0,0 @@
mutation CreateUser($user: CreateUserInput!) {
createUser(user: $user) {
id
}
}

View File

@@ -1,5 +0,0 @@
mutation DeleteGroupQuery($groupId: Int!) {
deleteGroup(groupId: $groupId) {
ok
}
}

View File

@@ -1,5 +0,0 @@
mutation DeleteUserQuery($user: String!) {
deleteUser(userId: $user) {
ok
}
}

View File

@@ -1,16 +0,0 @@
query GetUserDetails($id: String!) {
user(userId: $id) {
id
email
displayName
firstName
lastName
avatar
creationDate
uuid
groups {
id
displayName
}
}
}

View File

@@ -1,9 +0,0 @@
query ListGroups {
groups {
id
displayName
users {
id
}
}
}

View File

@@ -1,5 +0,0 @@
query ListUsers {
users(filters: null) {
id
}
}

View File

@@ -1,17 +1,12 @@
[package]
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
description = "CLI tool to set a user password in LLDAP"
edition = "2021"
homepage = "https://github.com/lldap/lldap"
license = "GPL-3.0-only"
name = "lldap_set_password"
repository = "https://github.com/lldap/lldap"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1"
anyhow = "*"
rand = "0.8"
serde = "1"
serde_json = "1"
@@ -21,10 +16,10 @@ features = ["std", "color", "suggestions", "derive", "env"]
version = "4"
[dependencies.lldap_auth]
version = "0.3"
path = "../auth"
features = ["opaque_client"]
[dependencies.reqwest]
version = "0.11"
version = "*"
default-features = false
features = ["json", "blocking", "rustls-tls"]