From 14531fa258ea698cefa46b0fb4ce2e1993ddca7d Mon Sep 17 00:00:00 2001 From: Austin Alvarado Date: Wed, 4 Jan 2023 00:24:40 -0700 Subject: [PATCH 01/62] docker: upgrade alpine in base dockerfile This allows us to upgrade rustc to past 1.65, which is required by sea-orm. --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6eda8dc..8eac2d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build image -FROM rust:alpine3.14 AS chef +FROM rust:alpine3.16 AS chef RUN set -x \ # Add user @@ -41,7 +41,7 @@ RUN cargo build --release -p lldap -p migration-tool \ && ./app/build.sh # Final image -FROM alpine:3.14 +FROM alpine:3.16 ENV GOSU_VERSION 1.14 # Fetch gosu from git From d7cc10fa006765330ab2c06fd7998cd766394f21 Mon Sep 17 00:00:00 2001 From: Dedy Martadinata S Date: Thu, 5 Jan 2023 21:36:01 +0700 Subject: [PATCH 02/62] ci: fetch missing web components --- .github/workflows/docker-build-static.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build-static.yml b/.github/workflows/docker-build-static.yml index 56d417c..fa92ff3 100644 --- a/.github/workflows/docker-build-static.yml +++ b/.github/workflows/docker-build-static.yml @@ -465,6 +465,13 @@ jobs: path: web - name: Web Cleanup run: mkdir app && mv web/index.html app/index.html && mv web/static app/static && mv web/pkg app/pkg + - name: Fetch web components + run: | + sudo apt update + sudo apt install wget + for file in $(cat app/static/libraries.txt); do wget -P app/static "$file"; done + for file in $(cat app/static/fonts/fonts.txt); do wget -P app/static/fonts "$file"; done + chmod a+r -R . - name: compress web run: sudo apt update && sudo apt install -y zip && zip -r web.zip app/ @@ -474,12 +481,12 @@ jobs: id: create_release with: allowUpdates: true - artifacts: "bin/armhf-bin/lldap-armhf, + artifacts: bin/armhf-bin/lldap-armhf, bin/aarch64-bin/lldap-aarch64, bin/amd64-bin/lldap-amd64, bin/armhf-bin/migration-tool-armhf, bin/aarch64-bin/migration-tool-aarch64, bin/amd64-bin/migration-tool-amd64, - web.zip" + web.zip env: GITHUB_TOKEN: ${{ github.token }} From c87adfeeccc703826d641dfb8ffc459026829608 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Jan 2023 13:13:54 +0100 Subject: [PATCH 03/62] build(deps): bump actions/checkout from 3.2.0 to 3.3.0 (#410) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.2.0 to 3.3.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3.2.0...v3.3.0) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker-build-static.yml | 12 ++++++------ .github/workflows/rust.yml | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docker-build-static.yml b/.github/workflows/docker-build-static.yml index fa92ff3..f7eac75 100644 --- a/.github/workflows/docker-build-static.yml +++ b/.github/workflows/docker-build-static.yml @@ -80,7 +80,7 @@ jobs: restore-keys: | lldap-ui- - name: Checkout repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: install rollup nodejs run: npm install -g rollup - name: install wasm-pack with cargo @@ -119,7 +119,7 @@ jobs: - name: smoke test run: rustc --version - name: Checkout repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - uses: actions/cache@v3 with: path: | @@ -164,11 +164,11 @@ jobs: CARGO_HOME: ${GITHUB_WORKSPACE}/.cargo steps: - name: Checkout repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: smoke test run: rustc --version - name: Checkout repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - uses: actions/cache@v3 with: path: | @@ -214,7 +214,7 @@ jobs: CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER: x86_64-linux-musl-gcc steps: - name: Checkout repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - uses: actions/cache@v3 with: path: | @@ -262,7 +262,7 @@ jobs: - name: install rsync run: sudo apt update && sudo apt install -y rsync - name: fetch repo - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Download armhf lldap artifacts uses: actions/download-artifact@v3 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0825b37..a5952d1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -34,7 +34,7 @@ jobs: steps: - name: Checkout sources - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - uses: Swatinem/rust-cache@v2 - name: Build run: cargo build --verbose --workspace @@ -53,7 +53,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - uses: Swatinem/rust-cache@v2 @@ -70,7 +70,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - uses: Swatinem/rust-cache@v2 @@ -87,7 +87,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.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 From 3a43b7a4c2156b603c9d98508828e048d6ed5009 Mon Sep 17 00:00:00 2001 From: Dedy Martadinata S Date: Fri, 6 Jan 2023 22:34:22 +0700 Subject: [PATCH 04/62] docker: simplify ci and better package release artifacts --- .github/workflows/Dockerfile.ci.alpine | 12 +- .github/workflows/Dockerfile.ci.debian | 12 +- .github/workflows/docker-build-static.yml | 193 ++++++++-------------- 3 files changed, 78 insertions(+), 139 deletions(-) diff --git a/.github/workflows/Dockerfile.ci.alpine b/.github/workflows/Dockerfile.ci.alpine index 0d074c3..47f778f 100644 --- a/.github/workflows/Dockerfile.ci.alpine +++ b/.github/workflows/Dockerfile.ci.alpine @@ -10,8 +10,8 @@ RUN mkdir -p target/ RUN mkdir -p /lldap/app RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \ - mv bin/amd64-bin/lldap target/lldap && \ - mv bin/amd64-bin/migration-tool target/migration-tool && \ + mv bin/amd64-lldap-bin/lldap target/lldap && \ + mv bin/amd64-migration-tool-bin/migration-tool target/migration-tool && \ chmod +x target/lldap && \ chmod +x target/migration-tool && \ ls -la target/ . && \ @@ -19,8 +19,8 @@ RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \ ; fi RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \ - mv bin/aarch64-bin/lldap target/lldap && \ - mv bin/aarch64-bin/migration-tool target/migration-tool && \ + mv bin/aarch64-lldap-bin/lldap target/lldap && \ + mv bin/aarch64-migration-tool-bin/migration-tool target/migration-tool && \ chmod +x target/lldap && \ chmod +x target/migration-tool && \ ls -la target/ . && \ @@ -28,8 +28,8 @@ RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \ ; fi RUN if [ "${TARGETPLATFORM}" = "linux/arm/v7" ]; then \ - mv bin/armhf-bin/lldap target/lldap && \ - mv bin/armhf-bin/migration-tool target/migration-tool && \ + mv bin/armhf-lldap-bin/lldap target/lldap && \ + mv bin/armhf-migration-tool-bin/migration-tool target/migration-tool && \ chmod +x target/lldap && \ chmod +x target/migration-tool && \ ls -la target/ . && \ diff --git a/.github/workflows/Dockerfile.ci.debian b/.github/workflows/Dockerfile.ci.debian index 03cdbfc..3f7c45b 100644 --- a/.github/workflows/Dockerfile.ci.debian +++ b/.github/workflows/Dockerfile.ci.debian @@ -10,8 +10,8 @@ RUN mkdir -p target/ RUN mkdir -p /lldap/app RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \ - mv bin/amd64-bin/lldap target/lldap && \ - mv bin/amd64-bin/migration-tool target/migration-tool && \ + mv bin/amd64-lldap-bin/lldap target/lldap && \ + mv bin/amd64-migration-tool-bin/migration-tool target/migration-tool && \ chmod +x target/lldap && \ chmod +x target/migration-tool && \ ls -la target/ . && \ @@ -19,8 +19,8 @@ RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \ ; fi RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \ - mv bin/aarch64-bin/lldap target/lldap && \ - mv bin/aarch64-bin/migration-tool target/migration-tool && \ + mv bin/aarch64-lldap-bin/lldap target/lldap && \ + mv bin/aarch64-migration-tool-bin/migration-tool target/migration-tool && \ chmod +x target/lldap && \ chmod +x target/migration-tool && \ ls -la target/ . && \ @@ -28,8 +28,8 @@ RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \ ; fi RUN if [ "${TARGETPLATFORM}" = "linux/arm/v7" ]; then \ - mv bin/armhf-bin/lldap target/lldap && \ - mv bin/armhf-bin/migration-tool target/migration-tool && \ + mv bin/armhf-lldap-bin/lldap target/lldap && \ + mv bin/armhf-migration-tool-bin/migration-tool target/migration-tool && \ chmod +x target/lldap && \ chmod +x target/migration-tool && \ ls -la target/ . && \ diff --git a/.github/workflows/docker-build-static.yml b/.github/workflows/docker-build-static.yml index f7eac75..cc8146c 100644 --- a/.github/workflows/docker-build-static.yml +++ b/.github/workflows/docker-build-static.yml @@ -22,16 +22,19 @@ env: # In total 5 jobs, all the jobs are containerized # --- +####################################################################################### +# GitHub actions randomly timeout when downloading musl-gcc # +# Using lldap dev image based on https://hub.docker.com/_/rust and musl-gcc bundled # +# Look into .github/workflows/Dockerfile.dev for development image details # +####################################################################################### + # build-ui , create/compile the web -## Use rustlang/rust:nighlty image -### Install nodejs from nodesource repo ### install wasm ### install rollup ### run app/build.sh ### upload artifacts # builds-armhf, build-aarch64, build-amd64 create binary for respective arch -## Use rustlang/rust:nightly image ### Add non-native architecture dpkg --add-architecture XXX ### Install dev tool gcc g++, etc. per respective arch ### Cargo build @@ -44,30 +47,16 @@ env: # build-ui,builds-armhf, build-aarch64, build-amd64 will upload artifacts will be used next job # build-docker-image job will fetch artifacts and run Dockerfile.ci then push the image. -# On current https://hub.docker.com/_/rust -# 1-bullseye, 1.61-bullseye, 1.61.0-bullseye, bullseye, 1, 1.61, 1.61.0, latest - -# cache -## cargo -## target +# cache based on Cargo.lock jobs: build-ui: runs-on: ubuntu-latest container: - image: rust:1.65 - env: - CARGO_TERM_COLOR: always - RUSTFLAGS: -Ctarget-feature=+crt-static + image: nitnelave/rust-dev:latest steps: - - name: install runtime - run: apt update && apt install -y gcc-x86-64-linux-gnu g++-x86-64-linux-gnu libc6-dev ca-certificates - - name: setup node repo LTS - run: curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - - - name: install nodejs - run: apt install -y nodejs && npm -g install npm - - name: smoke test - run: rustc --version + - name: Checkout repository + uses: actions/checkout@v3.3.0 - uses: actions/cache@v3 with: path: | @@ -79,10 +68,10 @@ jobs: key: lldap-ui-${{ hashFiles('**/Cargo.lock') }} restore-keys: | lldap-ui- - - name: Checkout repository - uses: actions/checkout@v3.3.0 - name: install rollup nodejs run: npm install -g rollup + - name: add wasm target + run: rustup target add wasm32-unknown-unknown - name: install wasm-pack with cargo run: cargo install wasm-pack || true env: @@ -100,7 +89,7 @@ jobs: build-armhf: runs-on: ubuntu-latest container: - image: rust:1.65 + image: nitnelave/rust-dev:latest env: CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER: arm-linux-gnueabihf-gcc CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER: arm-linux-gnueabihf-ld @@ -112,12 +101,8 @@ jobs: run: dpkg --add-architecture armhf - name: install runtime run: apt update && apt install -y gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf libc6-armhf-cross libc6-dev-armhf-cross tar ca-certificates - - name: smoke test - run: rustc --version - name: add armhf target run: rustup target add armv7-unknown-linux-gnueabihf - - name: smoke test - run: rustc --version - name: Checkout repository uses: actions/checkout@v3.3.0 - uses: actions/cache@v3 @@ -150,12 +135,6 @@ jobs: build-aarch64: runs-on: ubuntu-latest container: -################################################################################## -# GitHub actions currently timeout when downloading musl-gcc # -# Using lldap dev image based on rust:1.65-slim-bullseye and musl-gcc bundled # -# Only for Job build aarch64 and amd64 # -################################################################################### - #image: rust:1.65 image: nitnelave/rust-dev:latest env: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: aarch64-linux-musl-gcc @@ -163,10 +142,6 @@ jobs: RUSTFLAGS: -Ctarget-feature=+crt-static CARGO_HOME: ${GITHUB_WORKSPACE}/.cargo steps: - - name: Checkout repository - uses: actions/checkout@v3.3.0 - - name: smoke test - run: rustc --version - name: Checkout repository uses: actions/checkout@v3.3.0 - uses: actions/cache@v3 @@ -205,7 +180,6 @@ jobs: build-amd64: runs-on: ubuntu-latest container: -# image: rust:1.65 image: nitnelave/rust-dev:latest env: CARGO_TERM_COLOR: always @@ -263,42 +237,10 @@ jobs: run: sudo apt update && sudo apt install -y rsync - name: fetch repo uses: actions/checkout@v3.3.0 - - - name: Download armhf lldap artifacts + - name: Download All Artifacts uses: actions/download-artifact@v3 with: - name: armhf-lldap-bin - path: bin/armhf-bin - - name: Download armhf migration-tool artifacts - uses: actions/download-artifact@v3 - with: - name: armhf-migration-tool-bin - path: bin/armhf-bin - - - name: Download aarch64 lldap artifacts - uses: actions/download-artifact@v3 - with: - name: aarch64-lldap-bin - path: bin/aarch64-bin - - name: Download aarch64 migration-tool artifacts - uses: actions/download-artifact@v3 - with: - name: aarch64-migration-tool-bin - path: bin/aarch64-bin - - - name: Download amd64 lldap artifacts - uses: actions/download-artifact@v3 - with: - name: amd64-lldap-bin - path: bin/amd64-bin - - name: Download amd64 migration-tool artifacts - uses: actions/download-artifact@v3 - with: - name: amd64-migration-tool-bin - path: bin/amd64-bin - - - name: check bin path - run: ls -al bin/ + path: bin - name: Download llap ui artifacts uses: actions/download-artifact@v3 @@ -326,7 +268,7 @@ jobs: type=semver,pattern={{major}} type=sha - name: Cache Docker layers - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: /tmp/.buildx-cache key: ${{ runner.os }}-buildx-${{ github.sha }} @@ -344,9 +286,9 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} -###################### -#### latest build #### -###################### +######################################## +#### docker image :latest tag build #### +######################################## - name: Build and push latest alpine if: github.event_name != 'release' uses: docker/build-push-action@v3 @@ -371,9 +313,9 @@ jobs: cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache-new -####################### -#### release build #### -####################### +######################################## +#### docker image :semver tag build #### +######################################## - name: Build and push release alpine if: github.event_name == 'release' uses: docker/build-push-action@v3 @@ -411,52 +353,31 @@ jobs: password: ${{ secrets.DOCKERHUB_PASSWORD }} repository: nitnelave/lldap - +############################################################### +### Download artifacts, clean up ui, upload to release page ### +############################################################### create-release-artifacts: needs: [build-ui,build-armhf,build-aarch64,build-amd64] name: Create release artifacts if: github.event_name == 'release' runs-on: ubuntu-latest steps: - - - name: Download armhf lldap artifacts + - name: Download All Artifacts uses: actions/download-artifact@v3 with: - name: armhf-lldap-bin - path: bin/armhf-bin - - name: Download armhf migration-tool artifacts - uses: actions/download-artifact@v3 - with: - name: armhf-migration-tool-bin - path: bin/armhf-bin - - name: Fix binary name armhf - run: mv bin/armhf-bin/lldap bin/armhf-bin/lldap-armhf && mv bin/armhf-bin/migration-tool bin/armhf-bin/migration-tool-armhf - - - name: Download aarch64 lldap artifacts - uses: actions/download-artifact@v3 - with: - name: aarch64-lldap-bin - path: bin/aarch64-bin - - name: Download aarch64 migration-tool artifacts - uses: actions/download-artifact@v3 - with: - name: aarch64-migration-tool-bin - path: bin/aarch64-bin - - name: Fix binary name aarch64 - run: mv bin/aarch64-bin/lldap bin/aarch64-bin/lldap-aarch64 && mv bin/aarch64-bin/migration-tool bin/aarch64-bin/migration-tool-aarch64 - - - name: Download amd64 lldap artifacts - uses: actions/download-artifact@v3 - with: - name: amd64-lldap-bin - path: bin/amd64-bin - - name: Download amd64 migration-tool artifacts - uses: actions/download-artifact@v3 - with: - name: amd64-migration-tool-bin - path: bin/amd64-bin - - name: Fix binary name amd64 - run: mv bin/amd64-bin/lldap bin/amd64-bin/lldap-amd64 && mv bin/amd64-bin/migration-tool bin/amd64-bin/migration-tool-amd64 + path: bin/ + - name: Check file + run: ls -alR bin/ + - name: Fixing Filename + run: | + mv bin/aarch64-lldap-bin/lldap bin/aarch64-lldap + mv bin/amd64-lldap-bin/lldap bin/amd64-lldap + mv bin/armhf-lldap-bin/lldap bin/armhf-lldap + mv bin/aarch64-migration-tool-bin/migration-tool bin/aarch64-migration-tool + mv bin/amd64-migration-tool-bin/migration-tool bin/amd64-migration-tool + mv bin/armhf-migration-tool-bin/migration-tool bin/armhf-migration-tool + chmod +x bin/*-lldap + chmod +x bin/*-migration-tool - name: Download llap ui artifacts uses: actions/download-artifact@v3 @@ -472,8 +393,30 @@ jobs: for file in $(cat app/static/libraries.txt); do wget -P app/static "$file"; done for file in $(cat app/static/fonts/fonts.txt); do wget -P app/static/fonts "$file"; done chmod a+r -R . - - name: compress web - run: sudo apt update && sudo apt install -y zip && zip -r web.zip app/ + + - name: Setup LLDAP dir for packing + run: | + mkdir aarch64-lldap + mkdir amd64-lldap + mkdir armhf-lldap + mv bin/aarch64-lldap aarch64-lldap/lldap + mv bin/amd64-lldap amd64-lldap/lldap + mv bin/armhf-lldap armhf-lldap/lldap + 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 + cp -r app aarch64-lldap/ + cp -r app amd64-lldap/ + cp -r app armhf-lldap/ + ls -alR aarch64-lldap/ + ls -alR amd64-lldap/ + ls -alR armhf-lldap/ + + - name: Compress + run: | + tar -czvf aarch64-lldap.tar.gz aarch64-lldap/ + tar -czvf amd64-lldap.tar.gz amd64-lldap/ + tar -czvf armhf-lldap.tar.gz armhf-lldap/ - name: Upload artifacts release @@ -481,12 +424,8 @@ jobs: id: create_release with: allowUpdates: true - artifacts: bin/armhf-bin/lldap-armhf, - bin/aarch64-bin/lldap-aarch64, - bin/amd64-bin/lldap-amd64, - bin/armhf-bin/migration-tool-armhf, - bin/aarch64-bin/migration-tool-aarch64, - bin/amd64-bin/migration-tool-amd64, - web.zip + artifacts: aarch64-lldap.tar.gz, + amd64-lldap.tar.gz, + armhf-lldap.tar.gz env: GITHUB_TOKEN: ${{ github.token }} From 260b545a54d6ecd41e2cdd5ae4fdd50c0cf407f8 Mon Sep 17 00:00:00 2001 From: poVoq Date: Mon, 9 Jan 2023 15:53:44 -0100 Subject: [PATCH 05/62] example_configs,gitea: add additional attributes and group sync Not extensively tested, but group/team sync seems to work in Forgejo. --- example_configs/gitea.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/example_configs/gitea.md b/example_configs/gitea.md index b2c8a54..654d76e 100644 --- a/example_configs/gitea.md +++ b/example_configs/gitea.md @@ -1,4 +1,4 @@ -# Configuration for Gitea +# Configuration for Gitea (& Forgejo) In Gitea, go to `Site Administration > Authentication Sources` and click `Add Authentication Source` Select `LDAP (via BindDN)` @@ -14,9 +14,30 @@ To log in they can either use their email address or user name. If you only want For more info on the user filter, see: https://docs.gitea.io/en-us/authentication/#ldap-via-binddn * Admin Filter: Use `(memberof=cn=lldap_admin,ou=groups,dc=example,dc=com)` if you want lldap admins to become Gitea admins. Leave empty otherwise. * Username Attribute: `uid` +* First Name Attribute: `givenName` +* Surname Attribute: `sn` * Email Attribute: `mail` +* Avatar Attribute: `jpegPhoto` * Check `Enable User Synchronization` Replace every instance of `dc=example,dc=com` with your configured domain. -After applying the above settings, users should be able to log in with either their user name or email address. \ No newline at end of file +After applying the above settings, users should be able to log in with either their user name or email address. + +## Syncronizing LDAP groups with existing teams in organisations + +Groups in LLDAP can be syncronized with teams in organisations. Organisations and teams must be created manually in Gitea. +It is possible to syncronize one LDAP group with multiple teams in a Gitea organization. + +Check `Enable LDAP Groups` + +* Group Search Base DN: `ou=groups,dc=example,dc=com` +* Group Attribute Containing List Of Users: `member` +* User Attribute Listed In Group: `dn` +* Map LDAP groups to Organization teams: `{"cn=Groupname1,ou=groups,dc=example,dc=com":{"Organization1": ["Teamname"]},"cn=Groupname2,ou=groups,dc=example,dc=com": {"Organization2": ["Teamname1", "Teamname2"]}}` + +Check `Remove Users from syncronised teams...` + +The `Map LDAP groups to Organization teams` config is JSON formatted and can be extended to as many groups as needed. + +Replace every instance of `dc=example,dc=com` with your configured domain. From 692bbb00f1ba9b43a10e5cb6a0fcf01c002969ef Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Fri, 13 Jan 2023 15:01:33 +0100 Subject: [PATCH 06/62] db: Change the version number from u8 to i16 This is the smallest integer compatible with all of MySQL, Postgres and SQlite. This is a backwards-compatible change for SQlite since both are represented as "integer", and all u8 values can be represented as i16. --- server/src/domain/sql_migrations.rs | 3 ++- server/src/domain/sql_tables.rs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/domain/sql_migrations.rs b/server/src/domain/sql_migrations.rs index 62d9e59..ff5bb21 100644 --- a/server/src/domain/sql_migrations.rs +++ b/server/src/domain/sql_migrations.rs @@ -116,6 +116,7 @@ pub async fn upgrade_to_v1(pool: &DbConnection) -> std::result::Result<(), sea_o .col( ColumnDef::new(Groups::GroupId) .integer() + .auto_increment() .not_null() .primary_key(), ) @@ -309,7 +310,7 @@ pub async fn upgrade_to_v1(pool: &DbConnection) -> std::result::Result<(), sea_o Table::create() .table(Metadata::Table) .if_not_exists() - .col(ColumnDef::new(Metadata::Version).tiny_integer()), + .col(ColumnDef::new(Metadata::Version).small_integer()), ), ) .await?; diff --git a/server/src/domain/sql_tables.rs b/server/src/domain/sql_tables.rs index af5615a..1bc4f77 100644 --- a/server/src/domain/sql_tables.rs +++ b/server/src/domain/sql_tables.rs @@ -4,7 +4,7 @@ use sea_orm::Value; pub type DbConnection = sea_orm::DatabaseConnection; #[derive(Copy, PartialEq, Eq, Debug, Clone)] -pub struct SchemaVersion(pub u8); +pub struct SchemaVersion(pub i16); impl sea_orm::TryGetable for SchemaVersion { fn try_get( @@ -12,7 +12,7 @@ impl sea_orm::TryGetable for SchemaVersion { pre: &str, col: &str, ) -> Result { - Ok(SchemaVersion(u8::try_get(res, pre, col)?)) + Ok(SchemaVersion(i16::try_get(res, pre, col)?)) } } From e458aca3e39048e057763ed2fe32ed2fffb978ca Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Fri, 13 Jan 2023 15:09:25 +0100 Subject: [PATCH 07/62] db: Change the DB storage type to NaiveDateTime The entire internals of the server now work using only NaiveDateTime, since we know they are all UTC. At the fringes (LDAP, GraphQL, JWT tokens) we convert back into UTC to make sure we have a clear API. This allows us to be compatible with Postgres (which doesn't support DateTime, only NaiveDateTime). This change is backwards compatible since in SQlite with Sea-query/Sea-ORM, the UTC datetimes are stored without a timezone, as simple strings. It's the same format as NaiveDateTime. Fixes #87. --- server/src/domain/handler.rs | 10 ++++++-- server/src/domain/ldap/user.rs | 6 ++++- server/src/domain/model/groups.rs | 2 +- .../src/domain/model/jwt_refresh_storage.rs | 2 +- server/src/domain/model/jwt_storage.rs | 2 +- .../src/domain/model/password_reset_tokens.rs | 2 +- server/src/domain/model/users.rs | 2 +- .../src/domain/sql_group_backend_handler.rs | 2 +- server/src/domain/sql_migrations.rs | 4 +-- server/src/domain/sql_tables.rs | 4 +-- server/src/domain/sql_user_backend_handler.rs | 2 +- server/src/domain/types.rs | 23 ++++++++++------- server/src/infra/graphql/query.rs | 11 ++++---- server/src/infra/ldap_handler.rs | 25 +++++++++++-------- server/src/infra/sql_backend_handler.rs | 4 +-- 15 files changed, 60 insertions(+), 41 deletions(-) diff --git a/server/src/domain/handler.rs b/server/src/domain/handler.rs index ef43e93..a39c256 100644 --- a/server/src/domain/handler.rs +++ b/server/src/domain/handler.rs @@ -140,8 +140,14 @@ mod tests { fn test_uuid_time() { use chrono::prelude::*; let user_id = "bob"; - let date1 = Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11).unwrap(); - let date2 = Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 12).unwrap(); + let date1 = Utc + .with_ymd_and_hms(2014, 7, 8, 9, 10, 11) + .unwrap() + .naive_utc(); + let date2 = Utc + .with_ymd_and_hms(2014, 7, 8, 9, 10, 12) + .unwrap() + .naive_utc(); assert_ne!( Uuid::from_name_and_date(user_id, &date1), Uuid::from_name_and_date(user_id, &date2) diff --git a/server/src/domain/ldap/user.rs b/server/src/domain/ldap/user.rs index 08f9853..6903aa4 100644 --- a/server/src/domain/ldap/user.rs +++ b/server/src/domain/ldap/user.rs @@ -1,3 +1,4 @@ +use chrono::TimeZone; use ldap3_proto::{ proto::LdapOp, LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry, }; @@ -49,7 +50,10 @@ fn get_user_attribute( }) .collect(), "cn" | "displayname" => vec![user.display_name.clone()?.into_bytes()], - "createtimestamp" | "modifytimestamp" => vec![user.creation_date.to_rfc3339().into_bytes()], + "createtimestamp" | "modifytimestamp" => vec![chrono::Utc + .from_utc_datetime(&user.creation_date) + .to_rfc3339() + .into_bytes()], "1.1" => return None, // We ignore the operational attribute wildcard. "+" => return None, diff --git a/server/src/domain/model/groups.rs b/server/src/domain/model/groups.rs index 748a61e..d9a74c8 100644 --- a/server/src/domain/model/groups.rs +++ b/server/src/domain/model/groups.rs @@ -11,7 +11,7 @@ pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub group_id: GroupId, pub display_name: String, - pub creation_date: chrono::DateTime, + pub creation_date: chrono::NaiveDateTime, pub uuid: Uuid, } diff --git a/server/src/domain/model/jwt_refresh_storage.rs b/server/src/domain/model/jwt_refresh_storage.rs index d7753ff..ebca1b1 100644 --- a/server/src/domain/model/jwt_refresh_storage.rs +++ b/server/src/domain/model/jwt_refresh_storage.rs @@ -11,7 +11,7 @@ pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub refresh_token_hash: i64, pub user_id: UserId, - pub expiry_date: chrono::DateTime, + pub expiry_date: chrono::NaiveDateTime, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/server/src/domain/model/jwt_storage.rs b/server/src/domain/model/jwt_storage.rs index 6fc6a4e..6ca9208 100644 --- a/server/src/domain/model/jwt_storage.rs +++ b/server/src/domain/model/jwt_storage.rs @@ -11,7 +11,7 @@ pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub jwt_hash: i64, pub user_id: UserId, - pub expiry_date: chrono::DateTime, + pub expiry_date: chrono::NaiveDateTime, pub blacklisted: bool, } diff --git a/server/src/domain/model/password_reset_tokens.rs b/server/src/domain/model/password_reset_tokens.rs index 54b1bea..a252b36 100644 --- a/server/src/domain/model/password_reset_tokens.rs +++ b/server/src/domain/model/password_reset_tokens.rs @@ -11,7 +11,7 @@ pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub token: String, pub user_id: UserId, - pub expiry_date: chrono::DateTime, + pub expiry_date: chrono::NaiveDateTime, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/server/src/domain/model/users.rs b/server/src/domain/model/users.rs index a9f1b02..32f8d86 100644 --- a/server/src/domain/model/users.rs +++ b/server/src/domain/model/users.rs @@ -18,7 +18,7 @@ pub struct Model { pub first_name: Option, pub last_name: Option, pub avatar: Option, - pub creation_date: chrono::DateTime, + pub creation_date: chrono::NaiveDateTime, pub password_hash: Option>, pub totp_secret: Option, pub mfa_type: Option, diff --git a/server/src/domain/sql_group_backend_handler.rs b/server/src/domain/sql_group_backend_handler.rs index aaca7fe..5367090 100644 --- a/server/src/domain/sql_group_backend_handler.rs +++ b/server/src/domain/sql_group_backend_handler.rs @@ -116,7 +116,7 @@ impl GroupBackendHandler for SqlBackendHandler { #[instrument(skip_all, level = "debug", ret, err)] async fn create_group(&self, group_name: &str) -> Result { debug!(?group_name); - let now = chrono::Utc::now(); + let now = chrono::Utc::now().naive_utc(); let uuid = Uuid::from_name_and_date(group_name, &now); let new_group = model::groups::ActiveModel { display_name: ActiveValue::Set(group_name.to_owned()), diff --git a/server/src/domain/sql_migrations.rs b/server/src/domain/sql_migrations.rs index ff5bb21..7efb152 100644 --- a/server/src/domain/sql_migrations.rs +++ b/server/src/domain/sql_migrations.rs @@ -170,7 +170,7 @@ pub async fn upgrade_to_v1(pool: &DbConnection) -> std::result::Result<(), sea_o struct ShortGroupDetails { group_id: GroupId, display_name: String, - creation_date: chrono::DateTime, + creation_date: chrono::NaiveDateTime, } for result in ShortGroupDetails::find_by_statement( builder.build( @@ -220,7 +220,7 @@ pub async fn upgrade_to_v1(pool: &DbConnection) -> std::result::Result<(), sea_o #[derive(FromQueryResult)] struct ShortUserDetails { user_id: UserId, - creation_date: chrono::DateTime, + creation_date: chrono::NaiveDateTime, } for result in ShortUserDetails::find_by_statement( builder.build( diff --git a/server/src/domain/sql_tables.rs b/server/src/domain/sql_tables.rs index 1bc4f77..0f202b0 100644 --- a/server/src/domain/sql_tables.rs +++ b/server/src/domain/sql_tables.rs @@ -67,7 +67,7 @@ mod tests { #[derive(FromQueryResult, PartialEq, Eq, Debug)] struct ShortUserDetails { display_name: String, - creation_date: chrono::DateTime, + creation_date: chrono::NaiveDateTime, } let result = ShortUserDetails::find_by_statement(raw_statement( r#"SELECT display_name, creation_date FROM users WHERE user_id = "bôb""#, @@ -80,7 +80,7 @@ mod tests { result, ShortUserDetails { display_name: "Bob Bobbersön".to_owned(), - creation_date: Utc.timestamp_opt(0, 0).unwrap() + creation_date: Utc.timestamp_opt(0, 0).unwrap().naive_utc(), } ); } diff --git a/server/src/domain/sql_user_backend_handler.rs b/server/src/domain/sql_user_backend_handler.rs index dc7b99e..9220dff 100644 --- a/server/src/domain/sql_user_backend_handler.rs +++ b/server/src/domain/sql_user_backend_handler.rs @@ -158,7 +158,7 @@ impl UserBackendHandler for SqlBackendHandler { #[instrument(skip_all, level = "debug", err)] async fn create_user(&self, request: CreateUserRequest) -> Result<()> { debug!(user_id = ?request.user_id); - let now = chrono::Utc::now(); + let now = chrono::Utc::now().naive_utc(); let uuid = Uuid::from_name_and_date(request.user_id.as_str(), &now); let new_user = model::users::ActiveModel { user_id: Set(request.user_id), diff --git a/server/src/domain/types.rs b/server/src/domain/types.rs index 76673d8..494f8f9 100644 --- a/server/src/domain/types.rs +++ b/server/src/domain/types.rs @@ -1,3 +1,4 @@ +use chrono::{NaiveDateTime, TimeZone}; use sea_orm::{ entity::IntoActiveValue, sea_query::{value::ValueType, ArrayType, ColumnType, Nullable, ValueTypeErr}, @@ -7,18 +8,23 @@ use serde::{Deserialize, Serialize}; pub use super::model::{GroupColumn, UserColumn}; -pub type DateTime = chrono::DateTime; - #[derive(PartialEq, Hash, Eq, Clone, Debug, Default, Serialize, Deserialize)] #[serde(try_from = "&str")] pub struct Uuid(String); impl Uuid { - pub fn from_name_and_date(name: &str, creation_date: &DateTime) -> Self { + pub fn from_name_and_date(name: &str, creation_date: &NaiveDateTime) -> Self { Uuid( uuid::Uuid::new_v3( &uuid::Uuid::NAMESPACE_X500, - &[name.as_bytes(), creation_date.to_rfc3339().as_bytes()].concat(), + &[ + name.as_bytes(), + chrono::Utc + .from_utc_datetime(creation_date) + .to_rfc3339() + .as_bytes(), + ] + .concat(), ) .to_string(), ) @@ -308,15 +314,14 @@ pub struct User { pub first_name: Option, pub last_name: Option, pub avatar: Option, - pub creation_date: DateTime, + pub creation_date: NaiveDateTime, pub uuid: Uuid, } #[cfg(test)] impl Default for User { fn default() -> Self { - use chrono::TimeZone; - let epoch = chrono::Utc.timestamp_opt(0, 0).unwrap(); + let epoch = chrono::Utc.timestamp_opt(0, 0).unwrap().naive_utc(); User { user_id: UserId::default(), email: String::new(), @@ -373,7 +378,7 @@ impl TryFromU64 for GroupId { pub struct Group { pub id: GroupId, pub display_name: String, - pub creation_date: DateTime, + pub creation_date: NaiveDateTime, pub uuid: Uuid, pub users: Vec, } @@ -382,7 +387,7 @@ pub struct Group { pub struct GroupDetails { pub group_id: GroupId, pub display_name: String, - pub creation_date: DateTime, + pub creation_date: NaiveDateTime, pub uuid: Uuid, } diff --git a/server/src/infra/graphql/query.rs b/server/src/infra/graphql/query.rs index 03091f6..7c97050 100644 --- a/server/src/infra/graphql/query.rs +++ b/server/src/infra/graphql/query.rs @@ -3,6 +3,7 @@ use crate::domain::{ ldap::utils::map_user_field, types::{GroupDetails, GroupId, UserColumn, UserId}, }; +use chrono::TimeZone; use juniper::{graphql_object, FieldResult, GraphQLInputObject}; use serde::{Deserialize, Serialize}; use tracing::{debug, debug_span, Instrument}; @@ -230,7 +231,7 @@ impl User { } fn creation_date(&self) -> chrono::DateTime { - self.user.creation_date + chrono::Utc.from_utc_datetime(&self.user.creation_date) } fn uuid(&self) -> &str { @@ -275,7 +276,7 @@ impl From for User { pub struct Group { group_id: i32, display_name: String, - creation_date: chrono::DateTime, + creation_date: chrono::NaiveDateTime, uuid: String, members: Option>, _phantom: std::marker::PhantomData>, @@ -290,7 +291,7 @@ impl Group { self.display_name.clone() } fn creation_date(&self) -> chrono::DateTime { - self.creation_date + chrono::Utc.from_utc_datetime(&self.creation_date) } fn uuid(&self) -> String { self.uuid.clone() @@ -389,7 +390,7 @@ mod tests { Ok(DomainUser { user_id: UserId::new("bob"), email: "bob@bobbers.on".to_string(), - creation_date: chrono::Utc.timestamp_millis_opt(42).unwrap(), + creation_date: chrono::Utc.timestamp_millis_opt(42).unwrap().naive_utc(), uuid: crate::uuid!("b1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), ..Default::default() }) @@ -398,7 +399,7 @@ mod tests { groups.insert(GroupDetails { group_id: GroupId(3), display_name: "Bobbersons".to_string(), - creation_date: chrono::Utc.timestamp_nanos(42), + creation_date: chrono::Utc.timestamp_nanos(42).naive_utc(), uuid: crate::uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), }); mock.expect_get_user_groups() diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index 790d2e2..8ae3e1c 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -667,7 +667,7 @@ mod tests { set.insert(GroupDetails { group_id: GroupId(42), display_name: group, - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), }); Ok(set) @@ -754,7 +754,7 @@ mod tests { set.insert(GroupDetails { group_id: GroupId(42), display_name: "lldap_admin".to_string(), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), }); Ok(set) @@ -841,7 +841,7 @@ mod tests { groups: Some(vec![GroupDetails { group_id: GroupId(42), display_name: "rockstars".to_string(), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), }]), }]) @@ -1006,7 +1006,10 @@ mod tests { last_name: Some("Cricket".to_string()), avatar: Some(JpegPhoto::for_tests()), uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), - creation_date: Utc.with_ymd_and_hms(2014, 7, 8, 9, 10, 11).unwrap(), + creation_date: Utc + .with_ymd_and_hms(2014, 7, 8, 9, 10, 11) + .unwrap() + .naive_utc(), }, groups: None, }, @@ -1135,14 +1138,14 @@ mod tests { Group { id: GroupId(1), display_name: "group_1".to_string(), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), users: vec![UserId::new("bob"), UserId::new("john")], uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), }, Group { id: GroupId(3), display_name: "BestGroup".to_string(), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), users: vec![UserId::new("john")], uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), }, @@ -1228,7 +1231,7 @@ mod tests { Ok(vec![Group { display_name: "group_1".to_string(), id: GroupId(1), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), users: vec![], uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), }]) @@ -1279,7 +1282,7 @@ mod tests { Ok(vec![Group { display_name: "group_1".to_string(), id: GroupId(1), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), users: vec![], uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), }]) @@ -1555,7 +1558,7 @@ mod tests { Ok(vec![Group { id: GroupId(1), display_name: "group_1".to_string(), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), users: vec![UserId::new("bob"), UserId::new("john")], uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), }]) @@ -1629,7 +1632,7 @@ mod tests { Ok(vec![Group { id: GroupId(1), display_name: "group_1".to_string(), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), users: vec![UserId::new("bob"), UserId::new("john")], uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), }]) @@ -1962,7 +1965,7 @@ mod tests { groups.insert(GroupDetails { group_id: GroupId(0), display_name: "lldap_admin".to_string(), - creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"), }); mock.expect_get_user_groups() diff --git a/server/src/infra/sql_backend_handler.rs b/server/src/infra/sql_backend_handler.rs index 91741cc..253eca8 100644 --- a/server/src/infra/sql_backend_handler.rs +++ b/server/src/infra/sql_backend_handler.rs @@ -61,7 +61,7 @@ impl TcpBackendHandler for SqlBackendHandler { let new_token = model::jwt_refresh_storage::Model { refresh_token_hash: refresh_token_hash as i64, user_id: user.clone(), - expiry_date: chrono::Utc::now() + duration, + expiry_date: chrono::Utc::now().naive_utc() + duration, } .into_active_model(); new_token.insert(&self.sql_pool).await?; @@ -131,7 +131,7 @@ impl TcpBackendHandler for SqlBackendHandler { let new_token = model::password_reset_tokens::Model { token: token.clone(), user_id: user.clone(), - expiry_date: chrono::Utc::now() + duration, + expiry_date: chrono::Utc::now().naive_utc() + duration, } .into_active_model(); new_token.insert(&self.sql_pool).await?; From 955a559c21b8a0b15b382a9044984ac1a19daccf Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Fri, 13 Jan 2023 15:28:58 +0100 Subject: [PATCH 08/62] clippy: fix warning --- server/src/domain/sql_opaque_handler.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/domain/sql_opaque_handler.rs b/server/src/domain/sql_opaque_handler.rs index 1ba6e8d..b2ded01 100644 --- a/server/src/domain/sql_opaque_handler.rs +++ b/server/src/domain/sql_opaque_handler.rs @@ -133,7 +133,7 @@ impl OpaqueHandler for SqlOpaqueHandler { let encrypted_state = orion::aead::seal(&secret_key, &bincode::serialize(&server_data)?)?; Ok(login::ServerLoginStartResponse { - server_data: base64::encode(&encrypted_state), + server_data: base64::encode(encrypted_state), credential_response: start_response.message, }) } From f979e16b9543ee0312bb0c4ec57700be207e4001 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Mon, 16 Jan 2023 16:56:55 +0100 Subject: [PATCH 09/62] server: Fix healthcheck return code The healthcheck was not returning a non-zero code when failing, due to an extra layer of Results --- server/src/infra/healthcheck.rs | 1 + server/src/main.rs | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/server/src/infra/healthcheck.rs b/server/src/infra/healthcheck.rs index 0fdd997..40089a0 100644 --- a/server/src/infra/healthcheck.rs +++ b/server/src/infra/healthcheck.rs @@ -99,6 +99,7 @@ fn get_tls_connector() -> Result { #[instrument(skip_all, level = "info", err)] pub async fn check_ldaps(ldaps_options: &LdapsOptions) -> Result<()> { if !ldaps_options.enabled { + info!("LDAPS not enabled"); return Ok(()); }; let tls_connector = get_tls_connector()?; diff --git a/server/src/main.rs b/server/src/main.rs index 005e0ce..2c67bd9 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -158,6 +158,8 @@ fn run_healthcheck(opts: RunOpts) -> Result<()> { .enable_all() .build()?; + info!("Starting healthchecks"); + use tokio::time::timeout; let delay = Duration::from_millis(3000); let (ldap, ldaps, api) = runtime.block_on(async { @@ -168,14 +170,18 @@ fn run_healthcheck(opts: RunOpts) -> Result<()> { ) }); - let mut failure = false; - [ldap, ldaps, api] + let failure = [ldap, ldaps, api] .into_iter() - .filter_map(Result::err) - .for_each(|e| { - failure = true; - error!("{:#}", e) - }); + .flat_map(|res| { + if let Err(e) = &res { + error!("Error running the health check: {:#}", e); + } + res + }) + .any(|r| r.is_err()); + if failure { + error!("Healthcheck failed"); + } std::process::exit(i32::from(failure)) } From 807fd10d13a700c5d7507e2113748b95d6c7bf9e Mon Sep 17 00:00:00 2001 From: Luca Tagliavini Date: Tue, 17 Jan 2023 14:21:57 +0100 Subject: [PATCH 10/62] server: Add support for DN filters --- server/src/domain/ldap/group.rs | 16 +++++++++++++++- server/src/domain/ldap/user.rs | 18 +++++++++++++++++- server/src/infra/ldap_handler.rs | 10 ++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/server/src/domain/ldap/group.rs b/server/src/domain/ldap/group.rs index 2ab5b64..2a4e37f 100644 --- a/server/src/domain/ldap/group.rs +++ b/server/src/domain/ldap/group.rs @@ -12,7 +12,8 @@ use crate::domain::{ use super::{ error::LdapResult, utils::{ - expand_attribute_wildcards, get_user_id_from_distinguished_name, map_group_field, LdapInfo, + expand_attribute_wildcards, get_group_id_from_distinguished_name, + get_user_id_from_distinguished_name, map_group_field, LdapInfo, }, }; @@ -126,6 +127,19 @@ fn convert_group_filter( vec![], )))), }, + "dn" => Ok( + match get_group_id_from_distinguished_name( + value.to_ascii_lowercase().as_str(), + &ldap_info.base_dn, + &ldap_info.base_dn_str, + ) { + Ok(value) => GroupRequestFilter::DisplayName(value), + Err(_) => { + warn!("Invalid dn filter on group: {}", value); + GroupRequestFilter::Not(Box::new(GroupRequestFilter::And(vec![]))) + } + }, + ), _ => match map_group_field(field) { Some(GroupColumn::DisplayName) => { Ok(GroupRequestFilter::DisplayName(value.to_string())) diff --git a/server/src/domain/ldap/user.rs b/server/src/domain/ldap/user.rs index 6903aa4..20b06c9 100644 --- a/server/src/domain/ldap/user.rs +++ b/server/src/domain/ldap/user.rs @@ -6,7 +6,10 @@ use tracing::{debug, info, instrument, warn}; use crate::domain::{ handler::{BackendHandler, UserRequestFilter}, - ldap::{error::LdapError, utils::expand_attribute_wildcards}, + ldap::{ + error::LdapError, + utils::{expand_attribute_wildcards, get_user_id_from_distinguished_name}, + }, types::{GroupDetails, User, UserColumn, UserId}, }; @@ -147,6 +150,19 @@ fn convert_user_filter(ldap_info: &LdapInfo, filter: &LdapFilter) -> LdapResult< vec![], )))), }, + "dn" => Ok( + match get_user_id_from_distinguished_name( + value.to_ascii_lowercase().as_str(), + &ldap_info.base_dn, + &ldap_info.base_dn_str, + ) { + Ok(value) => UserRequestFilter::UserId(value), + Err(_) => { + warn!("Invalid dn filter on user: {}", value); + UserRequestFilter::Not(Box::new(UserRequestFilter::And(vec![]))) + } + }, + ), _ => match map_user_field(field) { Some(UserColumn::UserId) => Ok(UserRequestFilter::UserId(UserId::new(value))), Some(field) => Ok(UserRequestFilter::Equality(field, value.clone())), diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index 8ae3e1c..6287573 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -1217,6 +1217,7 @@ mod tests { .with(eq(Some(GroupRequestFilter::And(vec![ GroupRequestFilter::DisplayName("group_1".to_string()), GroupRequestFilter::Member(UserId::new("bob")), + GroupRequestFilter::DisplayName("rockstars".to_string()), GroupRequestFilter::And(vec![]), GroupRequestFilter::And(vec![]), GroupRequestFilter::And(vec![]), @@ -1245,6 +1246,10 @@ mod tests { "uniqueMember".to_string(), "uid=bob,ou=peopLe,Dc=eXample,dc=com".to_string(), ), + LdapFilter::Equality( + "dn".to_string(), + "uid=rockstars,ou=groups,dc=example,dc=com".to_string(), + ), LdapFilter::Equality("obJEctclass".to_string(), "groupofUniqueNames".to_string()), LdapFilter::Equality("objectclass".to_string(), "groupOfNames".to_string()), LdapFilter::Present("objectclass".to_string()), @@ -1403,6 +1408,7 @@ mod tests { UserRequestFilter::Not(Box::new(UserRequestFilter::UserId(UserId::new( "bob", )))), + UserRequestFilter::UserId("bob_1".to_string().into()), UserRequestFilter::And(vec![]), UserRequestFilter::Not(Box::new(UserRequestFilter::And(vec![]))), UserRequestFilter::And(vec![]), @@ -1422,6 +1428,10 @@ mod tests { "uid".to_string(), "bob".to_string(), ))), + LdapFilter::Equality( + "dn".to_string(), + "uid=bob_1,ou=people,dc=example,dc=com".to_string(), + ), LdapFilter::Equality("objectclass".to_string(), "persOn".to_string()), LdapFilter::Equality("objectclass".to_string(), "other".to_string()), LdapFilter::Present("objectClass".to_string()), From 9018e6fa348ff77a96378ec6394797a4e0cc3e1a Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Tue, 17 Jan 2023 14:43:37 +0100 Subject: [PATCH 11/62] server, refactor: Add a conversion from bool for the filters --- server/src/domain/handler.rs | 20 ++++++++++ server/src/domain/ldap/group.rs | 54 +++++++++++--------------- server/src/domain/ldap/user.rs | 65 ++++++++++++-------------------- server/src/infra/ldap_handler.rs | 44 +++++++++++---------- 4 files changed, 87 insertions(+), 96 deletions(-) diff --git a/server/src/domain/handler.rs b/server/src/domain/handler.rs index a39c256..d93657d 100644 --- a/server/src/domain/handler.rs +++ b/server/src/domain/handler.rs @@ -27,6 +27,16 @@ pub enum UserRequestFilter { MemberOfId(GroupId), } +impl From for UserRequestFilter { + fn from(val: bool) -> Self { + if val { + Self::And(vec![]) + } else { + Self::Not(Box::new(Self::And(vec![]))) + } + } +} + #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)] pub enum GroupRequestFilter { And(Vec), @@ -39,6 +49,16 @@ pub enum GroupRequestFilter { Member(UserId), } +impl From for GroupRequestFilter { + fn from(val: bool) -> Self { + if val { + Self::And(vec![]) + } else { + Self::Not(Box::new(Self::And(vec![]))) + } + } +} + #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)] pub struct CreateUserRequest { // Same fields as User, but no creation_date, and with password. diff --git a/server/src/domain/ldap/group.rs b/server/src/domain/ldap/group.rs index 2a4e37f..4555e67 100644 --- a/server/src/domain/ldap/group.rs +++ b/server/src/domain/ldap/group.rs @@ -121,25 +121,20 @@ fn convert_group_filter( )?; Ok(GroupRequestFilter::Member(user_name)) } - "objectclass" => match value.as_str() { - "groupofuniquenames" | "groupofnames" => Ok(GroupRequestFilter::And(vec![])), - _ => Ok(GroupRequestFilter::Not(Box::new(GroupRequestFilter::And( - vec![], - )))), - }, - "dn" => Ok( - match get_group_id_from_distinguished_name( - value.to_ascii_lowercase().as_str(), - &ldap_info.base_dn, - &ldap_info.base_dn_str, - ) { - Ok(value) => GroupRequestFilter::DisplayName(value), - Err(_) => { - warn!("Invalid dn filter on group: {}", value); - GroupRequestFilter::Not(Box::new(GroupRequestFilter::And(vec![]))) - } - }, - ), + "objectclass" => Ok(GroupRequestFilter::from(matches!( + value.as_str(), + "groupofuniquenames" | "groupofnames" + ))), + "dn" => Ok(get_group_id_from_distinguished_name( + value.to_ascii_lowercase().as_str(), + &ldap_info.base_dn, + &ldap_info.base_dn_str, + ) + .map(GroupRequestFilter::DisplayName) + .unwrap_or_else(|_| { + warn!("Invalid dn filter on group: {}", value); + GroupRequestFilter::from(false) + })), _ => match map_group_field(field) { Some(GroupColumn::DisplayName) => { Ok(GroupRequestFilter::DisplayName(value.to_string())) @@ -158,9 +153,7 @@ fn convert_group_filter( field ); } - Ok(GroupRequestFilter::Not(Box::new(GroupRequestFilter::And( - vec![], - )))) + Ok(GroupRequestFilter::from(false)) } }, } @@ -174,17 +167,12 @@ fn convert_group_filter( LdapFilter::Not(filter) => Ok(GroupRequestFilter::Not(Box::new(rec(filter)?))), LdapFilter::Present(field) => { let field = &field.to_ascii_lowercase(); - if field == "objectclass" - || field == "dn" - || field == "distinguishedname" - || map_group_field(field).is_some() - { - Ok(GroupRequestFilter::And(vec![])) - } else { - Ok(GroupRequestFilter::Not(Box::new(GroupRequestFilter::And( - vec![], - )))) - } + Ok(GroupRequestFilter::from( + field == "objectclass" + || field == "dn" + || field == "distinguishedname" + || map_group_field(field).is_some(), + )) } _ => Err(LdapError { code: LdapResultCode::UnwillingToPerform, diff --git a/server/src/domain/ldap/user.rs b/server/src/domain/ldap/user.rs index 20b06c9..caddb6d 100644 --- a/server/src/domain/ldap/user.rs +++ b/server/src/domain/ldap/user.rs @@ -134,35 +134,27 @@ fn convert_user_filter(ldap_info: &LdapInfo, filter: &LdapFilter) -> LdapResult< LdapFilter::Equality(field, value) => { let field = &field.to_ascii_lowercase(); match field.as_str() { - "memberof" => { - let group_name = get_group_id_from_distinguished_name( + "memberof" => Ok(UserRequestFilter::MemberOf( + get_group_id_from_distinguished_name( &value.to_ascii_lowercase(), &ldap_info.base_dn, &ldap_info.base_dn_str, - )?; - Ok(UserRequestFilter::MemberOf(group_name)) - } - "objectclass" => match value.to_ascii_lowercase().as_str() { - "person" | "inetorgperson" | "posixaccount" | "mailaccount" => { - Ok(UserRequestFilter::And(vec![])) - } - _ => Ok(UserRequestFilter::Not(Box::new(UserRequestFilter::And( - vec![], - )))), - }, - "dn" => Ok( - match get_user_id_from_distinguished_name( - value.to_ascii_lowercase().as_str(), - &ldap_info.base_dn, - &ldap_info.base_dn_str, - ) { - Ok(value) => UserRequestFilter::UserId(value), - Err(_) => { - warn!("Invalid dn filter on user: {}", value); - UserRequestFilter::Not(Box::new(UserRequestFilter::And(vec![]))) - } - }, - ), + )?, + )), + "objectclass" => Ok(UserRequestFilter::from(matches!( + value.to_ascii_lowercase().as_str(), + "person" | "inetorgperson" | "posixaccount" | "mailaccount" + ))), + "dn" => Ok(get_user_id_from_distinguished_name( + value.to_ascii_lowercase().as_str(), + &ldap_info.base_dn, + &ldap_info.base_dn_str, + ) + .map(UserRequestFilter::UserId) + .unwrap_or_else(|_| { + warn!("Invalid dn filter on user: {}", value); + UserRequestFilter::from(false) + })), _ => match map_user_field(field) { Some(UserColumn::UserId) => Ok(UserRequestFilter::UserId(UserId::new(value))), Some(field) => Ok(UserRequestFilter::Equality(field, value.clone())), @@ -174,9 +166,7 @@ fn convert_user_filter(ldap_info: &LdapInfo, filter: &LdapFilter) -> LdapResult< field ); } - Ok(UserRequestFilter::Not(Box::new(UserRequestFilter::And( - vec![], - )))) + Ok(UserRequestFilter::from(false)) } }, } @@ -184,17 +174,12 @@ fn convert_user_filter(ldap_info: &LdapInfo, filter: &LdapFilter) -> LdapResult< LdapFilter::Present(field) => { let field = &field.to_ascii_lowercase(); // Check that it's a field we support. - if field == "objectclass" - || field == "dn" - || field == "distinguishedname" - || map_user_field(field).is_some() - { - Ok(UserRequestFilter::And(vec![])) - } else { - Ok(UserRequestFilter::Not(Box::new(UserRequestFilter::And( - vec![], - )))) - } + Ok(UserRequestFilter::from( + field == "objectclass" + || field == "dn" + || field == "distinguishedname" + || map_user_field(field).is_some(), + )) } _ => Err(LdapError { code: LdapResultCode::UnwillingToPerform, diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index 6287573..2c5e6ce 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -778,7 +778,7 @@ mod tests { mock.expect_list_users() .with( eq(Some(UserRequestFilter::And(vec![ - UserRequestFilter::And(vec![]), + UserRequestFilter::from(true), UserRequestFilter::UserId(UserId::new("test")), ]))), eq(false), @@ -813,7 +813,7 @@ mod tests { async fn test_search_readonly_user() { let mut mock = MockTestBackendHandler::new(); mock.expect_list_users() - .with(eq(Some(UserRequestFilter::And(vec![]))), eq(false)) + .with(eq(Some(UserRequestFilter::from(true))), eq(false)) .times(1) .return_once(|_, _| Ok(vec![])); let mut ldap_handler = setup_bound_readonly_handler(mock).await; @@ -830,7 +830,7 @@ mod tests { async fn test_search_member_of() { let mut mock = MockTestBackendHandler::new(); mock.expect_list_users() - .with(eq(Some(UserRequestFilter::And(vec![]))), eq(true)) + .with(eq(Some(UserRequestFilter::from(true))), eq(true)) .times(1) .return_once(|_, _| { Ok(vec![UserAndGroups { @@ -873,7 +873,7 @@ mod tests { mock.expect_list_users() .with( eq(Some(UserRequestFilter::And(vec![ - UserRequestFilter::And(vec![]), + UserRequestFilter::from(true), UserRequestFilter::UserId(UserId::new("bob")), ]))), eq(false), @@ -1131,7 +1131,7 @@ mod tests { async fn test_search_groups() { let mut mock = MockTestBackendHandler::new(); mock.expect_list_groups() - .with(eq(Some(GroupRequestFilter::And(vec![])))) + .with(eq(Some(GroupRequestFilter::from(true)))) .times(1) .return_once(|_| { Ok(vec![ @@ -1218,14 +1218,12 @@ mod tests { GroupRequestFilter::DisplayName("group_1".to_string()), GroupRequestFilter::Member(UserId::new("bob")), GroupRequestFilter::DisplayName("rockstars".to_string()), - GroupRequestFilter::And(vec![]), - GroupRequestFilter::And(vec![]), - GroupRequestFilter::And(vec![]), - GroupRequestFilter::And(vec![]), - GroupRequestFilter::Not(Box::new(GroupRequestFilter::Not(Box::new( - GroupRequestFilter::And(vec![]), - )))), - GroupRequestFilter::Not(Box::new(GroupRequestFilter::And(vec![]))), + GroupRequestFilter::from(true), + GroupRequestFilter::from(true), + GroupRequestFilter::from(true), + GroupRequestFilter::from(true), + GroupRequestFilter::Not(Box::new(GroupRequestFilter::from(false))), + GroupRequestFilter::from(false), ])))) .times(1) .return_once(|_| { @@ -1321,7 +1319,7 @@ mod tests { let mut mock = MockTestBackendHandler::new(); mock.expect_list_groups() .with(eq(Some(GroupRequestFilter::And(vec![ - GroupRequestFilter::And(vec![]), + GroupRequestFilter::from(true), GroupRequestFilter::DisplayName("rockstars".to_string()), ])))) .times(1) @@ -1409,12 +1407,12 @@ mod tests { "bob", )))), UserRequestFilter::UserId("bob_1".to_string().into()), - UserRequestFilter::And(vec![]), - UserRequestFilter::Not(Box::new(UserRequestFilter::And(vec![]))), - UserRequestFilter::And(vec![]), - UserRequestFilter::And(vec![]), - UserRequestFilter::Not(Box::new(UserRequestFilter::And(vec![]))), - UserRequestFilter::Not(Box::new(UserRequestFilter::And(vec![]))), + UserRequestFilter::from(true), + UserRequestFilter::from(false), + UserRequestFilter::from(true), + UserRequestFilter::from(true), + UserRequestFilter::from(false), + UserRequestFilter::from(false), ], )]))), eq(false), @@ -1562,7 +1560,7 @@ mod tests { }]) }); mock.expect_list_groups() - .with(eq(Some(GroupRequestFilter::And(vec![])))) + .with(eq(Some(GroupRequestFilter::from(true)))) .times(1) .return_once(|_| { Ok(vec![Group { @@ -1637,7 +1635,7 @@ mod tests { }]) }); mock.expect_list_groups() - .with(eq(Some(GroupRequestFilter::And(vec![])))) + .with(eq(Some(GroupRequestFilter::from(true)))) .returning(|_| { Ok(vec![Group { id: GroupId(1), @@ -2093,7 +2091,7 @@ mod tests { async fn test_search_filter_non_attribute() { let mut mock = MockTestBackendHandler::new(); mock.expect_list_users() - .with(eq(Some(UserRequestFilter::And(vec![]))), eq(false)) + .with(eq(Some(UserRequestFilter::from(true))), eq(false)) .times(1) .return_once(|_, _| Ok(vec![])); let mut ldap_handler = setup_bound_admin_handler(mock).await; From d722be889689631320191c0e14506fcb72788a3d Mon Sep 17 00:00:00 2001 From: Igor Rzegocki Date: Thu, 19 Jan 2023 11:30:25 +0100 Subject: [PATCH 12/62] server: add option to use insecure SMTP connection --- lldap_config.docker_template.toml | 2 +- server/src/infra/cli.rs | 1 + server/src/infra/configuration.rs | 3 +++ server/src/infra/mail.rs | 19 ++++++++++++++----- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/lldap_config.docker_template.toml b/lldap_config.docker_template.toml index c16fd3f..02e2aac 100644 --- a/lldap_config.docker_template.toml +++ b/lldap_config.docker_template.toml @@ -113,7 +113,7 @@ key_file = "/data/private_key" #server="smtp.gmail.com" ## The SMTP port. #port=587 -## How the connection is encrypted, either "TLS" or "STARTTLS". +## How the connection is encrypted, either "NONE" (no encryption), "TLS" or "STARTTLS". #smtp_encryption = "TLS" ## The SMTP user, usually your email address. #user="sender@gmail.com" diff --git a/server/src/infra/cli.rs b/server/src/infra/cli.rs index a31a968..ab1ba42 100644 --- a/server/src/infra/cli.rs +++ b/server/src/infra/cli.rs @@ -117,6 +117,7 @@ pub struct LdapsOpts { clap::arg_enum! { #[derive(Clone, Debug, Deserialize, Serialize)] pub enum SmtpEncryption { + NONE, TLS, STARTTLS, } diff --git a/server/src/infra/configuration.rs b/server/src/infra/configuration.rs index 209adbe..11e517c 100644 --- a/server/src/infra/configuration.rs +++ b/server/src/infra/configuration.rs @@ -266,6 +266,9 @@ impl ConfigOverrider for SmtpOpts { if let Some(password) = &self.smtp_password { config.smtp_options.password = SecUtf8::from(password.clone()); } + if let Some(smtp_encryption) = &self.smtp_encryption { + config.smtp_options.smtp_encryption = smtp_encryption.clone(); + } if let Some(tls_required) = self.smtp_tls_required { config.smtp_options.tls_required = Some(tls_required); } diff --git a/server/src/infra/mail.rs b/server/src/infra/mail.rs index bbac6c0..c67614b 100644 --- a/server/src/infra/mail.rs +++ b/server/src/infra/mail.rs @@ -26,12 +26,21 @@ async fn send_email(to: Mailbox, subject: &str, body: String, options: &MailOpti options.user.clone(), options.password.unsecure().to_string(), ); - let relay_factory = match options.smtp_encryption { - SmtpEncryption::TLS => AsyncSmtpTransport::::relay, - SmtpEncryption::STARTTLS => AsyncSmtpTransport::::starttls_relay, + let mailer = match options.smtp_encryption { + SmtpEncryption::NONE => { + AsyncSmtpTransport::::builder_dangerous(&options.server) + } + SmtpEncryption::TLS => AsyncSmtpTransport::::relay(&options.server)?, + SmtpEncryption::STARTTLS => { + AsyncSmtpTransport::::starttls_relay(&options.server)? + } }; - let mailer = relay_factory(&options.server)?.credentials(creds).build(); - mailer.send(email).await?; + mailer + .credentials(creds) + .port(options.port) + .build() + .send(email) + .await?; Ok(()) } From 0ae1597ecda9557f471c1dcf6c263fc5b7dea042 Mon Sep 17 00:00:00 2001 From: arcoast <81871508+arcoast@users.noreply.github.com> Date: Sun, 22 Jan 2023 08:49:00 +0000 Subject: [PATCH 13/62] example_configs: Add Wikijs example In response to https://github.com/nitnelave/lldap/pull/424#discussion_r1083280235 --- README.md | 1 + example_configs/wikijs.md | 64 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 example_configs/wikijs.md diff --git a/README.md b/README.md index 9173f52..89c1279 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,7 @@ folder for help with: - [Vaultwarden](example_configs/vaultwarden.md) - [WeKan](example_configs/wekan.md) - [WG Portal](example_configs/wg_portal.env.example) + - [WikiJS](example_configs/wikijs.md) - [XBackBone](example_configs/xbackbone_config.php) - [Zendto](example_configs/zendto.md) diff --git a/example_configs/wikijs.md b/example_configs/wikijs.md new file mode 100644 index 0000000..07827e5 --- /dev/null +++ b/example_configs/wikijs.md @@ -0,0 +1,64 @@ +# Configuration for WikiJS +Replace `dc=example,dc=com` with your LLDAP configured domain. +### LDAP URL +``` +ldap://lldap:3890 +``` +### Admin Bind DN +``` +uid=admin,ou=people,dc=example,dc=com +``` +or +``` +uid=readonlyuser,ou=people,dc=example,dc=com +``` +### Admin Bind Credentials +``` +ADMINPASSWORD +``` +or +``` +READONLYUSERPASSWORD +``` +### Search Base +``` +ou=people,dc=example,dc=com +``` +### Search Filter +If you wish the permitted users to be restricted to just the `wiki` group: +``` +(&(memberof=cn=wiki,ou=groups,dc=example,dc=com)(|(uid={{username}})(mail={{username}))(objectClass=person)) +``` +If you wish any of the registered LLDAP users to be permitted to use WikiJS: +``` +(&(|(uid={{username}})(mail={{username}))(objectClass=person)) +``` +### Use TLS +Left toggled off +### Verify TLS Certificate +Left toggled off +### TLS Certificate Path +Left blank +### Unique ID Field Mapping +``` +uid +``` +### Email Field Mapping +``` +mail +``` +### Display Name Field Mapping +``` +givenname +``` +### Avatar Picture Field Mapping +``` +jpegPhoto +``` +### Allow self-registration +Toggled on +### Limit to specific email domains +Left blank +### Assign to group +I created a group called `users` and assign my LDAP users to that by default. +You can use the local admin account to login and promote an LDAP user to `admin` group if you wish and then deactivate the local login option From df1169e06d61eea555052732ccb24679f45643a5 Mon Sep 17 00:00:00 2001 From: Dedy Martadinata S Date: Sun, 22 Jan 2023 17:10:26 +0700 Subject: [PATCH 14/62] docker: simplify binary build, add db integration test --- .github/workflows/Dockerfile.ci.alpine | 12 +- .github/workflows/Dockerfile.ci.debian | 12 +- .github/workflows/Dockerfile.dev | 16 +- .github/workflows/docker-build-static.yml | 315 ++++++++++------------ 4 files changed, 170 insertions(+), 185 deletions(-) diff --git a/.github/workflows/Dockerfile.ci.alpine b/.github/workflows/Dockerfile.ci.alpine index 47f778f..5318fd8 100644 --- a/.github/workflows/Dockerfile.ci.alpine +++ b/.github/workflows/Dockerfile.ci.alpine @@ -10,8 +10,8 @@ RUN mkdir -p target/ RUN mkdir -p /lldap/app RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \ - mv bin/amd64-lldap-bin/lldap target/lldap && \ - mv bin/amd64-migration-tool-bin/migration-tool target/migration-tool && \ + mv bin/x86_64-unknown-linux-musl-lldap-bin/lldap target/lldap && \ + mv bin/x86_64-unknown-linux-musl-migration-tool-bin/migration-tool target/migration-tool && \ chmod +x target/lldap && \ chmod +x target/migration-tool && \ ls -la target/ . && \ @@ -19,8 +19,8 @@ RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \ ; fi RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \ - mv bin/aarch64-lldap-bin/lldap target/lldap && \ - mv bin/aarch64-migration-tool-bin/migration-tool target/migration-tool && \ + mv bin/aarch64-unknown-linux-musl-lldap-bin/lldap target/lldap && \ + mv bin/aarch64-unknown-linux-musl-migration-tool-bin/migration-tool target/migration-tool && \ chmod +x target/lldap && \ chmod +x target/migration-tool && \ ls -la target/ . && \ @@ -28,8 +28,8 @@ RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \ ; fi RUN if [ "${TARGETPLATFORM}" = "linux/arm/v7" ]; then \ - mv bin/armhf-lldap-bin/lldap target/lldap && \ - mv bin/armhf-migration-tool-bin/migration-tool target/migration-tool && \ + mv bin/armv7-unknown-linux-gnueabihf-lldap-bin/lldap target/lldap && \ + mv bin/armv7-unknown-linux-gnueabihf-migration-tool-bin/migration-tool target/migration-tool && \ chmod +x target/lldap && \ chmod +x target/migration-tool && \ ls -la target/ . && \ diff --git a/.github/workflows/Dockerfile.ci.debian b/.github/workflows/Dockerfile.ci.debian index 3f7c45b..b27b4e2 100644 --- a/.github/workflows/Dockerfile.ci.debian +++ b/.github/workflows/Dockerfile.ci.debian @@ -10,8 +10,8 @@ RUN mkdir -p target/ RUN mkdir -p /lldap/app RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \ - mv bin/amd64-lldap-bin/lldap target/lldap && \ - mv bin/amd64-migration-tool-bin/migration-tool target/migration-tool && \ + mv bin/x86_64-unknown-linux-musl-lldap-bin/lldap target/lldap && \ + mv bin/x86_64-unknown-linux-musl-migration-tool-bin/migration-tool target/migration-tool && \ chmod +x target/lldap && \ chmod +x target/migration-tool && \ ls -la target/ . && \ @@ -19,8 +19,8 @@ RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \ ; fi RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \ - mv bin/aarch64-lldap-bin/lldap target/lldap && \ - mv bin/aarch64-migration-tool-bin/migration-tool target/migration-tool && \ + mv bin/aarch64-unknown-linux-musl-lldap-bin/lldap target/lldap && \ + mv bin/aarch64-unknown-linux-musl-migration-tool-bin/migration-tool target/migration-tool && \ chmod +x target/lldap && \ chmod +x target/migration-tool && \ ls -la target/ . && \ @@ -28,8 +28,8 @@ RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \ ; fi RUN if [ "${TARGETPLATFORM}" = "linux/arm/v7" ]; then \ - mv bin/armhf-lldap-bin/lldap target/lldap && \ - mv bin/armhf-migration-tool-bin/migration-tool target/migration-tool && \ + mv bin/armv7-unknown-linux-gnueabihf-lldap-bin/lldap target/lldap && \ + mv bin/armv7-unknown-linux-gnueabihf-migration-tool-bin/migration-tool target/migration-tool && \ chmod +x target/lldap && \ chmod +x target/migration-tool && \ ls -la target/ . && \ diff --git a/.github/workflows/Dockerfile.dev b/.github/workflows/Dockerfile.dev index bb88144..7bca4e4 100644 --- a/.github/workflows/Dockerfile.dev +++ b/.github/workflows/Dockerfile.dev @@ -1,4 +1,5 @@ -FROM rust:1.65-slim-bullseye +# Keep tracking base image +FROM rust:1.66-slim-bullseye # Set needed env path ENV PATH="/opt/aarch64-linux-musl-cross/:/opt/aarch64-linux-musl-cross/bin/:/opt/x86_64-linux-musl-cross/:/opt/x86_64-linux-musl-cross/bin/:$PATH" @@ -23,6 +24,14 @@ RUN dpkg --add-architecture arm64 && \ rm -rf /var/lib/apt/lists/* && \ rustup target add aarch64-unknown-linux-gnu +### armhf deps +RUN dpkg --add-architecture armhf && \ + apt update && \ + apt install -y gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf libc6-armhf-cross libc6-dev-armhf-cross && \ + apt clean && \ + rm -rf /var/lib/apt/lists/* && \ + rustup target add armv7-unknown-linux-gnueabihf + ### Add musl-gcc aarch64 and x86_64 RUN wget -c https://musl.cc/x86_64-linux-musl-cross.tgz && \ tar zxf ./x86_64-linux-musl-cross.tgz -C /opt && \ @@ -31,4 +40,9 @@ RUN wget -c https://musl.cc/x86_64-linux-musl-cross.tgz && \ rm ./x86_64-linux-musl-cross.tgz && \ rm ./aarch64-linux-musl-cross.tgz +### Add musl target +RUN rustup target add x86_64-unknown-linux-musl && \ + rustup target add aarch64-unknown-linux-musl + + CMD ["bash"] diff --git a/.github/workflows/docker-build-static.yml b/.github/workflows/docker-build-static.yml index cc8146c..620e8c5 100644 --- a/.github/workflows/docker-build-static.yml +++ b/.github/workflows/docker-build-static.yml @@ -19,14 +19,8 @@ on: env: CARGO_TERM_COLOR: always -# In total 5 jobs, all the jobs are containerized -# --- -####################################################################################### -# GitHub actions randomly timeout when downloading musl-gcc # -# Using lldap dev image based on https://hub.docker.com/_/rust and musl-gcc bundled # -# Look into .github/workflows/Dockerfile.dev for development image details # -####################################################################################### +### CI Docs # build-ui , create/compile the web ### install wasm @@ -34,20 +28,36 @@ env: ### run app/build.sh ### upload artifacts -# builds-armhf, build-aarch64, build-amd64 create binary for respective arch -### Add non-native architecture dpkg --add-architecture XXX -### Install dev tool gcc g++, etc. per respective arch +# build-bin +## build-armhf, build-aarch64, build-amd64 , create binary for respective arch +####################################################################################### +# GitHub actions randomly timeout when downloading musl-gcc, using custom dev image # +# Look into .github/workflows/Dockerfile.dev for development image details # +# Using lldap dev image based on https://hub.docker.com/_/rust and musl-gcc bundled # +####################################################################################### ### Cargo build -### Upload artifacts - -## the CARGO_ env -#CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER: arm-linux-gnueabihf-gcc -# This will determine which architecture lib will be used. +### aarch64 and amd64 is musl based +### armv7 is glibc based, musl had issue with time_t when cross compile https://github.com/rust-lang/libc/issues/1848 # build-ui,builds-armhf, build-aarch64, build-amd64 will upload artifacts will be used next job -# build-docker-image job will fetch artifacts and run Dockerfile.ci then push the image. -# cache based on Cargo.lock +# lldap-test +### will run lldap with postgres, mariadb and sqlite backend, do selfcheck command. + +# Build docker image +### Triplet docker image arch with debian base +### amd64 & aarch64 with alpine base +# build-docker-image job will fetch artifacts and run Dockerfile.ci then push the image. +### Look into .github/workflows/Dockerfile.ci.debian or .github/workflowds/Dockerfile.ci.alpine + +# create release artifacts +### Fetch artifacts +### Clean up web artifact +### Setup folder structure +### Compress +### Upload + +# cache based on Cargo.lock per cargo target jobs: build-ui: @@ -68,124 +78,39 @@ jobs: key: lldap-ui-${{ hashFiles('**/Cargo.lock') }} restore-keys: | lldap-ui- - - name: install rollup nodejs + - name: Install rollup (nodejs) run: npm install -g rollup - - name: add wasm target + - name: Add wasm target (rust) run: rustup target add wasm32-unknown-unknown - - name: install wasm-pack with cargo + - name: Install wasm-pack with cargo run: cargo install wasm-pack || true env: RUSTFLAGS: "" - - name: build frontend + - name: Build frontend run: ./app/build.sh - - name: check path + - name: Check build path run: ls -al app/ - - name: upload ui artifacts + - name: Upload ui artifacts uses: actions/upload-artifact@v3 with: name: ui path: app/ - build-armhf: + + build-bin: runs-on: ubuntu-latest + strategy: + matrix: + target: [armv7-unknown-linux-gnueabihf, aarch64-unknown-linux-musl, x86_64-unknown-linux-musl] container: image: nitnelave/rust-dev:latest env: CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER: arm-linux-gnueabihf-gcc - CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER: arm-linux-gnueabihf-ld - CARGO_TERM_COLOR: always - RUSTFLAGS: -Ctarget-feature=-crt-static - CARGO_HOME: ${GITHUB_WORKSPACE}/.cargo - steps: - - name: add armhf architecture - run: dpkg --add-architecture armhf - - name: install runtime - run: apt update && apt install -y gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf libc6-armhf-cross libc6-dev-armhf-cross tar ca-certificates - - name: add armhf target - run: rustup target add armv7-unknown-linux-gnueabihf - - name: Checkout repository - uses: actions/checkout@v3.3.0 - - uses: actions/cache@v3 - with: - path: | - .cargo/bin - .cargo/registry/index - .cargo/registry/cache - .cargo/git/db - target - key: lldap-bin-armhf-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - lldap-bin-armhf- - - name: compile armhf - run: cargo build --target=armv7-unknown-linux-gnueabihf --release -p lldap -p migration-tool - - name: check path - run: ls -al target/release - - name: upload armhf lldap artifacts - uses: actions/upload-artifact@v3 - with: - name: armhf-lldap-bin - path: target/armv7-unknown-linux-gnueabihf/release/lldap - - name: upload armhfmigration-tool artifacts - uses: actions/upload-artifact@v3 - with: - name: armhf-migration-tool-bin - path: target/armv7-unknown-linux-gnueabihf/release/migration-tool - - - build-aarch64: - runs-on: ubuntu-latest - container: - image: nitnelave/rust-dev:latest - env: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: aarch64-linux-musl-gcc - CARGO_TERM_COLOR: always - RUSTFLAGS: -Ctarget-feature=+crt-static - CARGO_HOME: ${GITHUB_WORKSPACE}/.cargo - steps: - - name: Checkout repository - uses: actions/checkout@v3.3.0 - - uses: actions/cache@v3 - with: - path: | - .cargo/bin - .cargo/registry/index - .cargo/registry/cache - .cargo/git/db - target - key: lldap-bin-aarch64-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - lldap-bin-aarch64- -# - name: fetch musl-gcc -# run: | -# wget -c https://musl.cc/aarch64-linux-musl-cross.tgz -# tar zxf ./x86_64-linux-musl-cross.tgz -C /opt -# echo "/opt/aarch64-linux-musl-cross:/opt/aarch64-linux-musl-cross/bin" >> $GITHUB_PATH - - name: add musl aarch64 target - run: rustup target add aarch64-unknown-linux-musl - - name: build lldap aarch4 - run: cargo build --target=aarch64-unknown-linux-musl --release -p lldap -p migration-tool - - name: check path - run: ls -al target/aarch64-unknown-linux-musl/release/ - - name: upload aarch64 lldap artifacts - uses: actions/upload-artifact@v3 - with: - name: aarch64-lldap-bin - path: target/aarch64-unknown-linux-musl/release/lldap - - name: upload aarch64 migration-tool artifacts - uses: actions/upload-artifact@v3 - with: - name: aarch64-migration-tool-bin - path: target/aarch64-unknown-linux-musl/release/migration-tool - - build-amd64: - runs-on: ubuntu-latest - container: - image: nitnelave/rust-dev:latest - env: - CARGO_TERM_COLOR: always - RUSTFLAGS: -Ctarget-feature=+crt-static - CARGO_HOME: ${GITHUB_WORKSPACE}/.cargo CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER: x86_64-linux-musl-gcc + CARGO_TERM_COLOR: always + RUSTFLAGS: -Ctarget-feature=+crt-static + CARGO_HOME: ${GITHUB_WORKSPACE}/.cargo steps: - name: Checkout repository uses: actions/checkout@v3.3.0 @@ -197,47 +122,103 @@ jobs: .cargo/registry/cache .cargo/git/db target - key: lldap-bin-amd64-${{ hashFiles('**/Cargo.lock') }} + key: lldap-bin-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }} restore-keys: | - lldap-bin-amd64- - - name: install musl - run: apt update && apt install -y musl-tools tar wget -# - name: fetch musl-gcc -# run: | -# wget -c https://musl.cc/x86_64-linux-musl-cross.tgz -# tar zxf ./x86_64-linux-musl-cross.tgz -C /opt -# echo "/opt/x86_64-linux-musl-cross:/opt/x86_64-linux-musl-cross/bin" >> $GITHUB_PATH - - name: add x86_64 target - run: rustup target add x86_64-unknown-linux-musl - - name: build x86_64 lldap - run: cargo build --target=x86_64-unknown-linux-musl --release -p lldap -p migration-tool - - name: check path - run: ls -al target/x86_64-unknown-linux-musl/release/ - - name: upload amd64 lldap artifacts + lldap-bin-${{ matrix.target }}- + - name: Compile ${{ matrix.target }} lldap and migration tool + run: cargo build --target=${{ matrix.target }} --release -p lldap -p migration-tool + - name: Check path + run: ls -al target/release + - name: Upload ${{ matrix.target}} lldap artifacts uses: actions/upload-artifact@v3 with: - name: amd64-lldap-bin - path: target/x86_64-unknown-linux-musl/release/lldap - - name: upload amd64 migration-tool artifacts + name: ${{ matrix.target}}-lldap-bin + path: target/${{ matrix.target }}/release/lldap + - name: Upload ${{ matrix.target }} migration tool artifacts uses: actions/upload-artifact@v3 with: - name: amd64-migration-tool-bin - path: target/x86_64-unknown-linux-musl/release/migration-tool + name: ${{ matrix.target }}-migration-tool-bin + path: target/${{ matrix.target }}/release/migration-tool + + lldap-database-integration-test: + needs: [build-ui,build-bin] + name: LLDAP test + runs-on: ubuntu-latest + services: + mariadb: + image: mariadb:latest + ports: + - 3306:3306 + env: + MYSQL_USER: lldapuser + MYSQL_PASSWORD: lldappass + MYSQL_DATABASE: lldap + MYSQL_ROOT_PASSWORD: rootpass + + postgresql: + image: postgres:latest + ports: + - 5432:5432 + env: + POSTGRES_USER: lldapuser + POSTGRES_PASSWORD: lldappass + POSTGRES_DB: lldap + + steps: + - name: Download artifacts + uses: actions/download-artifact@v3 + with: + name: x86_64-unknown-linux-musl-lldap-bin + path: bin/ + - name: Where is the bin? + run: ls -alR bin + - name: Set executables to LLDAP + run: chmod +x bin/lldap + + - name: Run lldap with postgres DB and healthcheck + run: | + bin/lldap run & + sleep 10s + bin/lldap healthcheck + env: + LLDAP_database_url: postgres://lldapuser:lldappass@localhost/lldap + LLDAP_ldap_port: 3890 + LLDAP_http_port: 17170 + + + - name: Run lldap with mariadb DB (MySQL Compatible) and healthcheck + run: | + bin/lldap run & + sleep 10s + bin/lldap healthcheck + env: + LLDAP_database_url: mysql://lldapuser:lldappass@localhost/lldap + LLDAP_ldap_port: 3891 + LLDAP_http_port: 17171 + + + - name: Run lldap with sqlite DB and healthcheck + run: | + bin/lldap run & + sleep 10s + bin/lldap healthcheck + env: + LLDAP_database_url: sqlite://users.db?mode=rwc + LLDAP_ldap_port: 3892 + LLDAP_http_port: 17172 build-docker-image: - needs: [build-ui,build-armhf,build-aarch64,build-amd64] + needs: [build-ui, build-bin] name: Build Docker image runs-on: ubuntu-latest permissions: contents: read packages: write steps: - - name: install rsync - run: sudo apt update && sudo apt install -y rsync - - name: fetch repo + - name: Checkout repository uses: actions/checkout@v3.3.0 - - name: Download All Artifacts + - name: Download all artifacts uses: actions/download-artifact@v3 with: path: bin @@ -248,7 +229,7 @@ jobs: name: ui path: web - - name: setup qemu + - name: Setup QEMU uses: docker/setup-qemu-action@v2 - uses: docker/setup-buildx-action@v2 @@ -267,13 +248,6 @@ jobs: type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} type=sha - - name: Cache Docker layers - uses: actions/cache@v3 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx- - name: parse tag uses: gacts/github-slug@v1 @@ -298,8 +272,8 @@ jobs: platforms: linux/amd64,linux/arm64 file: ./.github/workflows/Dockerfile.ci.alpine tags: nitnelave/lldap:latest, nitnelave/lldap:latest-alpine - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache-new + cache-from: type=gha,mode=max + cache-to: type=gha,mode=max - name: Build and push latest debian if: github.event_name != 'release' @@ -310,8 +284,8 @@ jobs: platforms: linux/amd64,linux/arm64,linux/arm/v7 file: ./.github/workflows/Dockerfile.ci.debian tags: nitnelave/lldap:latest-debian - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache-new + cache-from: type=gha,mode=max + cache-to: type=gha,mode=max ######################################## #### docker image :semver tag build #### @@ -326,8 +300,8 @@ jobs: # 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=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache-new + cache-from: type=gha,mode=max + cache-to: type=gha,mode=max - name: Build and push release debian if: github.event_name == 'release' @@ -339,11 +313,8 @@ jobs: # 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=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache-new - - - name: Move cache - run: rsync -r /tmp/.buildx-cache-new /tmp/.buildx-cache --delete + cache-from: type=gha,mode=max + cache-to: type=gha,mode=max - name: Update repo description if: github.event_name != 'pull_request' @@ -357,12 +328,12 @@ jobs: ### Download artifacts, clean up ui, upload to release page ### ############################################################### create-release-artifacts: - needs: [build-ui,build-armhf,build-aarch64,build-amd64] + needs: [build-ui, build-bin] name: Create release artifacts if: github.event_name == 'release' runs-on: ubuntu-latest steps: - - name: Download All Artifacts + - name: Download all artifacts uses: actions/download-artifact@v3 with: path: bin/ @@ -370,12 +341,12 @@ jobs: run: ls -alR bin/ - name: Fixing Filename run: | - mv bin/aarch64-lldap-bin/lldap bin/aarch64-lldap - mv bin/amd64-lldap-bin/lldap bin/amd64-lldap - mv bin/armhf-lldap-bin/lldap bin/armhf-lldap - mv bin/aarch64-migration-tool-bin/migration-tool bin/aarch64-migration-tool - mv bin/amd64-migration-tool-bin/migration-tool bin/amd64-migration-tool - mv bin/armhf-migration-tool-bin/migration-tool bin/armhf-migration-tool + 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-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 chmod +x bin/*-lldap chmod +x bin/*-migration-tool @@ -384,7 +355,7 @@ jobs: with: name: ui path: web - - name: Web Cleanup + - name: UI (web) artifacts cleanup run: mkdir app && mv web/index.html app/index.html && mv web/static app/static && mv web/pkg app/pkg - name: Fetch web components run: | @@ -412,14 +383,14 @@ jobs: ls -alR amd64-lldap/ ls -alR armhf-lldap/ - - name: Compress + - name: Packing LLDAP and Web UI run: | tar -czvf aarch64-lldap.tar.gz aarch64-lldap/ tar -czvf amd64-lldap.tar.gz amd64-lldap/ tar -czvf armhf-lldap.tar.gz armhf-lldap/ - - name: Upload artifacts release + - name: Upload compressed release uses: ncipollo/release-action@v1 id: create_release with: From 3fa100be0c1644dd182075f26a07eeda0558076a Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Tue, 24 Jan 2023 10:38:06 +0100 Subject: [PATCH 15/62] server: update sea-orm dependency Fixes #405 --- Cargo.lock | 8 ++++---- server/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8ace93..aca26a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3428,9 +3428,9 @@ dependencies = [ [[package]] name = "sea-orm" -version = "0.10.3" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8744afc95ca462de12c2cea5a56d7e406f3be2b2683d3b05066e1afdba898bc5" +checksum = "88694d01b528a94f90ad87f8d2f546d060d070eee180315c67d158cb69476034" dependencies = [ "async-stream", "async-trait", @@ -3453,9 +3453,9 @@ dependencies = [ [[package]] name = "sea-orm-macros" -version = "0.10.3" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca4d01381fdcabc3818b6d39c5f1f0c885900af90da638e4001406907462784" +checksum = "7216195de9c6b2474fd0efab486173dccd0eff21f28cc54aa4c0205d52fb3af0" dependencies = [ "bae", "heck 0.3.3", diff --git a/server/Cargo.toml b/server/Cargo.toml index 9c38656..fc35e9e 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -109,7 +109,7 @@ default-features = false version = "0.24" [dependencies.sea-orm] -version= "0.10.3" +version= ">=0.10.7" default-features = false features = ["macros", "with-chrono", "with-uuid", "sqlx-all", "runtime-actix-rustls"] From d56de80381b24e95d9589a65d5bcd879ec0495e4 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Tue, 24 Jan 2023 13:31:22 +0100 Subject: [PATCH 16/62] server: Update lettre --- Cargo.lock | 4 ++-- server/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aca26a0..8c975a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2211,9 +2211,9 @@ dependencies = [ [[package]] name = "lettre" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5677c78c7c7ede1dd68e8a7078012bc625449fb304e7b509b917eaaedfe6e849" +checksum = "2eabca5e0b4d0e98e7f2243fb5b7520b6af2b65d8f87bcc86f2c75185a6ff243" dependencies = [ "async-trait", "base64", diff --git a/server/Cargo.toml b/server/Cargo.toml index fc35e9e..40f69dc 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -67,7 +67,7 @@ features = ["env-filter", "tracing-log"] [dependencies.lettre] features = ["builder", "serde", "smtp-transport", "tokio1-rustls-tls"] default-features = false -version = "0.10.0-rc.3" +version = "0.10.1" [dependencies.lldap_auth] path = "../auth" From 1e6a0edcfbb00ab39efd0f833f1570ba400e65bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Jan 2023 13:40:31 +0000 Subject: [PATCH 17/62] build(deps): bump bumpalo from 3.10.0 to 3.12.0 Bumps [bumpalo](https://github.com/fitzgen/bumpalo) from 3.10.0 to 3.12.0. - [Release notes](https://github.com/fitzgen/bumpalo/releases) - [Changelog](https://github.com/fitzgen/bumpalo/blob/main/CHANGELOG.md) - [Commits](https://github.com/fitzgen/bumpalo/compare/3.10.0...3.12.0) --- updated-dependencies: - dependency-name: bumpalo dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c975a8..1e0067b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -649,9 +649,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.10.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" [[package]] name = "bytemuck" From c3d18dbbe8f5bcc6091fd96d42fbf4278f070f3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jan 2023 20:07:23 +0000 Subject: [PATCH 18/62] build(deps): bump docker/build-push-action from 3 to 4 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3 to 4. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v3...v4) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/docker-build-static.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-build-static.yml b/.github/workflows/docker-build-static.yml index 620e8c5..c7a4604 100644 --- a/.github/workflows/docker-build-static.yml +++ b/.github/workflows/docker-build-static.yml @@ -265,7 +265,7 @@ jobs: ######################################## - name: Build and push latest alpine if: github.event_name != 'release' - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: context: . push: ${{ github.event_name != 'pull_request' }} @@ -277,7 +277,7 @@ jobs: - name: Build and push latest debian if: github.event_name != 'release' - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: context: . push: ${{ github.event_name != 'pull_request' }} @@ -292,7 +292,7 @@ jobs: ######################################## - name: Build and push release alpine if: github.event_name == 'release' - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: context: . platforms: linux/amd64,linux/arm64 @@ -305,7 +305,7 @@ jobs: - name: Build and push release debian if: github.event_name == 'release' - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: context: . platforms: linux/amd64,linux/arm64,linux/arm/v7 From 58b9c28a0bf8bd4c2c6e7061670c0b180ead834f Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 1 Feb 2023 17:32:52 +0530 Subject: [PATCH 19/62] example_configs: Add Dex example Fixes #428. --- README.md | 158 +++++++++++++++++---------------- example_configs/dex_config.yml | 32 +++++++ 2 files changed, 113 insertions(+), 77 deletions(-) create mode 100644 example_configs/dex_config.yml diff --git a/README.md b/README.md index 89c1279..ecf4c71 100644 --- a/README.md +++ b/README.md @@ -28,20 +28,20 @@

- - [About](#About) - - [Installation](#Installation) - - [With Docker](#With-Docker) - - [From source](#From-source) - - [Cross-compilation](#Cross-compilation) - - [Client configuration](#Client-configuration) - - [Compatible services](#compatible-services) - - [General configuration guide](#general-configuration-guide) - - [Sample client configurations](#Sample-client-configurations) - - [Comparisons with other services](#Comparisons-with-other-services) - - [vs OpenLDAP](#vs-openldap) - - [vs FreeIPA](#vs-freeipa) - - [I can't log in!](#i-cant-log-in) - - [Contributions](#Contributions) +- [About](#about) +- [Installation](#installation) + - [With Docker](#with-docker) + - [From source](#from-source) + - [Cross-compilation](#cross-compilation) +- [Client configuration](#client-configuration) + - [Compatible services](#compatible-services) + - [General configuration guide](#general-configuration-guide) + - [Sample client configurations](#sample-client-configurations) +- [Comparisons with other services](#comparisons-with-other-services) + - [vs OpenLDAP](#vs-openldap) + - [vs FreeIPA](#vs-freeipa) +- [I can't log in!](#i-cant-log-in) +- [Contributions](#contributions) ## About @@ -62,10 +62,11 @@ edit their own details or reset their password by email. The goal is _not_ to provide a full LDAP server; if you're interested in that, check out OpenLDAP. This server is a user management system that is: -* simple to setup (no messing around with `slapd`), -* simple to manage (friendly web UI), -* low resources, -* opinionated with basic defaults so you don't have to understand the + +- simple to setup (no messing around with `slapd`), +- simple to manage (friendly web UI), +- low resources, +- opinionated with basic defaults so you don't have to understand the subtleties of LDAP. It mostly targets self-hosting servers, with open-source components like @@ -98,14 +99,14 @@ contents are loaded into the respective configuration parameters. Note that `_FILE` variables take precedence. Example for docker compose: -* You can use either the `:latest` tag image or `:stable` as used in this example. -* `:latest` tag image contains recently pushed code or feature tests, in which some instability can be expected. -* If `UID` and `GID` no defined LLDAP will use default `UID` and `GID` number `1000`. -* If no `TZ` is set, default `UTC` timezone will be used. +- You can use either the `:latest` tag image or `:stable` as used in this example. +- `:latest` tag image contains recently pushed code or feature tests, in which some instability can be expected. +- If `UID` and `GID` no defined LLDAP will use default `UID` and `GID` number `1000`. +- If no `TZ` is set, default `UTC` timezone will be used. ```yaml -version: '3' +version: "3" volumes: lldap_data: @@ -139,9 +140,9 @@ front-end. To compile the project, you'll need: -* nodejs 16: [nodesource nodejs installation guide](https://github.com/nodesource/distributions) -* curl: `sudo apt install curl` -* Rust/Cargo: [rustup.rs](https://rustup.rs/) +- nodejs 16: [nodesource nodejs installation guide](https://github.com/nodesource/distributions) +- curl: `sudo apt install curl` +- Rust/Cargo: [rustup.rs](https://rustup.rs/) Then you can compile the server (and the migration tool if you want): @@ -155,8 +156,8 @@ just run `cargo run -- run` to run the server. To bring up the server, you'll need to compile the frontend. In addition to cargo, you'll need: -* WASM-pack: `cargo install wasm-pack` -* rollup.js: `npm install rollup` +- WASM-pack: `cargo install wasm-pack` +- rollup.js: `npm install rollup` Then you can build the frontend files with `./app/build.sh` (you'll need to run this after every front-end change to update the WASM package served). @@ -204,14 +205,15 @@ the config). ### General configuration guide To configure the services that will talk to LLDAP, here are the values: - - The LDAP user DN is from the configuration. By default, - `cn=admin,ou=people,dc=example,dc=com`. - - The LDAP password is from the configuration (same as to log in to the web - UI). - - The users are all located in `ou=people,` + the base DN, so by default user - `bob` is at `cn=bob,ou=people,dc=example,dc=com`. - - Similarly, the groups are located in `ou=groups`, so the group `family` - will be at `cn=family,ou=groups,dc=example,dc=com`. + +- The LDAP user DN is from the configuration. By default, + `cn=admin,ou=people,dc=example,dc=com`. +- The LDAP password is from the configuration (same as to log in to the web + UI). +- The users are all located in `ou=people,` + the base DN, so by default user + `bob` is at `cn=bob,ou=people,dc=example,dc=com`. +- Similarly, the groups are located in `ou=groups`, so the group `family` + will be at `cn=family,ou=groups,dc=example,dc=com`. Testing group membership through `memberOf` is supported, so you can have a filter like: `(memberOf=cn=admins,ou=groups,dc=example,dc=com)`. @@ -226,33 +228,35 @@ administration access to many services. Some specific clients have been tested to work and come with sample configuration files, or guides. See the [`example_configs`](example_configs) folder for help with: - - [Airsonic Advanced](example_configs/airsonic-advanced.md) - - [Apache Guacamole](example_configs/apacheguacamole.md) - - [Authelia](example_configs/authelia_config.yml) - - [Bookstack](example_configs/bookstack.env.example) - - [Calibre-Web](example_configs/calibre_web.md) - - [Dell iDRAC](example_configs/dell_idrac.md) - - [Dokuwiki](example_configs/dokuwiki.md) - - [Dolibarr](example_configs/dolibarr.md) - - [Emby](example_configs/emby.md) - - [Gitea](example_configs/gitea.md) - - [Grafana](example_configs/grafana_ldap_config.toml) - - [Hedgedoc](example_configs/hedgedoc.md) - - [Jellyfin](example_configs/jellyfin.md) - - [Jitsi Meet](example_configs/jitsi_meet.conf) - - [KeyCloak](example_configs/keycloak.md) - - [Matrix](example_configs/matrix_synapse.yml) - - [Nextcloud](example_configs/nextcloud.md) - - [Organizr](example_configs/Organizr.md) - - [Portainer](example_configs/portainer.md) - - [Seafile](example_configs/seafile.md) - - [Syncthing](example_configs/syncthing.md) - - [Vaultwarden](example_configs/vaultwarden.md) - - [WeKan](example_configs/wekan.md) - - [WG Portal](example_configs/wg_portal.env.example) - - [WikiJS](example_configs/wikijs.md) - - [XBackBone](example_configs/xbackbone_config.php) - - [Zendto](example_configs/zendto.md) + +- [Airsonic Advanced](example_configs/airsonic-advanced.md) +- [Apache Guacamole](example_configs/apacheguacamole.md) +- [Authelia](example_configs/authelia_config.yml) +- [Bookstack](example_configs/bookstack.env.example) +- [Calibre-Web](example_configs/calibre_web.md) +- [Dell iDRAC](example_configs/dell_idrac.md) +- [Dex](example_configs/dex_config.yml) +- [Dokuwiki](example_configs/dokuwiki.md) +- [Dolibarr](example_configs/dolibarr.md) +- [Emby](example_configs/emby.md) +- [Gitea](example_configs/gitea.md) +- [Grafana](example_configs/grafana_ldap_config.toml) +- [Hedgedoc](example_configs/hedgedoc.md) +- [Jellyfin](example_configs/jellyfin.md) +- [Jitsi Meet](example_configs/jitsi_meet.conf) +- [KeyCloak](example_configs/keycloak.md) +- [Matrix](example_configs/matrix_synapse.yml) +- [Nextcloud](example_configs/nextcloud.md) +- [Organizr](example_configs/Organizr.md) +- [Portainer](example_configs/portainer.md) +- [Seafile](example_configs/seafile.md) +- [Syncthing](example_configs/syncthing.md) +- [Vaultwarden](example_configs/vaultwarden.md) +- [WeKan](example_configs/wekan.md) +- [WG Portal](example_configs/wg_portal.env.example) +- [WikiJS](example_configs/wikijs.md) +- [XBackBone](example_configs/xbackbone_config.php) +- [Zendto](example_configs/zendto.md) ## Comparisons with other services @@ -291,20 +295,20 @@ use. It also comes conveniently packed in a docker container. If you just set up the server, can get to the login page but the password you set isn't working, try the following: - - (For docker): Make sure that the `/data` folder is persistent, either to a - docker volume or mounted from the host filesystem. - - Check if there is a `lldap_config.toml` file (either in `/data` for docker - or in the current directory). If there isn't, copy - `lldap_config.docker_template.toml` there, and fill in the various values - (passwords, secrets, ...). - - Check if there is a `users.db` file (either in `/data` for docker or where - you specified the DB URL, which defaults to the current directory). If - there isn't, check that the user running the command (user with ID 10001 - for docker) has the rights to write to the `/data` folder. If in doubt, you - can `chmod 777 /data` (or whatever the folder) to make it world-writeable. - - Make sure you restart the server. - - If it's still not working, join the - [Discord server](https://discord.gg/h5PEdRMNyP) to ask for help. +- (For docker): Make sure that the `/data` folder is persistent, either to a + docker volume or mounted from the host filesystem. +- Check if there is a `lldap_config.toml` file (either in `/data` for docker + or in the current directory). If there isn't, copy + `lldap_config.docker_template.toml` there, and fill in the various values + (passwords, secrets, ...). +- Check if there is a `users.db` file (either in `/data` for docker or where + you specified the DB URL, which defaults to the current directory). If + there isn't, check that the user running the command (user with ID 10001 + for docker) has the rights to write to the `/data` folder. If in doubt, you + can `chmod 777 /data` (or whatever the folder) to make it world-writeable. +- Make sure you restart the server. +- If it's still not working, join the + [Discord server](https://discord.gg/h5PEdRMNyP) to ask for help. ## Contributions diff --git a/example_configs/dex_config.yml b/example_configs/dex_config.yml new file mode 100644 index 0000000..0c566ec --- /dev/null +++ b/example_configs/dex_config.yml @@ -0,0 +1,32 @@ +# lldap configuration: +# LLDAP_LDAP_BASE_DN: dc=example,dc=com + +# ############################## +# rest of the Dex options +# ############################## + +connectors: + - type: ldap + id: ldap + name: LDAP + config: + host: lldap-host # make sure it does not start with `ldap://` + port: 3890 # or 6360 if you have ldaps enabled + insecureNoSSL: true # or false if you have ldaps enabled + insecureSkipVerify: true # or false if you have ldaps enabled + bindDN: uid=admin,ou=people,dc=example,dc=com # replace admin with your admin user + bindPW: very-secure-password # replace with your admin password + userSearch: + baseDN: ou=people,dc=example,dc=com + username: uid + idAttr: uid + emailAttr: mail + nameAttr: displayName + preferredUsernameAttr: uid + groupSearch: + baseDN: ou=groups,dc=example,dc=com + filter: "(objectClass=groupOfUniqueNames)" + userMatchers: + - userAttr: uid + groupAttr: member + nameAttr: displayName From 648848c816dd9dd4fa27567bb223e1baf1d22b62 Mon Sep 17 00:00:00 2001 From: Rex Zhang Date: Wed, 8 Feb 2023 17:30:23 +0800 Subject: [PATCH 20/62] example_configs: Add note for Gitea's simple auth mode --- example_configs/gitea.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/example_configs/gitea.md b/example_configs/gitea.md index 654d76e..f14fff6 100644 --- a/example_configs/gitea.md +++ b/example_configs/gitea.md @@ -41,3 +41,9 @@ Check `Remove Users from syncronised teams...` The `Map LDAP groups to Organization teams` config is JSON formatted and can be extended to as many groups as needed. Replace every instance of `dc=example,dc=com` with your configured domain. + +# Configuration for Gitea in `simple auth` mode + +* The configuration method is the same as `BindDN` mode. +* `BindDN` and `password` are not required +* Gitea will not be able to pre-sync users, user account will be created at login time. From 8f2c5b397cfc695e1fd739bca31227d7dd5505c2 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Fri, 10 Feb 2023 11:07:14 +0100 Subject: [PATCH 21/62] server: allow NULL for display_name Fixes #387. --- server/src/domain/sql_migrations.rs | 86 +++++++++++++++++++++++++++-- server/src/domain/sql_tables.rs | 45 ++++++++++----- 2 files changed, 114 insertions(+), 17 deletions(-) diff --git a/server/src/domain/sql_migrations.rs b/server/src/domain/sql_migrations.rs index 7efb152..e4ed092 100644 --- a/server/src/domain/sql_migrations.rs +++ b/server/src/domain/sql_migrations.rs @@ -2,10 +2,12 @@ use crate::domain::{ sql_tables::{DbConnection, SchemaVersion}, types::{GroupId, UserId, Uuid}, }; -use sea_orm::{ConnectionTrait, FromQueryResult, Statement}; +use sea_orm::{ConnectionTrait, FromQueryResult, Statement, TransactionTrait}; use sea_query::{ColumnDef, Expr, ForeignKey, ForeignKeyAction, Iden, Query, Table, Value}; use serde::{Deserialize, Serialize}; -use tracing::{instrument, warn}; +use tracing::{info, instrument, warn}; + +use super::sql_tables::LAST_SCHEMA_VERSION; #[derive(Iden, PartialEq, Eq, Debug, Serialize, Deserialize, Clone)] pub enum Users { @@ -331,11 +333,87 @@ pub async fn upgrade_to_v1(pool: &DbConnection) -> std::result::Result<(), sea_o } pub async fn migrate_from_version( - _pool: &DbConnection, + pool: &DbConnection, version: SchemaVersion, ) -> anyhow::Result<()> { - if version.0 > 1 { + if version > LAST_SCHEMA_VERSION { anyhow::bail!("DB version downgrading is not supported"); + } else if version == LAST_SCHEMA_VERSION { + return Ok(()); } + info!( + "Upgrading DB schema from {} to {}", + version.0, LAST_SCHEMA_VERSION.0 + ); + let builder = pool.get_database_backend(); + if version < SchemaVersion(2) { + // Drop the not_null constraint on display_name. Due to Sqlite, this is more complicated: + // - rename the display_name column to a temporary name + // - create the display_name column without the constraint + // - copy the data from the temp column to the new one + // - update the new one to replace empty strings with null + // - drop the old one + pool.transaction::<_, (), sea_orm::DbErr>(|transaction| { + Box::pin(async move { + #[derive(Iden)] + enum TempUsers { + TempDisplayName, + } + transaction + .execute( + builder.build( + Table::alter() + .table(Users::Table) + .rename_column(Users::DisplayName, TempUsers::TempDisplayName), + ), + ) + .await?; + transaction + .execute( + builder.build( + Table::alter() + .table(Users::Table) + .add_column(ColumnDef::new(Users::DisplayName).string_len(255)), + ), + ) + .await?; + transaction + .execute(builder.build(Query::update().table(Users::Table).value( + Users::DisplayName, + Expr::col((Users::Table, TempUsers::TempDisplayName)), + ))) + .await?; + transaction + .execute( + builder.build( + Query::update() + .table(Users::Table) + .value(Users::DisplayName, Option::::None) + .cond_where(Expr::col(Users::DisplayName).eq("")), + ), + ) + .await?; + transaction + .execute( + builder.build( + Table::alter() + .table(Users::Table) + .drop_column(TempUsers::TempDisplayName), + ), + ) + .await?; + Ok(()) + }) + }) + .await?; + } + pool.execute( + builder.build( + Query::update() + .table(Metadata::Table) + .value(Metadata::Version, Value::from(LAST_SCHEMA_VERSION)), + ), + ) + .await?; Ok(()) } diff --git a/server/src/domain/sql_tables.rs b/server/src/domain/sql_tables.rs index 0f202b0..0a81363 100644 --- a/server/src/domain/sql_tables.rs +++ b/server/src/domain/sql_tables.rs @@ -3,7 +3,7 @@ use sea_orm::Value; pub type DbConnection = sea_orm::DatabaseConnection; -#[derive(Copy, PartialEq, Eq, Debug, Clone)] +#[derive(Copy, PartialEq, Eq, Debug, Clone, PartialOrd, Ord)] pub struct SchemaVersion(pub i16); impl sea_orm::TryGetable for SchemaVersion { @@ -22,6 +22,8 @@ impl From for Value { } } +pub const LAST_SCHEMA_VERSION: SchemaVersion = SchemaVersion(2); + pub async fn init_table(pool: &DbConnection) -> anyhow::Result<()> { let version = { if let Some(version) = get_schema_version(pool).await { @@ -99,14 +101,21 @@ mod tests { let sql_pool = get_in_memory_db().await; sql_pool .execute(raw_statement( - r#"CREATE TABLE users ( user_id TEXT , creation_date TEXT);"#, + r#"CREATE TABLE users ( user_id TEXT, display_name TEXT, creation_date TEXT);"#, )) .await .unwrap(); sql_pool .execute(raw_statement( - r#"INSERT INTO users (user_id, creation_date) - VALUES ("bôb", "1970-01-01 00:00:00")"#, + r#"INSERT INTO users (user_id, display_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, creation_date) + VALUES ("john", "John Doe", "1971-01-01 00:00:00")"#, )) .await .unwrap(); @@ -132,17 +141,27 @@ mod tests { .await .unwrap(); #[derive(FromQueryResult, PartialEq, Eq, Debug)] - struct JustUuid { + struct SimpleUser { + display_name: Option, uuid: Uuid, } assert_eq!( - JustUuid::find_by_statement(raw_statement(r#"SELECT uuid FROM users"#)) - .all(&sql_pool) - .await - .unwrap(), - vec![JustUuid { - uuid: crate::uuid!("a02eaf13-48a7-30f6-a3d4-040ff7c52b04") - }] + SimpleUser::find_by_statement(raw_statement( + r#"SELECT display_name, uuid FROM users ORDER BY display_name"# + )) + .all(&sql_pool) + .await + .unwrap(), + vec![ + SimpleUser { + display_name: None, + uuid: crate::uuid!("a02eaf13-48a7-30f6-a3d4-040ff7c52b04") + }, + SimpleUser { + display_name: Some("John Doe".to_owned()), + uuid: crate::uuid!("986765a5-3f03-389e-b47b-536b2d6e1bec") + } + ] ); #[derive(FromQueryResult, PartialEq, Eq, Debug)] struct ShortGroupDetails { @@ -180,7 +199,7 @@ mod tests { .unwrap() .unwrap(), sql_migrations::JustSchemaVersion { - version: SchemaVersion(1) + version: LAST_SCHEMA_VERSION } ); } From 96eb17a9632fef1326082306a5b6e70886e13ea3 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Fri, 10 Feb 2023 11:37:36 +0100 Subject: [PATCH 22/62] server: fix clippy warning The clippy::uninlined_format_args warning in 1.67 was downgraded to pedantic in 1.67.1 due to lack of support in rust-analyzer, so we're not updating that one yet. --- app/src/lib.rs | 3 ++- migration-tool/src/main.rs | 2 ++ server/src/domain/sql_migrations.rs | 15 +++++++-------- server/src/main.rs | 3 ++- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/src/lib.rs b/app/src/lib.rs index 3937693..c079976 100644 --- a/app/src/lib.rs +++ b/app/src/lib.rs @@ -1,6 +1,7 @@ #![recursion_limit = "256"] #![forbid(non_ascii_idents)] -#![allow(clippy::nonstandard_macro_braces)] +#![allow(clippy::uninlined_format_args)] + pub mod components; pub mod infra; diff --git a/migration-tool/src/main.rs b/migration-tool/src/main.rs index 7685a9c..cf1358e 100644 --- a/migration-tool/src/main.rs +++ b/migration-tool/src/main.rs @@ -1,3 +1,5 @@ +#![allow(clippy::uninlined_format_args)] + use std::collections::HashSet; use anyhow::{anyhow, Result}; diff --git a/server/src/domain/sql_migrations.rs b/server/src/domain/sql_migrations.rs index e4ed092..deb82c7 100644 --- a/server/src/domain/sql_migrations.rs +++ b/server/src/domain/sql_migrations.rs @@ -336,15 +336,14 @@ pub async fn migrate_from_version( pool: &DbConnection, version: SchemaVersion, ) -> anyhow::Result<()> { - if version > LAST_SCHEMA_VERSION { - anyhow::bail!("DB version downgrading is not supported"); - } else if version == LAST_SCHEMA_VERSION { - return Ok(()); + 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 {} to {}", - version.0, LAST_SCHEMA_VERSION.0 - ); let builder = pool.get_database_backend(); if version < SchemaVersion(2) { // Drop the not_null constraint on display_name. Due to Sqlite, this is more complicated: diff --git a/server/src/main.rs b/server/src/main.rs index 2c67bd9..712d0a9 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,6 +1,7 @@ #![forbid(unsafe_code)] #![forbid(non_ascii_idents)] -#![allow(clippy::nonstandard_macro_braces)] +// TODO: Remove next line once ubuntu upgrades rustc to >=1.67.1 +#![allow(clippy::uninlined_format_args)] use std::time::Duration; From 63cbf30dd7c3975e88dc03f27c99909b3b8bf3dd Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Fri, 10 Feb 2023 12:32:41 +0100 Subject: [PATCH 23/62] server: upgrade sea-orm to 0.11 --- Cargo.lock | 43 +++++-------------- server/Cargo.toml | 6 +-- server/src/domain/model/users.rs | 6 +-- .../src/domain/sql_group_backend_handler.rs | 2 +- server/src/domain/sql_migrations.rs | 6 ++- server/src/domain/sql_tables.rs | 7 ++- server/src/domain/sql_user_backend_handler.rs | 3 +- server/src/domain/types.rs | 21 +++++---- server/src/infra/jwt_sql_tables.rs | 6 ++- server/src/infra/sql_backend_handler.rs | 6 +-- 10 files changed, 43 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e0067b..56eb8f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -855,21 +855,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53757d12b596c16c78b83458d732a5d1a17ab3f53f2f7412f6fb57cc8a140ab3" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff" - [[package]] name = "crc32fast" version = "1.3.2" @@ -2332,7 +2317,6 @@ dependencies = [ "rustls 0.20.6", "rustls-pemfile", "sea-orm", - "sea-query", "secstr", "serde", "serde_bytes", @@ -3428,15 +3412,14 @@ dependencies = [ [[package]] name = "sea-orm" -version = "0.10.7" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88694d01b528a94f90ad87f8d2f546d060d070eee180315c67d158cb69476034" +checksum = "e7a0e3ec90718d849c73b167df7a476672b64c7ee5f3c582179069e63b2451e1" dependencies = [ "async-stream", "async-trait", "chrono", "futures", - "futures-util", "log", "ouroboros", "sea-orm-macros", @@ -3453,9 +3436,9 @@ dependencies = [ [[package]] name = "sea-orm-macros" -version = "0.10.7" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7216195de9c6b2474fd0efab486173dccd0eff21f28cc54aa4c0205d52fb3af0" +checksum = "5d89f7d4d2533c178e08a9e1990619c391e9ca7b402851d02a605938b15e03d9" dependencies = [ "bae", "heck 0.3.3", @@ -3466,9 +3449,9 @@ dependencies = [ [[package]] name = "sea-query" -version = "0.27.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4f0fc4d8e44e1d51c739a68d336252a18bc59553778075d5e32649be6ec92ed" +checksum = "d2fbe015dbdaa7d8829d71c1e14fb6289e928ac256b93dfda543c85cd89d6f03" dependencies = [ "chrono", "sea-query-derive", @@ -3477,9 +3460,9 @@ dependencies = [ [[package]] name = "sea-query-binder" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2585b89c985cfacfe0ec9fc9e7bb055b776c1a2581c4e3c6185af2b8bf8865" +checksum = "03548c63aec07afd4fd190923e0160d2f2fc92def27470b54154cf232da6203b" dependencies = [ "chrono", "sea-query", @@ -3489,11 +3472,11 @@ dependencies = [ [[package]] name = "sea-query-derive" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34cdc022b4f606353fe5dc85b09713a04e433323b70163e81513b141c6ae6eb5" +checksum = "63f62030c60f3a691f5fe251713b4e220b306e50a71e1d6f9cce1f24bb781978" dependencies = [ - "heck 0.3.3", + "heck 0.4.0", "proc-macro2", "quote", "syn", @@ -3827,7 +3810,6 @@ dependencies = [ "byteorder", "bytes", "chrono", - "crc", "crossbeam-queue", "digest 0.10.6", "dirs", @@ -3888,7 +3870,6 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "sha2 0.10.6", "sqlx-core", "sqlx-rt", "syn", @@ -4508,9 +4489,7 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" dependencies = [ - "getrandom 0.2.7", "md-5", - "serde", ] [[package]] diff --git a/server/Cargo.toml b/server/Cargo.toml index 40f69dc..04d212f 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -72,10 +72,6 @@ version = "0.10.1" [dependencies.lldap_auth] path = "../auth" -[dependencies.sea-query] -version = "*" -features = ["with-chrono"] - [dependencies.opaque-ke] version = "0.6" @@ -109,7 +105,7 @@ default-features = false version = "0.24" [dependencies.sea-orm] -version= ">=0.10.7" +version= "0.11" default-features = false features = ["macros", "with-chrono", "with-uuid", "sqlx-all", "runtime-actix-rustls"] diff --git a/server/src/domain/model/users.rs b/server/src/domain/model/users.rs index 32f8d86..84b583a 100644 --- a/server/src/domain/model/users.rs +++ b/server/src/domain/model/users.rs @@ -1,6 +1,6 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.3 -use sea_orm::entity::prelude::*; +use sea_orm::{entity::prelude::*, sea_query::BlobSize}; use serde::{Deserialize, Serialize}; use crate::domain::types::{JpegPhoto, UserId, Uuid}; @@ -56,9 +56,9 @@ impl ColumnTrait for Column { Column::DisplayName => ColumnType::String(Some(255)), Column::FirstName => ColumnType::String(Some(255)), Column::LastName => ColumnType::String(Some(255)), - Column::Avatar => ColumnType::Binary, + Column::Avatar => ColumnType::Binary(BlobSize::Long), Column::CreationDate => ColumnType::DateTime, - Column::PasswordHash => ColumnType::Binary, + Column::PasswordHash => ColumnType::Binary(BlobSize::Medium), Column::TotpSecret => ColumnType::String(Some(64)), Column::MfaType => ColumnType::String(Some(64)), Column::Uuid => ColumnType::String(Some(36)), diff --git a/server/src/domain/sql_group_backend_handler.rs b/server/src/domain/sql_group_backend_handler.rs index 5367090..e5677a4 100644 --- a/server/src/domain/sql_group_backend_handler.rs +++ b/server/src/domain/sql_group_backend_handler.rs @@ -7,10 +7,10 @@ use crate::domain::{ }; use async_trait::async_trait; use sea_orm::{ + sea_query::{Cond, IntoCondition, SimpleExpr}, ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, QueryTrait, }; -use sea_query::{Cond, IntoCondition, SimpleExpr}; use tracing::{debug, instrument}; fn get_group_filter_expr(filter: GroupRequestFilter) -> Cond { diff --git a/server/src/domain/sql_migrations.rs b/server/src/domain/sql_migrations.rs index deb82c7..7be7b7f 100644 --- a/server/src/domain/sql_migrations.rs +++ b/server/src/domain/sql_migrations.rs @@ -2,8 +2,10 @@ use crate::domain::{ sql_tables::{DbConnection, SchemaVersion}, types::{GroupId, UserId, Uuid}, }; -use sea_orm::{ConnectionTrait, FromQueryResult, Statement, TransactionTrait}; -use sea_query::{ColumnDef, Expr, ForeignKey, ForeignKeyAction, Iden, Query, Table, Value}; +use sea_orm::{ + sea_query::{self, ColumnDef, Expr, ForeignKey, ForeignKeyAction, Query, Table, Value}, + ConnectionTrait, FromQueryResult, Iden, Statement, TransactionTrait, +}; use serde::{Deserialize, Serialize}; use tracing::{info, instrument, warn}; diff --git a/server/src/domain/sql_tables.rs b/server/src/domain/sql_tables.rs index 0a81363..b61fd93 100644 --- a/server/src/domain/sql_tables.rs +++ b/server/src/domain/sql_tables.rs @@ -7,12 +7,11 @@ pub type DbConnection = sea_orm::DatabaseConnection; pub struct SchemaVersion(pub i16); impl sea_orm::TryGetable for SchemaVersion { - fn try_get( + fn try_get_by( res: &sea_orm::QueryResult, - pre: &str, - col: &str, + index: I, ) -> Result { - Ok(SchemaVersion(i16::try_get(res, pre, col)?)) + Ok(SchemaVersion(i16::try_get_by(res, index)?)) } } diff --git a/server/src/domain/sql_user_backend_handler.rs b/server/src/domain/sql_user_backend_handler.rs index 9220dff..a481565 100644 --- a/server/src/domain/sql_user_backend_handler.rs +++ b/server/src/domain/sql_user_backend_handler.rs @@ -8,11 +8,10 @@ use super::{ use async_trait::async_trait; use sea_orm::{ entity::IntoActiveValue, - sea_query::{Cond, Expr, IntoCondition, SimpleExpr}, + sea_query::{Alias, Cond, Expr, IntoColumnRef, IntoCondition, SimpleExpr}, ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, ModelTrait, QueryFilter, QueryOrder, QuerySelect, QueryTrait, Set, }; -use sea_query::{Alias, IntoColumnRef}; use std::collections::HashSet; use tracing::{debug, instrument}; diff --git a/server/src/domain/types.rs b/server/src/domain/types.rs index 494f8f9..99ee6f4 100644 --- a/server/src/domain/types.rs +++ b/server/src/domain/types.rs @@ -53,8 +53,11 @@ impl std::string::ToString for Uuid { } impl TryGetable for Uuid { - fn try_get(res: &QueryResult, pre: &str, col: &str) -> std::result::Result { - Ok(Uuid(String::try_get(res, pre, col)?)) + fn try_get_by( + res: &QueryResult, + index: I, + ) -> std::result::Result { + Ok(Uuid(String::try_get_by(res, index)?)) } } @@ -142,8 +145,8 @@ impl From<&UserId> for Value { } impl TryGetable for UserId { - fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { - Ok(UserId::new(&String::try_get(res, pre, col)?)) + fn try_get_by(res: &QueryResult, index: I) -> Result { + Ok(UserId::new(&String::try_get_by(res, index)?)) } } @@ -261,8 +264,8 @@ impl JpegPhoto { } impl TryGetable for JpegPhoto { - fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { - >>::try_from(Vec::::try_get(res, pre, col)?) + fn try_get_by(res: &QueryResult, index: I) -> Result { + >>::try_from(Vec::::try_get_by(res, index)?) .map_err(|e| { TryGetError::DbErr(DbErr::TryIntoErr { from: "[u8]", @@ -345,8 +348,8 @@ impl From for Value { } impl TryGetable for GroupId { - fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { - Ok(GroupId(i32::try_get(res, pre, col)?)) + fn try_get_by(res: &QueryResult, index: I) -> Result { + Ok(GroupId(i32::try_get_by(res, index)?)) } } @@ -364,7 +367,7 @@ impl ValueType for GroupId { } fn column_type() -> ColumnType { - ColumnType::Integer(None) + ColumnType::Integer } } diff --git a/server/src/infra/jwt_sql_tables.rs b/server/src/infra/jwt_sql_tables.rs index b3443c6..2998235 100644 --- a/server/src/infra/jwt_sql_tables.rs +++ b/server/src/infra/jwt_sql_tables.rs @@ -1,5 +1,7 @@ -use sea_orm::ConnectionTrait; -use sea_query::{ColumnDef, ForeignKey, ForeignKeyAction, Iden, Table}; +use sea_orm::{ + sea_query::{self, ColumnDef, ForeignKey, ForeignKeyAction, Iden, Table}, + ConnectionTrait, +}; pub use crate::domain::{sql_migrations::Users, sql_tables::DbConnection}; diff --git a/server/src/infra/sql_backend_handler.rs b/server/src/infra/sql_backend_handler.rs index 253eca8..16e3f1a 100644 --- a/server/src/infra/sql_backend_handler.rs +++ b/server/src/infra/sql_backend_handler.rs @@ -7,10 +7,10 @@ use crate::domain::{ }; use async_trait::async_trait; use sea_orm::{ - sea_query::Cond, ActiveModelTrait, ColumnTrait, EntityTrait, FromQueryResult, IntoActiveModel, - QueryFilter, QuerySelect, + sea_query::{Cond, Expr}, + ActiveModelTrait, ColumnTrait, EntityTrait, FromQueryResult, IntoActiveModel, QueryFilter, + QuerySelect, }; -use sea_query::Expr; use std::collections::HashSet; use tracing::{debug, instrument}; From d04305433f79143b4f2dfbffb0831b2ff28550fd Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Fri, 10 Feb 2023 12:43:49 +0100 Subject: [PATCH 24/62] server: use the new into_tuple from sea_orm --- server/src/domain/sql_opaque_handler.rs | 10 +++------- server/src/infra/sql_backend_handler.rs | 16 +++++----------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/server/src/domain/sql_opaque_handler.rs b/server/src/domain/sql_opaque_handler.rs index b2ded01..5a5667b 100644 --- a/server/src/domain/sql_opaque_handler.rs +++ b/server/src/domain/sql_opaque_handler.rs @@ -8,7 +8,7 @@ use super::{ }; use async_trait::async_trait; use lldap_auth::opaque; -use sea_orm::{ActiveModelTrait, ActiveValue, EntityTrait, FromQueryResult, QuerySelect}; +use sea_orm::{ActiveModelTrait, ActiveValue, EntityTrait, QuerySelect}; use secstr::SecUtf8; use tracing::{debug, instrument}; @@ -50,18 +50,14 @@ impl SqlBackendHandler { #[instrument(skip_all, level = "debug", err)] async fn get_password_file_for_user(&self, user_id: UserId) -> Result>> { - #[derive(FromQueryResult)] - struct OnlyPasswordHash { - password_hash: Option>, - } // Fetch the previously registered password file from the DB. Ok(model::User::find_by_id(user_id) .select_only() .column(UserColumn::PasswordHash) - .into_model::() + .into_tuple::<(Option>,)>() .one(&self.sql_pool) .await? - .and_then(|u| u.password_hash)) + .and_then(|u| u.0)) } } diff --git a/server/src/infra/sql_backend_handler.rs b/server/src/infra/sql_backend_handler.rs index 16e3f1a..54ca857 100644 --- a/server/src/infra/sql_backend_handler.rs +++ b/server/src/infra/sql_backend_handler.rs @@ -8,8 +8,7 @@ use crate::domain::{ use async_trait::async_trait; use sea_orm::{ sea_query::{Cond, Expr}, - ActiveModelTrait, ColumnTrait, EntityTrait, FromQueryResult, IntoActiveModel, QueryFilter, - QuerySelect, + ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, QuerySelect, }; use std::collections::HashSet; use tracing::{debug, instrument}; @@ -24,11 +23,6 @@ fn gen_random_string(len: usize) -> String { .collect() } -#[derive(FromQueryResult)] -struct OnlyJwtHash { - jwt_hash: i64, -} - #[async_trait] impl TcpBackendHandler for SqlBackendHandler { #[instrument(skip_all, level = "debug")] @@ -37,11 +31,11 @@ impl TcpBackendHandler for SqlBackendHandler { .select_only() .column(JwtStorageColumn::JwtHash) .filter(JwtStorageColumn::Blacklisted.eq(true)) - .into_model::() + .into_tuple::<(i64,)>() .all(&self.sql_pool) .await? .into_iter() - .map(|m| m.jwt_hash as u64) + .map(|m| m.0 as u64) .collect::>()) } @@ -91,11 +85,11 @@ impl TcpBackendHandler for SqlBackendHandler { .add(JwtStorageColumn::UserId.eq(user)) .add(JwtStorageColumn::Blacklisted.eq(false)), ) - .into_model::() + .into_tuple::<(i64,)>() .all(&self.sql_pool) .await? .into_iter() - .map(|t| t.jwt_hash as u64) + .map(|t| t.0 as u64) .collect::>(); model::JwtStorage::update_many() .col_expr(JwtStorageColumn::Blacklisted, Expr::value(true)) From 94d45f7320c47e7af6796478c3a50d0760492926 Mon Sep 17 00:00:00 2001 From: Juli <49914615+SnowJuli@users.noreply.github.com> Date: Sun, 12 Feb 2023 11:10:52 +0100 Subject: [PATCH 25/62] example_configs: Added explanation to Jellyfin Docs --- example_configs/jellyfin.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/example_configs/jellyfin.md b/example_configs/jellyfin.md index 6b43064..5d71446 100644 --- a/example_configs/jellyfin.md +++ b/example_configs/jellyfin.md @@ -35,6 +35,12 @@ Otherwise, just use: ``` (uid=*) ``` +### Admin Base DN + +The DN of your admin group. If you have `media_admin` as your group you would use: +``` +cn=media_admin,ou=groups,dc=example,dc=com +``` ### Admin Filter From e92947fc3b7261799ded448f8defdb55b3b87d63 Mon Sep 17 00:00:00 2001 From: DarkSpir Date: Mon, 13 Feb 2023 09:29:54 +0100 Subject: [PATCH 26/62] app: Change input field to password type in change_password ui (#443) Change input field type for field old_password from its default "text" to "password" Fixes #442 --- app/src/components/change_password.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/components/change_password.rs b/app/src/components/change_password.rs index 91e1673..eabec53 100644 --- a/app/src/components/change_password.rs +++ b/app/src/components/change_password.rs @@ -246,6 +246,7 @@ impl Component for ChangePasswordForm { Date: Mon, 13 Feb 2023 12:03:14 +0100 Subject: [PATCH 27/62] server: Add support for LdapCompare op --- Cargo.lock | 34 ++- Cargo.toml | 5 - server/Cargo.toml | 2 +- server/src/domain/ldap/group.rs | 47 ++-- server/src/domain/ldap/user.rs | 77 +++--- server/src/domain/ldap/utils.rs | 2 +- server/src/infra/ldap_handler.rs | 427 ++++++++++++++++++++++++------- 7 files changed, 439 insertions(+), 155 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 56eb8f9..733a60a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2185,13 +2185,16 @@ dependencies = [ [[package]] name = "ldap3_proto" -version = "0.2.3" -source = "git+https://github.com/nitnelave/ldap3_server/?rev=7b50b2b82c383f5f70e02e11072bb916629ed2bc#7b50b2b82c383f5f70e02e11072bb916629ed2bc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4162706b6f3b3d58f577990e22e9a0e03e2f9bedc2b8181d8abab2498da32003" dependencies = [ "bytes", "lber", + "peg", "tokio-util 0.7.3", "tracing", + "uuid 1.2.2", ] [[package]] @@ -2864,6 +2867,33 @@ dependencies = [ "syn", ] +[[package]] +name = "peg" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a07f2cafdc3babeebc087e499118343442b742cc7c31b4d054682cc598508554" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a90084dc05cf0428428e3d12399f39faad19b0909f64fb9170c9fdd6d9cd49b" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa00462b37ead6d11a82c9d568b26682d78e0477dc02d1966c013af80969739" + [[package]] name = "pem-rfc7468" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index f2283cd..85e1dd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,11 +8,6 @@ members = [ default-members = ["server"] -# Remove once https://github.com/kanidm/ldap3_proto/pull/8 is merged. -[patch.crates-io.ldap3_proto] -git = 'https://github.com/nitnelave/ldap3_server/' -rev = '7b50b2b82c383f5f70e02e11072bb916629ed2bc' - [patch.crates-io.opaque-ke] git = 'https://github.com/nitnelave/opaque-ke/' branch = 'zeroize_1.5' diff --git a/server/Cargo.toml b/server/Cargo.toml index 04d212f..824b569 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -28,7 +28,7 @@ itertools = "0.10.1" juniper = "0.15.10" juniper_actix = "0.4.0" jwt = "0.13" -ldap3_proto = "*" +ldap3_proto = ">=0.3.1" log = "*" orion = "0.16" rustls = "0.20" diff --git a/server/src/domain/ldap/group.rs b/server/src/domain/ldap/group.rs index 4555e67..a2cd2a9 100644 --- a/server/src/domain/ldap/group.rs +++ b/server/src/domain/ldap/group.rs @@ -17,7 +17,7 @@ use super::{ }, }; -fn get_group_attribute( +pub fn get_group_attribute( group: &Group, base_dn_str: &str, attribute: &str, @@ -29,8 +29,8 @@ fn get_group_attribute( "objectclass" => vec![b"groupOfUniqueNames".to_vec()], // Always returned as part of the base response. "dn" | "distinguishedname" => return None, - "cn" | "uid" => vec![group.display_name.clone().into_bytes()], - "entryuuid" => vec![group.uuid.to_string().into_bytes()], + "cn" | "uid" | "id" => vec![group.display_name.clone().into_bytes()], + "entryuuid" | "uuid" => vec![group.uuid.to_string().into_bytes()], "member" | "uniquemember" => group .users .iter() @@ -73,6 +73,10 @@ const ALL_GROUP_ATTRIBUTE_KEYS: &[&str] = &[ "entryuuid", ]; +fn expand_group_attribute_wildcards(attributes: &[String]) -> Vec<&str> { + expand_attribute_wildcards(attributes, ALL_GROUP_ATTRIBUTE_KEYS) +} + fn make_ldap_search_group_result_entry( group: Group, base_dn_str: &str, @@ -80,7 +84,7 @@ fn make_ldap_search_group_result_entry( user_filter: &Option<&UserId>, ignored_group_attributes: &[String], ) -> LdapSearchResultEntry { - let expanded_attributes = expand_attribute_wildcards(attributes, ALL_GROUP_ATTRIBUTE_KEYS); + let expanded_attributes = expand_group_attribute_wildcards(attributes); LdapSearchResultEntry { dn: format!("cn={},ou=groups,{}", group.display_name, base_dn_str), @@ -185,11 +189,10 @@ fn convert_group_filter( pub async fn get_groups_list( ldap_info: &LdapInfo, ldap_filter: &LdapFilter, - attributes: &[String], base: &str, user_filter: &Option<&UserId>, backend: &mut Backend, -) -> LdapResult> { +) -> LdapResult> { debug!(?ldap_filter); let filter = convert_group_filter(ldap_info, ldap_filter)?; let parsed_filters = match user_filter { @@ -200,24 +203,28 @@ pub async fn get_groups_list( } }; debug!(?parsed_filters); - let groups = backend + backend .list_groups(Some(parsed_filters)) .await .map_err(|e| LdapError { code: LdapResultCode::Other, message: format!(r#"Error while listing groups "{}": {:#}"#, base, e), - })?; - - Ok(groups - .into_iter() - .map(|u| { - LdapOp::SearchResultEntry(make_ldap_search_group_result_entry( - u, - &ldap_info.base_dn_str, - attributes, - user_filter, - &ldap_info.ignored_group_attributes, - )) }) - .collect::>()) +} + +pub fn convert_groups_to_ldap_op<'a>( + groups: Vec, + attributes: &'a [String], + ldap_info: &'a LdapInfo, + user_filter: &'a Option<&'a UserId>, +) -> impl Iterator + 'a { + groups.into_iter().map(move |g| { + LdapOp::SearchResultEntry(make_ldap_search_group_result_entry( + g, + &ldap_info.base_dn_str, + attributes, + user_filter, + &ldap_info.ignored_group_attributes, + )) + }) } diff --git a/server/src/domain/ldap/user.rs b/server/src/domain/ldap/user.rs index caddb6d..ab27a07 100644 --- a/server/src/domain/ldap/user.rs +++ b/server/src/domain/ldap/user.rs @@ -10,7 +10,7 @@ use crate::domain::{ error::LdapError, utils::{expand_attribute_wildcards, get_user_id_from_distinguished_name}, }, - types::{GroupDetails, User, UserColumn, UserId}, + types::{GroupDetails, User, UserAndGroups, UserColumn, UserId}, }; use super::{ @@ -18,7 +18,7 @@ use super::{ utils::{get_group_id_from_distinguished_name, map_user_field, LdapInfo}, }; -fn get_user_attribute( +pub fn get_user_attribute( user: &User, attribute: &str, base_dn_str: &str, @@ -35,12 +35,12 @@ fn get_user_attribute( ], // dn is always returned as part of the base response. "dn" | "distinguishedname" => return None, - "uid" => vec![user.user_id.to_string().into_bytes()], - "entryuuid" => vec![user.uuid.to_string().into_bytes()], - "mail" => vec![user.email.clone().into_bytes()], - "givenname" => vec![user.first_name.clone()?.into_bytes()], - "sn" => vec![user.last_name.clone()?.into_bytes()], - "jpegphoto" => vec![user.avatar.clone()?.into_bytes()], + "uid" | "user_id" | "id" => vec![user.user_id.to_string().into_bytes()], + "entryuuid" | "uuid" => vec![user.uuid.to_string().into_bytes()], + "mail" | "email" => vec![user.email.clone().into_bytes()], + "givenname" | "first_name" | "firstname" => vec![user.first_name.clone()?.into_bytes()], + "sn" | "last_name" | "lastname" => vec![user.last_name.clone()?.into_bytes()], + "jpegphoto" | "avatar" => vec![user.avatar.clone()?.into_bytes()], "memberof" => groups .into_iter() .flatten() @@ -53,10 +53,12 @@ fn get_user_attribute( }) .collect(), "cn" | "displayname" => vec![user.display_name.clone()?.into_bytes()], - "createtimestamp" | "modifytimestamp" => vec![chrono::Utc - .from_utc_datetime(&user.creation_date) - .to_rfc3339() - .into_bytes()], + "creationdate" | "creation_date" | "createtimestamp" | "modifytimestamp" => { + vec![chrono::Utc + .from_utc_datetime(&user.creation_date) + .to_rfc3339() + .into_bytes()] + } "1.1" => return None, // We ignore the operational attribute wildcard. "+" => return None, @@ -99,15 +101,17 @@ const ALL_USER_ATTRIBUTE_KEYS: &[&str] = &[ fn make_ldap_search_user_result_entry( user: User, base_dn_str: &str, - attributes: &[&str], + attributes: &[String], groups: Option<&[GroupDetails]>, ignored_user_attributes: &[String], ) -> LdapSearchResultEntry { + let expanded_attributes = expand_user_attribute_wildcards(attributes); let dn = format!("uid={},ou=people,{}", user.user_id.as_str(), base_dn_str); + dbg!(&attributes, &expanded_attributes, &user); LdapSearchResultEntry { dn, - attributes: attributes + attributes: expanded_attributes .iter() .filter_map(|a| { let values = @@ -188,15 +192,19 @@ fn convert_user_filter(ldap_info: &LdapInfo, filter: &LdapFilter) -> LdapResult< } } +fn expand_user_attribute_wildcards(attributes: &[String]) -> Vec<&str> { + expand_attribute_wildcards(attributes, ALL_USER_ATTRIBUTE_KEYS) +} + #[instrument(skip_all, level = "debug")] pub async fn get_user_list( ldap_info: &LdapInfo, ldap_filter: &LdapFilter, - attributes: &[String], + request_groups: bool, base: &str, user_filter: &Option<&UserId>, backend: &mut Backend, -) -> LdapResult> { +) -> LdapResult> { debug!(?ldap_filter); let filters = convert_user_filter(ldap_info, ldap_filter)?; let parsed_filters = match user_filter { @@ -207,28 +215,27 @@ pub async fn get_user_list( } }; debug!(?parsed_filters); - let expanded_attributes = expand_attribute_wildcards(attributes, ALL_USER_ATTRIBUTE_KEYS); - let need_groups = expanded_attributes - .iter() - .any(|s| s.to_ascii_lowercase() == "memberof"); - let users = backend - .list_users(Some(parsed_filters), need_groups) + backend + .list_users(Some(parsed_filters), request_groups) .await .map_err(|e| LdapError { code: LdapResultCode::Other, message: format!(r#"Error while searching user "{}": {:#}"#, base, e), - })?; - - Ok(users - .into_iter() - .map(|u| { - LdapOp::SearchResultEntry(make_ldap_search_user_result_entry( - u.user, - &ldap_info.base_dn_str, - &expanded_attributes, - u.groups.as_deref(), - &ldap_info.ignored_user_attributes, - )) }) - .collect::>()) +} + +pub fn convert_users_to_ldap_op<'a>( + users: Vec, + attributes: &'a [String], + ldap_info: &'a LdapInfo, +) -> impl Iterator + 'a { + users.into_iter().map(move |u| { + LdapOp::SearchResultEntry(make_ldap_search_user_result_entry( + u.user, + &ldap_info.base_dn_str, + attributes, + u.groups.as_deref(), + &ldap_info.ignored_user_attributes, + )) + }) } diff --git a/server/src/domain/ldap/utils.rs b/server/src/domain/ldap/utils.rs index e2cbec4..d84c430 100644 --- a/server/src/domain/ldap/utils.rs +++ b/server/src/domain/ldap/utils.rs @@ -143,7 +143,7 @@ pub fn map_user_field(field: &str) -> Option { "cn" | "displayname" | "display_name" => UserColumn::DisplayName, "givenname" | "first_name" => UserColumn::FirstName, "sn" | "last_name" => UserColumn::LastName, - "avatar" => UserColumn::Avatar, + "avatar" | "jpegphoto" => UserColumn::Avatar, "creationdate" | "createtimestamp" | "modifytimestamp" | "creation_date" => { UserColumn::CreationDate } diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index 2c5e6ce..3527900 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -3,23 +3,23 @@ use crate::{ handler::{BackendHandler, BindRequest, CreateUserRequest, LoginHandler}, ldap::{ error::{LdapError, LdapResult}, - group::get_groups_list, - user::get_user_list, + group::{convert_groups_to_ldap_op, get_groups_list}, + user::{convert_users_to_ldap_op, get_user_list}, utils::{ get_user_id_from_distinguished_name, is_subtree, parse_distinguished_name, LdapInfo, }, }, opaque_handler::OpaqueHandler, - types::{JpegPhoto, UserId}, + types::{Group, JpegPhoto, UserAndGroups, UserId}, }, infra::auth_service::{Permission, ValidationResults}, }; use anyhow::Result; use ldap3_proto::proto::{ - LdapAddRequest, LdapBindCred, LdapBindRequest, LdapBindResponse, LdapExtendedRequest, - LdapExtendedResponse, LdapFilter, LdapOp, LdapPartialAttribute, LdapPasswordModifyRequest, - LdapResult as LdapResultOp, LdapResultCode, LdapSearchRequest, LdapSearchResultEntry, - LdapSearchScope, + LdapAddRequest, LdapBindCred, LdapBindRequest, LdapBindResponse, LdapCompareRequest, + LdapDerefAliases, LdapExtendedRequest, LdapExtendedResponse, LdapFilter, LdapOp, + LdapPartialAttribute, LdapPasswordModifyRequest, LdapResult as LdapResultOp, LdapResultCode, + LdapSearchRequest, LdapSearchResultEntry, LdapSearchScope, }; use std::collections::HashMap; use tracing::{debug, instrument, warn}; @@ -71,6 +71,23 @@ fn get_search_scope(base_dn: &[(String, String)], dn_parts: &[(String, String)]) } } +fn make_search_request>( + base: &str, + filter: LdapFilter, + attrs: Vec, +) -> LdapSearchRequest { + LdapSearchRequest { + base: base.to_string(), + scope: LdapSearchScope::Base, + aliases: LdapDerefAliases::Never, + sizelimit: 0, + timelimit: 0, + typesonly: false, + filter, + attrs: attrs.into_iter().map(Into::into).collect(), + } +} + fn make_search_success() -> LdapOp { make_search_error(LdapResultCode::Success, "".to_string()) } @@ -334,6 +351,18 @@ impl LdapHandler LdapResult> { + let user_info = self.user_info.as_ref().ok_or_else(|| LdapError { + code: LdapResultCode::InsufficentAccessRights, + message: "No user currently bound".to_string(), + })?; + Ok(if user_info.is_admin_or_readonly() { + None + } else { + Some(user_info.user.clone()) + }) + } + pub async fn do_search_or_dse( &mut self, request: &LdapSearchRequest, @@ -349,30 +378,19 @@ impl LdapHandler, - ) -> LdapResult> { - let user_filter = user_filter.as_ref(); + user_filter: &Option<&UserId>, + ) -> LdapResult<(Option>, Option>)> { let dn_parts = parse_distinguished_name(&request.base.to_ascii_lowercase())?; let scope = get_search_scope(&self.ldap_info.base_dn, &dn_parts); debug!(?request.base, ?scope); // Disambiguate the lifetimes. - fn cast(x: T) -> T + fn cast<'a, T, R, B: 'a>(x: T) -> T where T: Fn(&'a mut B, &'a LdapFilter) -> R + 'a, { @@ -380,12 +398,16 @@ impl LdapHandler LdapHandler = match scope { - SearchScope::Global => { - let mut results = Vec::new(); - results.extend(get_user_list(&mut self.backend_handler, &request.filter).await?); - results.extend(get_group_list(&mut self.backend_handler, &request.filter).await?); - results - } - SearchScope::Users => get_user_list(&mut self.backend_handler, &request.filter).await?, - SearchScope::Groups => { - get_group_list(&mut self.backend_handler, &request.filter).await? - } + Ok(match scope { + SearchScope::Global => ( + Some(get_user_list(&mut self.backend_handler, &request.filter).await?), + Some(get_group_list(&mut self.backend_handler, &request.filter).await?), + ), + SearchScope::Users => ( + Some(get_user_list(&mut self.backend_handler, &request.filter).await?), + None, + ), + SearchScope::Groups => ( + None, + Some(get_group_list(&mut self.backend_handler, &request.filter).await?), + ), SearchScope::User(filter) => { let filter = LdapFilter::And(vec![request.filter.clone(), filter]); - get_user_list(&mut self.backend_handler, &filter).await? + ( + Some(get_user_list(&mut self.backend_handler, &filter).await?), + None, + ) } SearchScope::Group(filter) => { let filter = LdapFilter::And(vec![request.filter.clone(), filter]); - get_group_list(&mut self.backend_handler, &filter).await? + ( + None, + Some(get_group_list(&mut self.backend_handler, &filter).await?), + ) } SearchScope::Unknown => { warn!( r#"The requested search tree "{}" matches neither the user subtree "ou=people,{}" nor the group subtree "ou=groups,{}""#, &request.base, &self.ldap_info.base_dn_str, &self.ldap_info.base_dn_str ); - Vec::new() + (None, None) } SearchScope::Invalid => { // Search path is not in our tree, just return an empty success. @@ -433,9 +462,33 @@ impl LdapHandler LdapResult> { + let user_filter = self.get_user_permission_filter()?; + let user_filter = user_filter.as_ref(); + let (users, groups) = self.do_search_internal(request, &user_filter).await?; + + let mut results = Vec::new(); + if let Some(users) = users { + results.extend(convert_users_to_ldap_op( + users, + &request.attrs, + &self.ldap_info, + )); + } + if let Some(groups) = groups { + results.extend(convert_groups_to_ldap_op( + groups, + &request.attrs, + &self.ldap_info, + &user_filter, + )); + } if results.is_empty() || matches!(results[results.len() - 1], LdapOp::SearchResultEntry(_)) { results.push(make_search_success()); @@ -527,6 +580,57 @@ impl LdapHandler LdapResult> { + dbg!(&request); + let req = make_search_request::( + &self.ldap_info.base_dn_str, + LdapFilter::Equality("dn".to_string(), request.dn.to_string()), + vec![request.atype.clone()], + ); + let entries = self.do_search(&req).await?; + if entries.len() > 2 { + // SearchResultEntry + SearchResultDone + return Err(LdapError { + code: LdapResultCode::OperationsError, + message: "Too many search results".to_string(), + }); + } + + match entries.first() { + Some(LdapOp::SearchResultEntry(entry)) => { + dbg!(&entry.attributes); + let available = entry + .attributes + .iter() + .any(|attr| attr.atype == request.atype && attr.vals.contains(&request.val)); + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: if available { + LdapResultCode::CompareTrue + } else { + LdapResultCode::CompareFalse + }, + matcheddn: request.dn, + message: "".to_string(), + referral: vec![], + })]) + } + Some(LdapOp::SearchResultDone(_)) => Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::NoSuchObject, + matcheddn: self.ldap_info.base_dn_str.clone(), + message: "".to_string(), + referral: vec![], + })]), + None => Err(LdapError { + code: LdapResultCode::OperationsError, + message: "Search request returned nothing".to_string(), + }), + _ => Err(LdapError { + code: LdapResultCode::OperationsError, + message: "Unexpected results from search".to_string(), + }), + } + } + pub async fn handle_ldap_message(&mut self, ldap_op: LdapOp) -> Option> { Some(match ldap_op { LdapOp::BindRequest(request) => { @@ -555,6 +659,10 @@ impl LdapHandler self + .do_compare(request) + .await + .unwrap_or_else(|e: LdapError| vec![make_search_error(e.code, e.message)]), op => vec![make_extended_response( LdapResultCode::UnwillingToPerform, format!("Unsupported operation: {:#?}", op), @@ -625,23 +733,6 @@ mod tests { } } - fn make_search_request>( - base: &str, - filter: LdapFilter, - attrs: Vec, - ) -> LdapSearchRequest { - LdapSearchRequest { - base: base.to_string(), - scope: LdapSearchScope::Base, - aliases: LdapDerefAliases::Never, - sizelimit: 0, - timelimit: 0, - typesonly: false, - filter, - attrs: attrs.into_iter().map(Into::into).collect(), - } - } - fn make_user_search_request>( filter: LdapFilter, attrs: Vec, @@ -649,6 +740,13 @@ mod tests { make_search_request::("ou=people,Dc=example,dc=com", filter, attrs) } + fn make_group_search_request>( + filter: LdapFilter, + attrs: Vec, + ) -> LdapSearchRequest { + make_search_request::("ou=groups,dc=example,dc=com", filter, attrs) + } + async fn setup_bound_handler_with_group( mut mock: MockTestBackendHandler, group: &str, @@ -778,7 +876,7 @@ mod tests { mock.expect_list_users() .with( eq(Some(UserRequestFilter::And(vec![ - UserRequestFilter::from(true), + true.into(), UserRequestFilter::UserId(UserId::new("test")), ]))), eq(false), @@ -813,7 +911,7 @@ mod tests { async fn test_search_readonly_user() { let mut mock = MockTestBackendHandler::new(); mock.expect_list_users() - .with(eq(Some(UserRequestFilter::from(true))), eq(false)) + .with(eq(Some(true.into())), eq(false)) .times(1) .return_once(|_, _| Ok(vec![])); let mut ldap_handler = setup_bound_readonly_handler(mock).await; @@ -830,7 +928,7 @@ mod tests { async fn test_search_member_of() { let mut mock = MockTestBackendHandler::new(); mock.expect_list_users() - .with(eq(Some(UserRequestFilter::from(true))), eq(true)) + .with(eq(Some(true.into())), eq(true)) .times(1) .return_once(|_, _| { Ok(vec![UserAndGroups { @@ -873,7 +971,7 @@ mod tests { mock.expect_list_users() .with( eq(Some(UserRequestFilter::And(vec![ - UserRequestFilter::from(true), + true.into(), UserRequestFilter::UserId(UserId::new("bob")), ]))), eq(false), @@ -1131,7 +1229,7 @@ mod tests { async fn test_search_groups() { let mut mock = MockTestBackendHandler::new(); mock.expect_list_groups() - .with(eq(Some(GroupRequestFilter::from(true)))) + .with(eq(Some(true.into()))) .times(1) .return_once(|_| { Ok(vec![ @@ -1152,8 +1250,7 @@ mod tests { ]) }); let mut ldap_handler = setup_bound_admin_handler(mock).await; - let request = make_search_request( - "ou=groups,dc=example,dc=cOm", + let request = make_group_search_request( LdapFilter::And(vec![]), vec!["objectClass", "dn", "cn", "uniqueMember", "entryUuid"], ); @@ -1218,12 +1315,13 @@ mod tests { GroupRequestFilter::DisplayName("group_1".to_string()), GroupRequestFilter::Member(UserId::new("bob")), GroupRequestFilter::DisplayName("rockstars".to_string()), - GroupRequestFilter::from(true), - GroupRequestFilter::from(true), - GroupRequestFilter::from(true), - GroupRequestFilter::from(true), - GroupRequestFilter::Not(Box::new(GroupRequestFilter::from(false))), - GroupRequestFilter::from(false), + false.into(), + true.into(), + true.into(), + true.into(), + true.into(), + GroupRequestFilter::Not(Box::new(false.into())), + false.into(), ])))) .times(1) .return_once(|_| { @@ -1236,8 +1334,7 @@ mod tests { }]) }); let mut ldap_handler = setup_bound_admin_handler(mock).await; - let request = make_search_request( - "ou=groups,dc=example,dc=com", + let request = make_group_search_request( LdapFilter::And(vec![ LdapFilter::Equality("cN".to_string(), "Group_1".to_string()), LdapFilter::Equality( @@ -1248,6 +1345,10 @@ mod tests { "dn".to_string(), "uid=rockstars,ou=groups,dc=example,dc=com".to_string(), ), + LdapFilter::Equality( + "dn".to_string(), + "uid=rockstars,ou=people,dc=example,dc=com".to_string(), + ), LdapFilter::Equality("obJEctclass".to_string(), "groupofUniqueNames".to_string()), LdapFilter::Equality("objectclass".to_string(), "groupOfNames".to_string()), LdapFilter::Present("objectclass".to_string()), @@ -1291,8 +1392,7 @@ mod tests { }]) }); let mut ldap_handler = setup_bound_admin_handler(mock).await; - let request = make_search_request( - "ou=groups,dc=example,dc=com", + let request = make_group_search_request( LdapFilter::Or(vec![LdapFilter::Not(Box::new(LdapFilter::Equality( "displayname".to_string(), "group_2".to_string(), @@ -1319,7 +1419,7 @@ mod tests { let mut mock = MockTestBackendHandler::new(); mock.expect_list_groups() .with(eq(Some(GroupRequestFilter::And(vec![ - GroupRequestFilter::from(true), + true.into(), GroupRequestFilter::DisplayName("rockstars".to_string()), ])))) .times(1) @@ -1358,8 +1458,7 @@ mod tests { )) }); let mut ldap_handler = setup_bound_admin_handler(mock).await; - let request = make_search_request( - "ou=groups,dc=example,dc=com", + let request = make_group_search_request( LdapFilter::Or(vec![LdapFilter::Not(Box::new(LdapFilter::Equality( "displayname".to_string(), "group_2".to_string(), @@ -1378,8 +1477,7 @@ mod tests { #[tokio::test] async fn test_search_groups_filter_error() { let mut ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await; - let request = make_search_request( - "ou=groups,dc=example,dc=com", + let request = make_group_search_request( LdapFilter::And(vec![LdapFilter::Substring( "whatever".to_string(), ldap3_proto::proto::LdapSubstringFilter::default(), @@ -1407,12 +1505,13 @@ mod tests { "bob", )))), UserRequestFilter::UserId("bob_1".to_string().into()), - UserRequestFilter::from(true), - UserRequestFilter::from(false), - UserRequestFilter::from(true), - UserRequestFilter::from(true), - UserRequestFilter::from(false), - UserRequestFilter::from(false), + false.into(), + true.into(), + false.into(), + true.into(), + true.into(), + false.into(), + false.into(), ], )]))), eq(false), @@ -1430,6 +1529,10 @@ mod tests { "dn".to_string(), "uid=bob_1,ou=people,dc=example,dc=com".to_string(), ), + LdapFilter::Equality( + "dn".to_string(), + "uid=bob_1,ou=groups,dc=example,dc=com".to_string(), + ), LdapFilter::Equality("objectclass".to_string(), "persOn".to_string()), LdapFilter::Equality("objectclass".to_string(), "other".to_string()), LdapFilter::Present("objectClass".to_string()), @@ -1560,7 +1663,7 @@ mod tests { }]) }); mock.expect_list_groups() - .with(eq(Some(GroupRequestFilter::from(true)))) + .with(eq(Some(true.into()))) .times(1) .return_once(|_| { Ok(vec![Group { @@ -1635,7 +1738,7 @@ mod tests { }]) }); mock.expect_list_groups() - .with(eq(Some(GroupRequestFilter::from(true)))) + .with(eq(Some(true.into()))) .returning(|_| { Ok(vec![Group { id: GroupId(1), @@ -2091,7 +2194,7 @@ mod tests { async fn test_search_filter_non_attribute() { let mut mock = MockTestBackendHandler::new(); mock.expect_list_users() - .with(eq(Some(UserRequestFilter::from(true))), eq(false)) + .with(eq(Some(true.into())), eq(false)) .times(1) .return_once(|_, _| Ok(vec![])); let mut ldap_handler = setup_bound_admin_handler(mock).await; @@ -2104,4 +2207,146 @@ mod tests { Ok(vec![make_search_success()]) ); } + + #[tokio::test] + async fn test_compare_user() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users().returning(|f, g| { + assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob")))); + assert!(!g); + Ok(vec![UserAndGroups { + user: User { + user_id: UserId::new("bob"), + email: "bob@bobmail.bob".to_string(), + ..Default::default() + }, + groups: None, + }]) + }); + mock.expect_list_groups().returning(|_| Ok(vec![])); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let dn = "uid=bob,ou=people,dc=example,dc=com"; + let request = LdapCompareRequest { + dn: dn.to_string(), + atype: "uid".to_owned(), + val: b"bob".to_vec(), + }; + assert_eq!( + ldap_handler.do_compare(request).await, + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::CompareTrue, + matcheddn: dn.to_string(), + message: "".to_string(), + referral: vec![], + })]) + ); + // Non-canonical attribute. + let request = LdapCompareRequest { + dn: dn.to_string(), + atype: "eMail".to_owned(), + val: b"bob@bobmail.bob".to_vec(), + }; + assert_eq!( + ldap_handler.do_compare(request).await, + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::CompareTrue, + matcheddn: dn.to_string(), + message: "".to_string(), + referral: vec![], + })]) + ); + } + + #[tokio::test] + async fn test_compare_group() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users().returning(|_, _| Ok(vec![])); + mock.expect_list_groups().returning(|f| { + assert_eq!(f, Some(GroupRequestFilter::DisplayName("group".to_owned()))); + Ok(vec![Group { + id: GroupId(1), + display_name: "group".to_string(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + users: vec![UserId::new("bob")], + uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), + }]) + }); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let dn = "uid=group,ou=groups,dc=example,dc=com"; + let request = LdapCompareRequest { + dn: dn.to_string(), + atype: "uid".to_owned(), + val: b"group".to_vec(), + }; + assert_eq!( + ldap_handler.do_compare(request).await, + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::CompareTrue, + matcheddn: dn.to_string(), + message: "".to_string(), + referral: vec![], + })]) + ); + } + + #[tokio::test] + async fn test_compare_not_found() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users().returning(|f, g| { + assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob")))); + assert!(!g); + Ok(vec![]) + }); + mock.expect_list_groups().returning(|_| Ok(vec![])); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let dn = "uid=bob,ou=people,dc=example,dc=com"; + let request = LdapCompareRequest { + dn: dn.to_string(), + atype: "uid".to_owned(), + val: b"bob".to_vec(), + }; + assert_eq!( + ldap_handler.do_compare(request).await, + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::NoSuchObject, + matcheddn: "dc=example,dc=com".to_owned(), + message: "".to_string(), + referral: vec![], + })]) + ); + } + + #[tokio::test] + async fn test_compare_no_match() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users().returning(|f, g| { + assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob")))); + assert!(!g); + Ok(vec![UserAndGroups { + user: User { + user_id: UserId::new("bob"), + email: "bob@bobmail.bob".to_string(), + ..Default::default() + }, + groups: None, + }]) + }); + mock.expect_list_groups().returning(|_| Ok(vec![])); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let dn = "uid=bob,ou=people,dc=example,dc=com"; + let request = LdapCompareRequest { + dn: dn.to_string(), + atype: "mail".to_owned(), + val: b"bob@bob".to_vec(), + }; + assert_eq!( + ldap_handler.do_compare(request).await, + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::CompareFalse, + matcheddn: dn.to_string(), + message: "".to_string(), + referral: vec![], + })]) + ); + } } From 81036943c22d49c26e0fd3d4aceefe3e727ef8c2 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Mon, 13 Feb 2023 15:41:07 +0100 Subject: [PATCH 28/62] server: Add support for SubString ldap filter --- server/src/domain/handler.rs | 34 ++++++++ server/src/domain/ldap/group.rs | 15 ++++ server/src/domain/ldap/user.rs | 22 +++++ server/src/domain/ldap/utils.rs | 23 +++++- .../src/domain/sql_group_backend_handler.rs | 29 ++++++- server/src/domain/sql_user_backend_handler.rs | 35 +++++++- server/src/infra/ldap_handler.rs | 80 ++++++++++++++++--- 7 files changed, 219 insertions(+), 19 deletions(-) diff --git a/server/src/domain/handler.rs b/server/src/domain/handler.rs index d93657d..1b2cb4f 100644 --- a/server/src/domain/handler.rs +++ b/server/src/domain/handler.rs @@ -14,13 +14,46 @@ pub struct BindRequest { pub password: String, } +#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)] +pub struct SubStringFilter { + pub initial: Option, + pub any: Vec, + pub final_: Option, +} + +impl SubStringFilter { + pub fn to_sql_filter(&self) -> String { + let mut filter = String::with_capacity( + self.initial.as_ref().map(String::len).unwrap_or_default() + + 1 + + self.any.iter().map(String::len).sum::() + + self.any.len() + + self.final_.as_ref().map(String::len).unwrap_or_default(), + ); + if let Some(f) = &self.initial { + filter.push_str(&f.to_ascii_lowercase()); + } + filter.push('%'); + for part in self.any.iter() { + filter.push_str(&part.to_ascii_lowercase()); + filter.push('%'); + } + if let Some(f) = &self.final_ { + filter.push_str(&f.to_ascii_lowercase()); + } + filter + } +} + #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)] pub enum UserRequestFilter { And(Vec), Or(Vec), Not(Box), UserId(UserId), + UserIdSubString(SubStringFilter), Equality(UserColumn, String), + SubString(UserColumn, SubStringFilter), // Check if a user belongs to a group identified by name. MemberOf(String), // Same, by id. @@ -43,6 +76,7 @@ pub enum GroupRequestFilter { Or(Vec), Not(Box), DisplayName(String), + DisplayNameSubString(SubStringFilter), Uuid(Uuid), GroupId(GroupId), // Check if the group contains a user identified by uid. diff --git a/server/src/domain/ldap/group.rs b/server/src/domain/ldap/group.rs index a2cd2a9..00bde72 100644 --- a/server/src/domain/ldap/group.rs +++ b/server/src/domain/ldap/group.rs @@ -178,6 +178,21 @@ fn convert_group_filter( || map_group_field(field).is_some(), )) } + LdapFilter::Substring(field, substring_filter) => { + let field = &field.to_ascii_lowercase(); + match map_group_field(field.as_str()) { + Some(GroupColumn::DisplayName) => Ok(GroupRequestFilter::DisplayNameSubString( + substring_filter.clone().into(), + )), + _ => Err(LdapError { + code: LdapResultCode::UnwillingToPerform, + message: format!( + "Unsupported group attribute for substring filter: {:?}", + field + ), + }), + } + } _ => Err(LdapError { code: LdapResultCode::UnwillingToPerform, message: format!("Unsupported group filter: {:?}", filter), diff --git a/server/src/domain/ldap/user.rs b/server/src/domain/ldap/user.rs index ab27a07..d84a065 100644 --- a/server/src/domain/ldap/user.rs +++ b/server/src/domain/ldap/user.rs @@ -185,6 +185,28 @@ fn convert_user_filter(ldap_info: &LdapInfo, filter: &LdapFilter) -> LdapResult< || map_user_field(field).is_some(), )) } + LdapFilter::Substring(field, substring_filter) => { + let field = &field.to_ascii_lowercase(); + match map_user_field(field.as_str()) { + Some(UserColumn::UserId) => Ok(UserRequestFilter::UserIdSubString( + substring_filter.clone().into(), + )), + None + | Some(UserColumn::CreationDate) + | Some(UserColumn::Avatar) + | Some(UserColumn::Uuid) => Err(LdapError { + code: LdapResultCode::UnwillingToPerform, + message: format!( + "Unsupported user attribute for substring filter: {:?}", + field + ), + }), + Some(field) => Ok(UserRequestFilter::SubString( + field, + substring_filter.clone().into(), + )), + } + } _ => Err(LdapError { code: LdapResultCode::UnwillingToPerform, message: format!("Unsupported user filter: {:?}", filter), diff --git a/server/src/domain/ldap/utils.rs b/server/src/domain/ldap/utils.rs index d84c430..852d6e6 100644 --- a/server/src/domain/ldap/utils.rs +++ b/server/src/domain/ldap/utils.rs @@ -1,12 +1,29 @@ use itertools::Itertools; -use ldap3_proto::LdapResultCode; +use ldap3_proto::{proto::LdapSubstringFilter, LdapResultCode}; use tracing::{debug, instrument, warn}; use crate::domain::{ + handler::SubStringFilter, ldap::error::{LdapError, LdapResult}, types::{GroupColumn, UserColumn, UserId}, }; +impl From for SubStringFilter { + fn from( + LdapSubstringFilter { + initial, + any, + final_, + }: LdapSubstringFilter, + ) -> Self { + Self { + initial, + any, + final_, + } + } +} + fn make_dn_pair(mut iter: I) -> LdapResult<(String, String)> where I: Iterator, @@ -141,8 +158,8 @@ pub fn map_user_field(field: &str) -> Option { "uid" | "user_id" | "id" => UserColumn::UserId, "mail" | "email" => UserColumn::Email, "cn" | "displayname" | "display_name" => UserColumn::DisplayName, - "givenname" | "first_name" => UserColumn::FirstName, - "sn" | "last_name" => UserColumn::LastName, + "givenname" | "first_name" | "firstname" => UserColumn::FirstName, + "sn" | "last_name" | "lastname" => UserColumn::LastName, "avatar" | "jpegphoto" => UserColumn::Avatar, "creationdate" | "createtimestamp" | "modifytimestamp" | "creation_date" => { UserColumn::CreationDate diff --git a/server/src/domain/sql_group_backend_handler.rs b/server/src/domain/sql_group_backend_handler.rs index e5677a4..afffb5c 100644 --- a/server/src/domain/sql_group_backend_handler.rs +++ b/server/src/domain/sql_group_backend_handler.rs @@ -7,7 +7,7 @@ use crate::domain::{ }; use async_trait::async_trait; use sea_orm::{ - sea_query::{Cond, IntoCondition, SimpleExpr}, + sea_query::{Alias, Cond, Expr, Func, IntoCondition, SimpleExpr}, ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, QueryTrait, }; @@ -15,6 +15,7 @@ use tracing::{debug, instrument}; fn get_group_filter_expr(filter: GroupRequestFilter) -> Cond { use GroupRequestFilter::*; + let group_table = Alias::new("groups"); match filter { And(fs) => { if fs.is_empty() { @@ -46,6 +47,12 @@ fn get_group_filter_expr(filter: GroupRequestFilter) -> Cond { .into_query(), ) .into_condition(), + DisplayNameSubString(filter) => SimpleExpr::FunctionCall(Func::lower(Expr::col(( + group_table, + GroupColumn::DisplayName, + )))) + .like(filter.to_sql_filter()) + .into_condition(), } } @@ -146,7 +153,7 @@ impl GroupBackendHandler for SqlBackendHandler { #[cfg(test)] mod tests { use super::*; - use crate::domain::{sql_backend_handler::tests::*, types::UserId}; + use crate::domain::{handler::SubStringFilter, sql_backend_handler::tests::*, types::UserId}; async fn get_group_ids( handler: &SqlBackendHandler, @@ -221,6 +228,24 @@ mod tests { ); } + #[tokio::test] + async fn test_list_groups_substring_filter() { + let fixture = TestFixture::new().await; + assert_eq!( + get_group_ids( + &fixture.handler, + Some(GroupRequestFilter::DisplayNameSubString(SubStringFilter { + initial: Some("be".to_owned()), + any: vec!["sT".to_owned()], + final_: Some("P".to_owned()), + })), + ) + .await, + // Best group + vec![fixture.groups[0]] + ); + } + #[tokio::test] async fn test_get_group_details() { let fixture = TestFixture::new().await; diff --git a/server/src/domain/sql_user_backend_handler.rs b/server/src/domain/sql_user_backend_handler.rs index a481565..529c3e9 100644 --- a/server/src/domain/sql_user_backend_handler.rs +++ b/server/src/domain/sql_user_backend_handler.rs @@ -8,7 +8,7 @@ use super::{ use async_trait::async_trait; use sea_orm::{ entity::IntoActiveValue, - sea_query::{Alias, Cond, Expr, IntoColumnRef, IntoCondition, SimpleExpr}, + sea_query::{Alias, Cond, Expr, Func, IntoColumnRef, IntoCondition, SimpleExpr}, ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, ModelTrait, QueryFilter, QueryOrder, QuerySelect, QueryTrait, Set, }; @@ -49,8 +49,15 @@ fn get_user_filter_expr(filter: UserRequestFilter) -> Cond { MemberOfId(group_id) => Expr::col((group_table, GroupColumn::GroupId)) .eq(group_id) .into_condition(), + UserIdSubString(filter) => UserColumn::UserId + .like(&filter.to_sql_filter()) + .into_condition(), + SubString(col, filter) => SimpleExpr::FunctionCall(Func::lower(Expr::col(col))) + .like(filter.to_sql_filter()) + .into_condition(), } } + fn to_value(opt_name: &Option) -> ActiveValue> { match opt_name { None => ActiveValue::NotSet, @@ -236,6 +243,7 @@ impl UserBackendHandler for SqlBackendHandler { mod tests { use super::*; use crate::domain::{ + handler::SubStringFilter, sql_backend_handler::tests::*, types::{JpegPhoto, UserColumn}, }; @@ -286,6 +294,31 @@ mod tests { assert_eq!(users, vec!["bob"]); } + #[tokio::test] + async fn test_list_users_substring_filter() { + let fixture = TestFixture::new().await; + let users = get_user_names( + &fixture.handler, + Some(UserRequestFilter::And(vec![ + UserRequestFilter::UserIdSubString(SubStringFilter { + initial: Some("Pa".to_owned()), + any: vec!["rI".to_owned()], + final_: Some("K".to_owned()), + }), + UserRequestFilter::SubString( + UserColumn::FirstName, + SubStringFilter { + initial: None, + any: vec!["r".to_owned(), "t".to_owned()], + final_: None, + }, + ), + ])), + ) + .await; + assert_eq!(users, vec!["patrick"]); + } + #[tokio::test] async fn test_list_users_false_filter() { let fixture = TestFixture::new().await; diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index 3527900..b6959d3 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -680,7 +680,7 @@ mod tests { }; use async_trait::async_trait; use chrono::TimeZone; - use ldap3_proto::proto::{LdapDerefAliases, LdapSearchScope}; + use ldap3_proto::proto::{LdapDerefAliases, LdapSearchScope, LdapSubstringFilter}; use mockall::predicate::eq; use std::collections::HashSet; use tokio; @@ -1322,6 +1322,11 @@ mod tests { true.into(), GroupRequestFilter::Not(Box::new(false.into())), false.into(), + GroupRequestFilter::DisplayNameSubString(SubStringFilter { + initial: Some("iNIt".to_owned()), + any: vec!["1".to_owned(), "2aA".to_owned()], + final_: Some("finAl".to_owned()), + }), ])))) .times(1) .return_once(|_| { @@ -1357,6 +1362,14 @@ mod tests { "random_attribUte".to_string(), ))), LdapFilter::Equality("unknown_attribute".to_string(), "randomValue".to_string()), + LdapFilter::Substring( + "cn".to_owned(), + LdapSubstringFilter { + initial: Some("iNIt".to_owned()), + any: vec!["1".to_owned(), "2aA".to_owned()], + final_: Some("finAl".to_owned()), + }, + ), ]), vec!["1.1"], ); @@ -1442,6 +1455,22 @@ mod tests { ); } + #[tokio::test] + async fn test_search_groups_unsupported_substring() { + let mut ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await; + let request = make_group_search_request( + LdapFilter::Substring("member".to_owned(), LdapSubstringFilter::default()), + vec!["cn"], + ); + assert_eq!( + ldap_handler.do_search_or_dse(&request).await, + Err(LdapError { + code: LdapResultCode::UnwillingToPerform, + message: r#"Unsupported group attribute for substring filter: "member""#.to_owned() + }) + ); + } + #[tokio::test] async fn test_search_groups_error() { let mut mock = MockTestBackendHandler::new(); @@ -1478,18 +1507,17 @@ mod tests { async fn test_search_groups_filter_error() { let mut ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await; let request = make_group_search_request( - LdapFilter::And(vec![LdapFilter::Substring( - "whatever".to_string(), - ldap3_proto::proto::LdapSubstringFilter::default(), + LdapFilter::And(vec![LdapFilter::Approx( + "whatever".to_owned(), + "value".to_owned(), )]), vec!["cn"], ); assert_eq!( ldap_handler.do_search_or_dse(&request).await, - Err(LdapError{ + Err(LdapError { code: LdapResultCode::UnwillingToPerform, - message: r#"Unsupported group filter: Substring("whatever", LdapSubstringFilter { initial: None, any: [], final_: None })"# - .to_string() + message: r#"Unsupported group filter: Approx("whatever", "value")"#.to_string() }) ); } @@ -1512,6 +1540,19 @@ mod tests { true.into(), false.into(), false.into(), + UserRequestFilter::UserIdSubString(SubStringFilter { + initial: Some("iNIt".to_owned()), + any: vec!["1".to_owned(), "2aA".to_owned()], + final_: Some("finAl".to_owned()), + }), + UserRequestFilter::SubString( + UserColumn::FirstName, + SubStringFilter { + initial: Some("iNIt".to_owned()), + any: vec!["1".to_owned(), "2aA".to_owned()], + final_: Some("finAl".to_owned()), + }, + ), ], )]))), eq(false), @@ -1539,6 +1580,22 @@ mod tests { LdapFilter::Present("uid".to_string()), LdapFilter::Present("unknown".to_string()), LdapFilter::Equality("unknown_attribute".to_string(), "randomValue".to_string()), + LdapFilter::Substring( + "uid".to_owned(), + LdapSubstringFilter { + initial: Some("iNIt".to_owned()), + any: vec!["1".to_owned(), "2aA".to_owned()], + final_: Some("finAl".to_owned()), + }, + ), + LdapFilter::Substring( + "firstName".to_owned(), + LdapSubstringFilter { + initial: Some("iNIt".to_owned()), + any: vec!["1".to_owned(), "2aA".to_owned()], + final_: Some("finAl".to_owned()), + }, + ), ])]), vec!["objectClass"], ); @@ -1910,17 +1967,14 @@ mod tests { async fn test_search_unsupported_filters() { let mut ldap_handler = setup_bound_admin_handler(MockTestBackendHandler::new()).await; let request = make_user_search_request( - LdapFilter::Substring( - "uid".to_string(), - ldap3_proto::proto::LdapSubstringFilter::default(), - ), + LdapFilter::Approx("uid".to_owned(), "value".to_owned()), vec!["objectClass"], ); assert_eq!( ldap_handler.do_search_or_dse(&request).await, - Err(LdapError{ + Err(LdapError { code: LdapResultCode::UnwillingToPerform, - message: r#"Unsupported user filter: Substring("uid", LdapSubstringFilter { initial: None, any: [], final_: None })"#.to_string() + message: r#"Unsupported user filter: Approx("uid", "value")"#.to_string() }) ); } From 1ce239103cb936d0a59ec5dee535a1e5ff28953f Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Mon, 13 Feb 2023 15:46:48 +0100 Subject: [PATCH 29/62] server: removed dbg --- server/src/domain/ldap/user.rs | 2 -- server/src/infra/ldap_handler.rs | 2 -- 2 files changed, 4 deletions(-) diff --git a/server/src/domain/ldap/user.rs b/server/src/domain/ldap/user.rs index d84a065..603bb05 100644 --- a/server/src/domain/ldap/user.rs +++ b/server/src/domain/ldap/user.rs @@ -107,8 +107,6 @@ fn make_ldap_search_user_result_entry( ) -> LdapSearchResultEntry { let expanded_attributes = expand_user_attribute_wildcards(attributes); let dn = format!("uid={},ou=people,{}", user.user_id.as_str(), base_dn_str); - dbg!(&attributes, &expanded_attributes, &user); - LdapSearchResultEntry { dn, attributes: expanded_attributes diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index b6959d3..9198f2c 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -581,7 +581,6 @@ impl LdapHandler LdapResult> { - dbg!(&request); let req = make_search_request::( &self.ldap_info.base_dn_str, LdapFilter::Equality("dn".to_string(), request.dn.to_string()), @@ -598,7 +597,6 @@ impl LdapHandler { - dbg!(&entry.attributes); let available = entry .attributes .iter() From ea498df78bc01941e75b0784051513b2ed3e5e4d Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Mon, 13 Feb 2023 16:32:26 +0100 Subject: [PATCH 30/62] server: add a test for compare with uniqueMember --- server/src/infra/ldap_handler.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index 9198f2c..e24bcf0 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -2401,4 +2401,36 @@ mod tests { })]) ); } + + #[tokio::test] + async fn test_compare_group_member() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users().returning(|_, _| Ok(vec![])); + mock.expect_list_groups().returning(|f| { + assert_eq!(f, Some(GroupRequestFilter::DisplayName("group".to_owned()))); + Ok(vec![Group { + id: GroupId(1), + display_name: "group".to_string(), + creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(), + users: vec![UserId::new("bob")], + uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"), + }]) + }); + let mut ldap_handler = setup_bound_admin_handler(mock).await; + let dn = "uid=group,ou=groups,dc=example,dc=com"; + let request = LdapCompareRequest { + dn: dn.to_string(), + atype: "uniqueMember".to_owned(), + val: b"uid=bob,ou=people,dc=example,dc=com".to_vec(), + }; + assert_eq!( + ldap_handler.do_compare(request).await, + Ok(vec![LdapOp::CompareResult(LdapResultOp { + code: LdapResultCode::CompareTrue, + matcheddn: dn.to_owned(), + message: "".to_string(), + referral: vec![], + })]) + ); + } } From 562ad524c42c3f8d5c0db0c50ac58f81e568d5d8 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Mon, 13 Feb 2023 18:56:39 +0100 Subject: [PATCH 31/62] server: only add password reset routes if they are enabled --- server/src/infra/auth_service.rs | 20 +++++++++++--------- server/src/infra/tcp_server.rs | 6 +++++- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/server/src/infra/auth_service.rs b/server/src/infra/auth_service.rs index e8f8c7e..ce7bc1b 100644 --- a/server/src/infra/auth_service.rs +++ b/server/src/infra/auth_service.rs @@ -677,7 +677,7 @@ pub(crate) fn check_if_token_is_valid( }) } -pub fn configure_server(cfg: &mut web::ServiceConfig) +pub fn configure_server(cfg: &mut web::ServiceConfig, enable_password_reset: bool) where Backend: TcpBackendHandler + LoginHandler + OpaqueHandler + BackendHandler + 'static, { @@ -694,14 +694,6 @@ where web::resource("/simple/login").route(web::post().to(simple_login_handler::)), ) .service(web::resource("/refresh").route(web::get().to(get_refresh_handler::))) - .service( - web::resource("/reset/step1/{user_id}") - .route(web::get().to(get_password_reset_step1_handler::)), - ) - .service( - web::resource("/reset/step2/{token}") - .route(web::get().to(get_password_reset_step2_handler::)), - ) .service(web::resource("/logout").route(web::get().to(get_logout_handler::))) .service( web::scope("/opaque/register") @@ -715,4 +707,14 @@ where .route(web::post().to(opaque_register_finish_handler::)), ), ); + if enable_password_reset { + cfg.service( + web::resource("/reset/step1/{user_id}") + .route(web::get().to(get_password_reset_step1_handler::)), + ) + .service( + web::resource("/reset/step2/{token}") + .route(web::get().to(get_password_reset_step2_handler::)), + ); + } } diff --git a/server/src/infra/tcp_server.rs b/server/src/infra/tcp_server.rs index 27a751e..c27846d 100644 --- a/server/src/infra/tcp_server.rs +++ b/server/src/infra/tcp_server.rs @@ -73,6 +73,7 @@ fn http_config( ) where Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Sync + 'static, { + let enable_password_reset = mail_options.enable_password_reset; cfg.app_data(web::Data::new(AppState:: { backend_handler, jwt_key: Hmac::new_varkey(jwt_secret.unsecure().as_bytes()).unwrap(), @@ -81,7 +82,10 @@ fn http_config( mail_options, })) .route("/health", web::get().to(|| HttpResponse::Ok().finish())) - .service(web::scope("/auth").configure(auth_service::configure_server::)) + .service( + web::scope("/auth") + .configure(|cfg| auth_service::configure_server::(cfg, enable_password_reset)), + ) // API endpoint. .service( web::scope("/api") From 62104b417a0da702291a95c7cbe20447317557bf Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Mon, 13 Feb 2023 20:09:24 +0100 Subject: [PATCH 32/62] app: probe for password reset support --- app/src/components/app.rs | 66 +++++++++++++++++++++++++++++++------ app/src/components/login.rs | 20 +++++++---- app/src/infra/api.rs | 21 ++++++++++-- 3 files changed, 88 insertions(+), 19 deletions(-) diff --git a/app/src/components/app.rs b/app/src/components/app.rs index 7f35c43..a41346f 100644 --- a/app/src/components/app.rs +++ b/app/src/components/app.rs @@ -13,10 +13,13 @@ use crate::{ user_details::UserDetails, user_table::UserTable, }, - infra::cookies::get_cookie, + infra::{api::HostService, cookies::get_cookie}, +}; + +use yew::{ + prelude::*, + services::{fetch::FetchTask, ConsoleService}, }; -use yew::prelude::*; -use yew::services::ConsoleService; use yew_router::{ agent::{RouteAgentDispatcher, RouteRequest}, route::Route, @@ -29,11 +32,14 @@ pub struct App { user_info: Option<(String, bool)>, redirect_to: Option, route_dispatcher: RouteAgentDispatcher, + password_reset_enabled: bool, + task: Option, } pub enum Msg { Login((String, bool)), Logout, + PasswordResetProbeFinished(anyhow::Result), } impl Component for App { @@ -58,7 +64,15 @@ impl Component for App { }), redirect_to: Self::get_redirect_route(), route_dispatcher: RouteAgentDispatcher::new(), + password_reset_enabled: false, + task: None, }; + app.task = Some( + HostService::probe_password_reset( + app.link.callback_once(Msg::PasswordResetProbeFinished), + ) + .unwrap(), + ); app.apply_initial_redirections(); app } @@ -82,6 +96,16 @@ impl Component for App { self.user_info = None; self.redirect_to = None; } + Msg::PasswordResetProbeFinished(Ok(enabled)) => { + self.task = None; + self.password_reset_enabled = enabled; + } + Msg::PasswordResetProbeFinished(Err(err)) => { + self.task = None; + ConsoleService::error(&format!( + "Could not probe for password reset support: {err:#}" + )); + } } if self.user_info.is_none() { self.route_dispatcher @@ -97,6 +121,7 @@ impl Component for App { fn view(&self) -> Html { let link = self.link.clone(); let is_admin = self.is_admin(); + let password_reset_enabled = self.password_reset_enabled; html! {
{self.view_banner()} @@ -104,7 +129,7 @@ impl Component for App {
- render = Router::render(move |s| Self::dispatch_route(s, &link, is_admin)) + render = Router::render(move |s| Self::dispatch_route(s, &link, is_admin, password_reset_enabled)) />
@@ -135,6 +160,10 @@ impl App { let route_service = RouteService::<()>::new(); let current_route = route_service.get_path(); if current_route.contains("reset-password") { + if !self.password_reset_enabled { + self.route_dispatcher + .send(RouteRequest::ReplaceRoute(Route::from(AppRoute::Login))); + } return; } match &self.user_info { @@ -162,10 +191,15 @@ impl App { } } - fn dispatch_route(switch: AppRoute, link: &ComponentLink, is_admin: bool) -> Html { + fn dispatch_route( + switch: AppRoute, + link: &ComponentLink, + is_admin: bool, + password_reset_enabled: bool, + ) -> Html { match switch { AppRoute::Login => html! { - + }, AppRoute::CreateUser => html! { @@ -200,11 +234,23 @@ impl App { AppRoute::ChangePassword(username) => html! { }, - AppRoute::StartResetPassword => html! { - - }, + AppRoute::StartResetPassword => { + if password_reset_enabled { + html! { + + } + } else { + App::dispatch_route(AppRoute::Login, link, is_admin, password_reset_enabled) + } + } AppRoute::FinishResetPassword(token) => html! { - + if password_reset_enabled { + html! { + + } + } else { + App::dispatch_route(AppRoute::Login, link, is_admin, password_reset_enabled) + } }, } } diff --git a/app/src/components/login.rs b/app/src/components/login.rs index 0503e95..06adc6e 100644 --- a/app/src/components/login.rs +++ b/app/src/components/login.rs @@ -30,6 +30,7 @@ pub struct FormModel { #[derive(Clone, PartialEq, Properties)] pub struct Props { pub on_logged_in: Callback<(String, bool)>, + pub password_reset_enabled: bool, } pub enum Msg { @@ -147,6 +148,7 @@ impl Component for LoginForm { fn view(&self) -> Html { type Field = yew_form::Field; + let password_reset_enabled = self.common.password_reset_enabled; if self.refreshing { html! {
@@ -198,12 +200,18 @@ impl Component for LoginForm { {"Login"} - - {"Forgot your password?"} - + { if password_reset_enabled { + html! { + + {"Forgot your password?"} + + } + } else { + html!{} + }}
{ if let Some(e) = &self.common.error { diff --git a/app/src/infra/api.rs b/app/src/infra/api.rs index a210b9a..49e1b0e 100644 --- a/app/src/infra/api.rs +++ b/app/src/infra/api.rs @@ -3,9 +3,11 @@ use anyhow::{anyhow, Context, Result}; use graphql_client::GraphQLQuery; use lldap_auth::{login, registration, JWTClaims}; -use yew::callback::Callback; -use yew::format::Json; -use yew::services::fetch::{Credentials, FetchOptions, FetchService, FetchTask, Request, Response}; +use yew::{ + callback::Callback, + format::Json, + services::fetch::{Credentials, FetchOptions, FetchService, FetchTask, Request, Response}, +}; #[derive(Default)] pub struct HostService {} @@ -286,4 +288,17 @@ impl HostService { "Could not validate token", ) } + + pub fn probe_password_reset(callback: Callback>) -> Result { + let request = Request::get("/auth/reset/step1/lldap_unlikely_very_long_user_name") + .header("Content-Type", "application/json") + .body(yew::format::Nothing)?; + FetchService::fetch_with_options( + request, + get_default_options(), + create_handler(callback, move |status: http::StatusCode, _data: String| { + Ok(status != http::StatusCode::NOT_FOUND) + }), + ) + } } From 672dd96e7eab108c0f79baec9bf2f62ee770e9c7 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Tue, 14 Feb 2023 11:13:10 +0100 Subject: [PATCH 33/62] server: add content-type header to the email --- server/src/infra/mail.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/infra/mail.rs b/server/src/infra/mail.rs index c67614b..8105d31 100644 --- a/server/src/infra/mail.rs +++ b/server/src/infra/mail.rs @@ -21,7 +21,11 @@ async fn send_email(to: Mailbox, subject: &str, body: String, options: &MailOpti .reply_to(reply_to) .to(to) .subject(subject) - .body(body)?; + .singlepart( + lettre::message::SinglePart::builder() + .header(lettre::message::header::ContentType::TEXT_PLAIN) + .body(body), + )?; let creds = Credentials::new( options.user.clone(), options.password.unsecure().to_string(), From 5bee73180d8bdba6beec38301812f6d62fb07f37 Mon Sep 17 00:00:00 2001 From: arcoast <81871508+arcoast@users.noreply.github.com> Date: Tue, 14 Feb 2023 17:22:49 +0000 Subject: [PATCH 34/62] example_configs: add authentik configuration This should import users, groups & memberships --- README.md | 1 + example_configs/authentik.md | 105 +++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 example_configs/authentik.md diff --git a/README.md b/README.md index ecf4c71..463ea89 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,7 @@ folder for help with: - [Airsonic Advanced](example_configs/airsonic-advanced.md) - [Apache Guacamole](example_configs/apacheguacamole.md) - [Authelia](example_configs/authelia_config.yml) +- [Authentik](example_configs/authentik.md) - [Bookstack](example_configs/bookstack.env.example) - [Calibre-Web](example_configs/calibre_web.md) - [Dell iDRAC](example_configs/dell_idrac.md) diff --git a/example_configs/authentik.md b/example_configs/authentik.md new file mode 100644 index 0000000..1b4f73a --- /dev/null +++ b/example_configs/authentik.md @@ -0,0 +1,105 @@ +# Name +``` +lldap +``` + +# Slug +``` +lldap +``` +- [x] Enabled +- [x] Sync Users +- [x] User password writeback +- [x] Sync groups + +# Connection settings + +## Server URI +``` +ldap://lldap:3890 +``` + +- [ ] Enable StartTLS + +## TLS Verification Certificate +``` +--------- +``` + +## Bind CN +``` +uid=admin,ou=people,dc=example,dc=com +``` + +## Bind Password +``` +ADMIN_PASSWORD +``` + +## Base DN +``` +dc=example,dc=com +``` + +# LDAP Attribute mapping +## User Property Mappings +- [x] authentik default LDAP Mapping: mail +- [x] authentik default LDAP Mapping: Name +- [x] authentik default Active Directory Mapping: givenName +- [ ] authentik default Active Directory Mapping: sAMAccountName +- [x] authentik default Active Directory Mapping: sn +- [ ] authentik default Active Directory Mapping: userPrincipalName +- [x] authentik default OpenLDAP Mapping: cn +- [x] authentik default OpenLDAP Mapping: uid + +## Group Property Mappings +- [ ] authentik default LDAP Mapping: mail +- [ ] authentik default LDAP Mapping: Name +- [ ] authentik default Active Directory Mapping: givenName +- [ ] authentik default Active Directory Mapping: sAMAccountName +- [ ] authentik default Active Directory Mapping: sn +- [ ] authentik default Active Directory Mapping: userPrincipalName +- [x] authentik default OpenLDAP Mapping: cn +- [ ] authentik default OpenLDAP Mapping: uid + +# Additional settings + +## Group +``` +--------- +``` + +## User path +``` +LDAP/users +``` + +## Addition User DN +``` +ou=people +``` + +## Addition Group DN +``` +ou=groups +``` + +## User object filter +``` +(objectClass=person) +``` + +## Group object filter +``` +(objectClass=groupOfUniqueNames) +``` + +## Group membership field +``` +member +``` + +## Object uniqueness field +``` +uid +``` From 3650a438dfa5df43aa1af15ce615e3d89f1bc6e9 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Wed, 15 Feb 2023 09:51:47 +0100 Subject: [PATCH 35/62] docker: fix healthcheck --- .github/workflows/Dockerfile.ci.alpine | 2 +- .github/workflows/Dockerfile.ci.debian | 2 +- Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/Dockerfile.ci.alpine b/.github/workflows/Dockerfile.ci.alpine index 5318fd8..504a2e4 100644 --- a/.github/workflows/Dockerfile.ci.alpine +++ b/.github/workflows/Dockerfile.ci.alpine @@ -104,4 +104,4 @@ VOLUME ["/data"] WORKDIR /app ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"] CMD ["run", "--config-file", "/data/lldap_config.toml"] -HEALTHCHECK CMD ["/app/lldap", "run", "--config-file", "/data/lldap_config.toml"] +HEALTHCHECK CMD ["/app/lldap", "healthcheck", "--config-file", "/data/lldap_config.toml"] diff --git a/.github/workflows/Dockerfile.ci.debian b/.github/workflows/Dockerfile.ci.debian index b27b4e2..2b74df5 100644 --- a/.github/workflows/Dockerfile.ci.debian +++ b/.github/workflows/Dockerfile.ci.debian @@ -69,4 +69,4 @@ VOLUME ["/data"] WORKDIR /app ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"] CMD ["run", "--config-file", "/data/lldap_config.toml"] -HEALTHCHECK CMD ["/app/lldap", "run", "--config-file", "/data/lldap_config.toml"] +HEALTHCHECK CMD ["/app/lldap", "healthcheck", "--config-file", "/data/lldap_config.toml"] diff --git a/Dockerfile b/Dockerfile index 8eac2d9..c82aff3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -94,4 +94,4 @@ EXPOSE ${LDAP_PORT} ${HTTP_PORT} ENTRYPOINT ["/app/docker-entrypoint.sh"] CMD ["run", "--config-file", "/data/lldap_config.toml"] -HEALTHCHECK CMD ["/app/lldap", "run", "--config-file", "/data/lldap_config.toml"] +HEALTHCHECK CMD ["/app/lldap", "healthcheck", "--config-file", "/data/lldap_config.toml"] From 193a0fd710ba436c65eed3c169962398458d55cf Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Wed, 15 Feb 2023 11:15:02 +0100 Subject: [PATCH 36/62] server: Remove trailing / from the domain URL --- server/src/infra/mail.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/infra/mail.rs b/server/src/infra/mail.rs index 8105d31..c10815c 100644 --- a/server/src/infra/mail.rs +++ b/server/src/infra/mail.rs @@ -65,7 +65,9 @@ compromised. You should reset your password and contact an administrator. To reset your password please visit the following URL: {}/reset-password/step2/{} Please contact an administrator if you did not initiate the process.", - username, domain, token + username, + domain.trim_end_matches('/'), + token ); send_email(to, "[LLDAP] Password reset requested", body, options).await } From bebb00aa2e416c21f37bab9bedc4a1bb2c271936 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Wed, 15 Feb 2023 14:21:51 +0100 Subject: [PATCH 37/62] app: improve error message for wrong/expired reset token --- app/src/components/app.rs | 120 ++++++++++----------- app/src/components/reset_password_step2.rs | 16 ++- server/src/infra/auth_service.rs | 8 +- server/src/infra/tcp_server.rs | 3 + 4 files changed, 78 insertions(+), 69 deletions(-) diff --git a/app/src/components/app.rs b/app/src/components/app.rs index a41346f..e20fed7 100644 --- a/app/src/components/app.rs +++ b/app/src/components/app.rs @@ -32,7 +32,7 @@ pub struct App { user_info: Option<(String, bool)>, redirect_to: Option, route_dispatcher: RouteAgentDispatcher, - password_reset_enabled: bool, + password_reset_enabled: Option, task: Option, } @@ -64,7 +64,7 @@ impl Component for App { }), redirect_to: Self::get_redirect_route(), route_dispatcher: RouteAgentDispatcher::new(), - password_reset_enabled: false, + password_reset_enabled: None, task: None, }; app.task = Some( @@ -95,22 +95,21 @@ impl Component for App { Msg::Logout => { self.user_info = None; self.redirect_to = None; + self.route_dispatcher + .send(RouteRequest::ReplaceRoute(Route::from(AppRoute::Login))); } Msg::PasswordResetProbeFinished(Ok(enabled)) => { self.task = None; - self.password_reset_enabled = enabled; + self.password_reset_enabled = Some(enabled); } Msg::PasswordResetProbeFinished(Err(err)) => { self.task = None; + self.password_reset_enabled = Some(false); ConsoleService::error(&format!( "Could not probe for password reset support: {err:#}" )); } } - if self.user_info.is_none() { - self.route_dispatcher - .send(RouteRequest::ReplaceRoute(Route::from(AppRoute::Login))); - } true } @@ -160,7 +159,7 @@ impl App { let route_service = RouteService::<()>::new(); let current_route = route_service.get_path(); if current_route.contains("reset-password") { - if !self.password_reset_enabled { + if self.password_reset_enabled == Some(false) { self.route_dispatcher .send(RouteRequest::ReplaceRoute(Route::from(AppRoute::Login))); } @@ -195,11 +194,11 @@ impl App { switch: AppRoute, link: &ComponentLink, is_admin: bool, - password_reset_enabled: bool, + password_reset_enabled: Option, ) -> Html { match switch { AppRoute::Login => html! { - + }, AppRoute::CreateUser => html! { @@ -234,23 +233,20 @@ impl App { AppRoute::ChangePassword(username) => html! { }, - AppRoute::StartResetPassword => { - if password_reset_enabled { - html! { - - } - } else { + AppRoute::StartResetPassword => match password_reset_enabled { + Some(true) => html! { }, + Some(false) => { App::dispatch_route(AppRoute::Login, link, is_admin, password_reset_enabled) } - } - AppRoute::FinishResetPassword(token) => html! { - if password_reset_enabled { - html! { - - } - } else { + + None => html! {}, + }, + AppRoute::FinishResetPassword(token) => match password_reset_enabled { + Some(true) => html! { }, + Some(false) => { App::dispatch_route(AppRoute::Login, link, is_admin, password_reset_enabled) } + None => html! {}, }, } } @@ -287,50 +283,48 @@ impl App { } } else { html!{} } } - + } else { html!{} } + }
diff --git a/app/src/components/reset_password_step2.rs b/app/src/components/reset_password_step2.rs index a14b36b..c1cdeba 100644 --- a/app/src/components/reset_password_step2.rs +++ b/app/src/components/reset_password_step2.rs @@ -1,5 +1,5 @@ use crate::{ - components::router::AppRoute, + components::router::{AppRoute, NavButton}, infra::{ api::HostService, common_component::{CommonComponent, CommonComponentParts}, @@ -158,9 +158,17 @@ impl Component for ResetPasswordStep2Form { } (None, Some(e)) => { return html! { -
- {e.to_string() } -
+ <> +
+ {e.to_string() } +
+ + {"Back"} + + } } _ => (), diff --git a/server/src/infra/auth_service.rs b/server/src/infra/auth_service.rs index ce7bc1b..5dc0cc1 100644 --- a/server/src/infra/auth_service.rs +++ b/server/src/infra/auth_service.rs @@ -214,11 +214,15 @@ where let token = request .match_info() .get("token") - .ok_or_else(|| TcpError::BadRequest("Missing reset token".to_string()))?; + .ok_or_else(|| TcpError::BadRequest("Missing reset token".to_owned()))?; let user_id = data .backend_handler .get_user_id_for_password_reset_token(token) - .await?; + .await + .map_err(|e| { + debug!("Reset token error: {e:#}"); + TcpError::NotFoundError("Wrong or expired reset token".to_owned()) + })?; let _ = data .backend_handler .delete_password_reset_token(token) diff --git a/server/src/infra/tcp_server.rs b/server/src/infra/tcp_server.rs index c27846d..166b65e 100644 --- a/server/src/infra/tcp_server.rs +++ b/server/src/infra/tcp_server.rs @@ -37,6 +37,8 @@ pub enum TcpError { BadRequest(String), #[error("Internal server error: `{0}`")] InternalServerError(String), + #[error("Not found: `{0}`")] + NotFoundError(String), #[error("Unauthorized: `{0}`")] UnauthorizedError(String), } @@ -57,6 +59,7 @@ 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(), } From 733f99085820d17c09f33d784f159fd03f31e89e Mon Sep 17 00:00:00 2001 From: WS <59507751+Evantage-WS@users.noreply.github.com> Date: Mon, 20 Feb 2023 15:27:00 +0100 Subject: [PATCH 38/62] example_configs: Add Rancher example --- README.md | 5 + .../images/rancher_ldap_config.png | Bin 0 -> 152000 bytes example_configs/rancher.md | 95 ++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 example_configs/images/rancher_ldap_config.png create mode 100644 example_configs/rancher.md diff --git a/README.md b/README.md index 463ea89..f63f5b3 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,10 @@ services: Then the service will listen on two ports, one for LDAP and one for the web front-end. +### With Kubernetes + +See https://github.com/Evantage-WS/lldap-kubernetes for a LLDAP deployment for Kubernetes + ### From source To compile the project, you'll need: @@ -250,6 +254,7 @@ folder for help with: - [Nextcloud](example_configs/nextcloud.md) - [Organizr](example_configs/Organizr.md) - [Portainer](example_configs/portainer.md) +- [Rancher](example_configs/rancher.md) - [Seafile](example_configs/seafile.md) - [Syncthing](example_configs/syncthing.md) - [Vaultwarden](example_configs/vaultwarden.md) diff --git a/example_configs/images/rancher_ldap_config.png b/example_configs/images/rancher_ldap_config.png new file mode 100644 index 0000000000000000000000000000000000000000..dfec864dce5e0209042ee0050c3ed6c7a838e613 GIT binary patch literal 152000 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 z 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

8aN6bvJ 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 ni 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 X?B=Q7>@!&;2Om2t$8C9AyiffLF_f~~ literal 0 HcmV?d00001 diff --git a/example_configs/rancher.md b/example_configs/rancher.md new file mode 100644 index 0000000..09ee3d0 --- /dev/null +++ b/example_configs/rancher.md @@ -0,0 +1,95 @@ +# Configuration for SUSE Rancher (any version) +### Left (hamburger) menu > Users & Authentication > OpenLDAP (yes, we are using the OpenLDAP config page) +--- + +## LDAP configuration + +#### Hostname/IP +``` +ip-address, DNS name or when running in Kubernetes (see https://github.com/Evantage-WS/lldap-kubernetes), lldap-service.lldap.svc.cluster.local +``` +#### Port +``` +3890 +``` +#### Service Account Distinguished name +A better option is to use a readonly account for accessing the LLDAP server +``` +cn=admin,ou=people,dc=example,dc=com +``` +#### Service Account Password +``` +xxx +``` +#### User Search Base +``` +ou=people,dc=example,dc=com +``` + +#### Group Search Base +``` +ou=groups,dc=example,dc=com +``` + +#### Object Class (users) +``` +inetOrgPerson +``` + +#### Object Class (groups) +``` +groupOfUniqueNames +``` + +#### Username Attribute +``` +uid +``` + +#### Name Attribute +``` +cn +``` + +#### Login Attribute +``` +uid +``` + +#### Group Member User Attribute +``` +dn +``` + +#### User Member Attribute +``` +memberOf +``` + +#### Search Attribute (groups) +``` +cn +``` + +#### Search Attribute (users) +``` +uid|sn|givenName +``` + +#### Group Member Mapping Attribute +``` +member +``` + +#### Group DN Attribute +``` +dn +``` + +##### Choose "Search direct and nested group memberships" + +##### Fill in the username and password of an admin user at Test and Enable Authentication and hit save + +## Rancher OpenLDAP config page + +![Rancher OpenLDAP config page](images/rancher_ldap_config.png) \ No newline at end of file From 98acd68f060562f41a829e0e659a25029823069c Mon Sep 17 00:00:00 2001 From: carolosf Date: Thu, 23 Feb 2023 08:33:35 +0000 Subject: [PATCH 39/62] example_configs: Add example for Sonatype Nexus Repository Manager 3 --- README.md | 1 + example_configs/nexus.md | 56 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 example_configs/nexus.md diff --git a/README.md b/README.md index f63f5b3..55ad2ad 100644 --- a/README.md +++ b/README.md @@ -252,6 +252,7 @@ folder for help with: - [KeyCloak](example_configs/keycloak.md) - [Matrix](example_configs/matrix_synapse.yml) - [Nextcloud](example_configs/nextcloud.md) +- [Nexus](example_configs/nexus.md) - [Organizr](example_configs/Organizr.md) - [Portainer](example_configs/portainer.md) - [Rancher](example_configs/rancher.md) diff --git a/example_configs/nexus.md b/example_configs/nexus.md new file mode 100644 index 0000000..e599a25 --- /dev/null +++ b/example_configs/nexus.md @@ -0,0 +1,56 @@ +# Configuration for Sonatype Nexus Repository Manager 3 +In Nexus log in as an administrator, go to `Server Administration and configuration (gear icon)` + +Select `LDAP` under the `Security` section + +Click `Create connection` + +* Host: A name for the connection e.g. lldap +* Type: ldap +* Host: Your lldap server's ip/hostname +* Port: Your lldap server's port (3890 by default) +* Base DN: `dc=example,dc=com` +* Authentication Method: Simple Authentication +* Username or DN: `uid=admin,ou=people,dc=example,dc=com` or preferably create a read only user in lldap with the lldap_strict_readonly group. +* Password: The password for the user specified above + +Click `Verify connection` if successful click `Next` + +* Select a template: Generic ldap server +* User Relative DN: `ou=people` +* User subtree: Leave unchecked +* Object class: person +* User Filter: Leave empty to allow all users to log in or `(memberOf=uid=nexus_users,ou=groups,dc=example,dc=com)` for a specific group +* Username Attribute: `uid` +* Real Name Attribute: `cn` +* Email Attribute: `mail` +* Password Attribute: Leave blank +* Check `Enable User Synchronization` + +Test user login credentials with `Verify login` + +## Set up group mapping as roles + +Check `Map LDAP groups as roles` + +* Group Type: `Static Groups` +* Group relative DN: `ou=groups` +* Group subtree: Leave unchecked +* Group object class: `groupOfUniqueNames` +* Group ID attribute: `cn` +* Group member attribute: `member` +* Group member format: `uid=${username},ou=people,dc=example,dc=com` + +Check user mapping with `Verify user mapping` + +## Map specific roles to groups +In Nexus log in as an administrator, go to `Server Administration and configuration (gear icon)` +Select `Roles` under the `Security` section + +Click `Create Role` + +* Role ID: e.g. nexus_admin (name in nexus) +* Role Name: e.g. nexus_admin (group in lldap) +* Add privileges/roles as needed e.g. under Roles add nx-admin to the "contained" list + +Click `Save` \ No newline at end of file From 322bf26db5ae11cc8b087ca03f45512b1becb451 Mon Sep 17 00:00:00 2001 From: Luca Tagliavini Date: Sat, 25 Feb 2023 18:56:49 +0100 Subject: [PATCH 40/62] server: allow non authenticated smtp connections --- server/src/infra/mail.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/server/src/infra/mail.rs b/server/src/infra/mail.rs index c10815c..e446f58 100644 --- a/server/src/infra/mail.rs +++ b/server/src/infra/mail.rs @@ -26,11 +26,7 @@ async fn send_email(to: Mailbox, subject: &str, body: String, options: &MailOpti .header(lettre::message::header::ContentType::TEXT_PLAIN) .body(body), )?; - let creds = Credentials::new( - options.user.clone(), - options.password.unsecure().to_string(), - ); - let mailer = match options.smtp_encryption { + let mut mailer = match options.smtp_encryption { SmtpEncryption::NONE => { AsyncSmtpTransport::::builder_dangerous(&options.server) } @@ -39,12 +35,15 @@ async fn send_email(to: Mailbox, subject: &str, body: String, options: &MailOpti AsyncSmtpTransport::::starttls_relay(&options.server)? } }; - mailer - .credentials(creds) - .port(options.port) - .build() - .send(email) - .await?; + if options.user.as_str() != "" { + let creds = Credentials::new( + options.user.clone(), + options.password.unsecure().to_string(), + ); + mailer = mailer.credentials(creds) + } + + mailer.port(options.port).build().send(email).await?; Ok(()) } From c9997d4c17db165e89d9b8a86b32fc4642ca93c1 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Fri, 17 Feb 2023 15:59:32 +0100 Subject: [PATCH 41/62] server: statically enforce access control --- server/src/domain/handler.rs | 34 +- server/src/domain/ldap/group.rs | 30 +- server/src/domain/ldap/user.rs | 20 +- server/src/domain/opaque_handler.rs | 2 +- server/src/domain/sql_backend_handler.rs | 5 +- .../src/domain/sql_group_backend_handler.rs | 9 +- server/src/domain/sql_user_backend_handler.rs | 12 +- server/src/infra/access_control.rs | 317 ++++++++++++++++++ server/src/infra/auth_service.rs | 112 ++----- server/src/infra/graphql/api.rs | 68 +++- server/src/infra/graphql/mutation.rs | 113 +++---- server/src/infra/graphql/query.rs | 107 +++--- server/src/infra/ldap_handler.rs | 185 +++++----- server/src/infra/ldap_server.rs | 9 +- server/src/infra/mod.rs | 1 + server/src/infra/tcp_backend_handler.rs | 12 +- server/src/infra/tcp_server.rs | 30 +- server/src/main.rs | 5 +- 18 files changed, 712 insertions(+), 359 deletions(-) create mode 100644 server/src/infra/access_control.rs diff --git a/server/src/domain/handler.rs b/server/src/domain/handler.rs index 1b2cb4f..7ba11bb 100644 --- a/server/src/domain/handler.rs +++ b/server/src/domain/handler.rs @@ -122,13 +122,17 @@ pub struct UpdateGroupRequest { } #[async_trait] -pub trait LoginHandler: Clone + Send { +pub trait LoginHandler: Send + Sync { async fn bind(&self, request: BindRequest) -> Result<()>; } #[async_trait] -pub trait GroupBackendHandler { +pub trait GroupListerBackendHandler { async fn list_groups(&self, filters: Option) -> Result>; +} + +#[async_trait] +pub trait GroupBackendHandler { async fn get_group_details(&self, group_id: GroupId) -> Result; async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; async fn create_group(&self, group_name: &str) -> Result; @@ -136,12 +140,16 @@ pub trait GroupBackendHandler { } #[async_trait] -pub trait UserBackendHandler { +pub trait UserListerBackendHandler { async fn list_users( &self, filters: Option, get_groups: bool, ) -> Result>; +} + +#[async_trait] +pub trait UserBackendHandler { async fn get_user_details(&self, user_id: &UserId) -> Result; async fn create_user(&self, request: CreateUserRequest) -> Result<()>; async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; @@ -152,7 +160,15 @@ pub trait UserBackendHandler { } #[async_trait] -pub trait BackendHandler: Clone + Send + GroupBackendHandler + UserBackendHandler {} +pub trait BackendHandler: + Send + + Sync + + GroupBackendHandler + + UserBackendHandler + + UserListerBackendHandler + + GroupListerBackendHandler +{ +} #[cfg(test)] mockall::mock! { @@ -161,16 +177,22 @@ mockall::mock! { fn clone(&self) -> Self; } #[async_trait] - impl GroupBackendHandler for TestBackendHandler { + impl GroupListerBackendHandler for TestBackendHandler { async fn list_groups(&self, filters: Option) -> Result>; + } + #[async_trait] + impl GroupBackendHandler for TestBackendHandler { async fn get_group_details(&self, group_id: GroupId) -> Result; async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; async fn create_group(&self, group_name: &str) -> Result; async fn delete_group(&self, group_id: GroupId) -> Result<()>; } #[async_trait] - impl UserBackendHandler for TestBackendHandler { + impl UserListerBackendHandler for TestBackendHandler { async fn list_users(&self, filters: Option, get_groups: bool) -> Result>; + } + #[async_trait] + impl UserBackendHandler for TestBackendHandler { async fn get_user_details(&self, user_id: &UserId) -> Result; async fn create_user(&self, request: CreateUserRequest) -> Result<()>; async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; diff --git a/server/src/domain/ldap/group.rs b/server/src/domain/ldap/group.rs index 00bde72..4f29b7d 100644 --- a/server/src/domain/ldap/group.rs +++ b/server/src/domain/ldap/group.rs @@ -1,10 +1,10 @@ use ldap3_proto::{ proto::LdapOp, LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry, }; -use tracing::{debug, info, instrument, warn}; +use tracing::{debug, instrument, warn}; use crate::domain::{ - handler::{BackendHandler, GroupRequestFilter}, + handler::{GroupListerBackendHandler, GroupRequestFilter}, ldap::error::LdapError, types::{Group, GroupColumn, UserId, Uuid}, }; @@ -21,7 +21,7 @@ pub fn get_group_attribute( group: &Group, base_dn_str: &str, attribute: &str, - user_filter: &Option<&UserId>, + user_filter: &Option, ignored_group_attributes: &[String], ) -> Option>> { let attribute = attribute.to_ascii_lowercase(); @@ -34,7 +34,7 @@ pub fn get_group_attribute( "member" | "uniquemember" => group .users .iter() - .filter(|u| user_filter.map(|f| *u == f).unwrap_or(true)) + .filter(|u| user_filter.as_ref().map(|f| *u == f).unwrap_or(true)) .map(|u| format!("uid={},ou=people,{}", u, base_dn_str).into_bytes()) .collect(), "1.1" => return None, @@ -81,7 +81,7 @@ fn make_ldap_search_group_result_entry( group: Group, base_dn_str: &str, attributes: &[String], - user_filter: &Option<&UserId>, + user_filter: &Option, ignored_group_attributes: &[String], ) -> LdapSearchResultEntry { let expanded_attributes = expand_group_attribute_wildcards(attributes); @@ -201,25 +201,17 @@ fn convert_group_filter( } #[instrument(skip_all, level = "debug")] -pub async fn get_groups_list( +pub async fn get_groups_list( ldap_info: &LdapInfo, ldap_filter: &LdapFilter, base: &str, - user_filter: &Option<&UserId>, - backend: &mut Backend, + backend: &Backend, ) -> LdapResult> { debug!(?ldap_filter); - let filter = convert_group_filter(ldap_info, ldap_filter)?; - let parsed_filters = match user_filter { - None => filter, - Some(u) => { - info!("Unprivileged search, limiting results"); - GroupRequestFilter::And(vec![filter, GroupRequestFilter::Member((*u).clone())]) - } - }; - debug!(?parsed_filters); + let filters = convert_group_filter(ldap_info, ldap_filter)?; + debug!(?filters); backend - .list_groups(Some(parsed_filters)) + .list_groups(Some(filters)) .await .map_err(|e| LdapError { code: LdapResultCode::Other, @@ -231,7 +223,7 @@ pub fn convert_groups_to_ldap_op<'a>( groups: Vec, attributes: &'a [String], ldap_info: &'a LdapInfo, - user_filter: &'a Option<&'a UserId>, + user_filter: &'a Option, ) -> impl Iterator + 'a { groups.into_iter().map(move |g| { LdapOp::SearchResultEntry(make_ldap_search_group_result_entry( diff --git a/server/src/domain/ldap/user.rs b/server/src/domain/ldap/user.rs index 603bb05..bad6764 100644 --- a/server/src/domain/ldap/user.rs +++ b/server/src/domain/ldap/user.rs @@ -2,10 +2,10 @@ use chrono::TimeZone; use ldap3_proto::{ proto::LdapOp, LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry, }; -use tracing::{debug, info, instrument, warn}; +use tracing::{debug, instrument, warn}; use crate::domain::{ - handler::{BackendHandler, UserRequestFilter}, + handler::{UserListerBackendHandler, UserRequestFilter}, ldap::{ error::LdapError, utils::{expand_attribute_wildcards, get_user_id_from_distinguished_name}, @@ -217,26 +217,18 @@ fn expand_user_attribute_wildcards(attributes: &[String]) -> Vec<&str> { } #[instrument(skip_all, level = "debug")] -pub async fn get_user_list( +pub async fn get_user_list( ldap_info: &LdapInfo, ldap_filter: &LdapFilter, request_groups: bool, base: &str, - user_filter: &Option<&UserId>, - backend: &mut Backend, + backend: &Backend, ) -> LdapResult> { debug!(?ldap_filter); let filters = convert_user_filter(ldap_info, ldap_filter)?; - let parsed_filters = match user_filter { - None => filters, - Some(u) => { - info!("Unprivileged search, limiting results"); - UserRequestFilter::And(vec![filters, UserRequestFilter::UserId((*u).clone())]) - } - }; - debug!(?parsed_filters); + debug!(?filters); backend - .list_users(Some(parsed_filters), request_groups) + .list_users(Some(filters), request_groups) .await .map_err(|e| LdapError { code: LdapResultCode::Other, diff --git a/server/src/domain/opaque_handler.rs b/server/src/domain/opaque_handler.rs index d5f71dd..13ed81a 100644 --- a/server/src/domain/opaque_handler.rs +++ b/server/src/domain/opaque_handler.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; pub use lldap_auth::{login, registration}; #[async_trait] -pub trait OpaqueHandler: Clone + Send { +pub trait OpaqueHandler: Send + Sync { async fn login_start( &self, request: login::ClientLoginStartRequest, diff --git a/server/src/domain/sql_backend_handler.rs b/server/src/domain/sql_backend_handler.rs index 86181f7..2b0f757 100644 --- a/server/src/domain/sql_backend_handler.rs +++ b/server/src/domain/sql_backend_handler.rs @@ -1,4 +1,4 @@ -use super::{handler::BackendHandler, sql_tables::DbConnection}; +use crate::domain::{handler::BackendHandler, sql_tables::DbConnection}; use crate::infra::configuration::Configuration; use async_trait::async_trait; @@ -23,7 +23,8 @@ pub mod tests { use crate::{ domain::{ handler::{ - CreateUserRequest, GroupBackendHandler, UserBackendHandler, UserRequestFilter, + CreateUserRequest, GroupBackendHandler, UserBackendHandler, + UserListerBackendHandler, UserRequestFilter, }, sql_tables::init_table, types::{GroupId, UserId}, diff --git a/server/src/domain/sql_group_backend_handler.rs b/server/src/domain/sql_group_backend_handler.rs index afffb5c..6ab2cb3 100644 --- a/server/src/domain/sql_group_backend_handler.rs +++ b/server/src/domain/sql_group_backend_handler.rs @@ -1,6 +1,8 @@ use crate::domain::{ error::{DomainError, Result}, - handler::{GroupBackendHandler, GroupRequestFilter, UpdateGroupRequest}, + handler::{ + GroupBackendHandler, GroupListerBackendHandler, GroupRequestFilter, UpdateGroupRequest, + }, model::{self, GroupColumn, MembershipColumn}, sql_backend_handler::SqlBackendHandler, types::{Group, GroupDetails, GroupId, Uuid}, @@ -57,7 +59,7 @@ fn get_group_filter_expr(filter: GroupRequestFilter) -> Cond { } #[async_trait] -impl GroupBackendHandler for SqlBackendHandler { +impl GroupListerBackendHandler for SqlBackendHandler { #[instrument(skip_all, level = "debug", ret, err)] async fn list_groups(&self, filters: Option) -> Result> { debug!(?filters); @@ -94,7 +96,10 @@ impl GroupBackendHandler for SqlBackendHandler { }) .collect()) } +} +#[async_trait] +impl GroupBackendHandler for SqlBackendHandler { #[instrument(skip_all, level = "debug", ret, err)] async fn get_group_details(&self, group_id: GroupId) -> Result { debug!(?group_id); diff --git a/server/src/domain/sql_user_backend_handler.rs b/server/src/domain/sql_user_backend_handler.rs index 529c3e9..a53de3d 100644 --- a/server/src/domain/sql_user_backend_handler.rs +++ b/server/src/domain/sql_user_backend_handler.rs @@ -1,6 +1,9 @@ -use super::{ +use crate::domain::{ error::{DomainError, Result}, - handler::{CreateUserRequest, UpdateUserRequest, UserBackendHandler, UserRequestFilter}, + handler::{ + CreateUserRequest, UpdateUserRequest, UserBackendHandler, UserListerBackendHandler, + UserRequestFilter, + }, model::{self, GroupColumn, UserColumn}, sql_backend_handler::SqlBackendHandler, types::{GroupDetails, GroupId, User, UserAndGroups, UserId, Uuid}, @@ -70,7 +73,7 @@ fn to_value(opt_name: &Option) -> ActiveValue> { } #[async_trait] -impl UserBackendHandler for SqlBackendHandler { +impl UserListerBackendHandler for SqlBackendHandler { #[instrument(skip_all, level = "debug", ret, err)] async fn list_users( &self, @@ -135,7 +138,10 @@ impl UserBackendHandler for SqlBackendHandler { .collect()) } } +} +#[async_trait] +impl UserBackendHandler for SqlBackendHandler { #[instrument(skip_all, level = "debug", ret)] async fn get_user_details(&self, user_id: &UserId) -> Result { debug!(?user_id); diff --git a/server/src/infra/access_control.rs b/server/src/infra/access_control.rs new file mode 100644 index 0000000..e4196cc --- /dev/null +++ b/server/src/infra/access_control.rs @@ -0,0 +1,317 @@ +use std::collections::HashSet; + +use async_trait::async_trait; +use tracing::info; + +use crate::domain::{ + error::Result, + handler::{ + BackendHandler, CreateUserRequest, GroupListerBackendHandler, GroupRequestFilter, + UpdateGroupRequest, UpdateUserRequest, UserListerBackendHandler, UserRequestFilter, + }, + types::{Group, GroupDetails, GroupId, User, UserAndGroups, UserId}, +}; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Permission { + Admin, + PasswordManager, + Readonly, + Regular, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ValidationResults { + pub user: UserId, + pub permission: Permission, +} + +impl ValidationResults { + #[cfg(test)] + pub fn admin() -> Self { + Self { + user: UserId::new("admin"), + permission: Permission::Admin, + } + } + + #[must_use] + pub fn is_admin(&self) -> bool { + self.permission == Permission::Admin + } + + #[must_use] + pub fn can_read_all(&self) -> bool { + self.permission == Permission::Admin + || self.permission == Permission::Readonly + || self.permission == Permission::PasswordManager + } + + #[must_use] + pub fn can_read(&self, user: &UserId) -> bool { + self.permission == Permission::Admin + || self.permission == Permission::PasswordManager + || self.permission == Permission::Readonly + || &self.user == user + } + + #[must_use] + pub fn can_change_password(&self, user: &UserId, user_is_admin: bool) -> bool { + self.permission == Permission::Admin + || (self.permission == Permission::PasswordManager && !user_is_admin) + || &self.user == user + } + + #[must_use] + pub fn can_write(&self, user: &UserId) -> bool { + self.permission == Permission::Admin || &self.user == user + } +} + +#[async_trait] +pub trait UserReadableBackendHandler { + async fn get_user_details(&self, user_id: &UserId) -> Result; + async fn get_user_groups(&self, user_id: &UserId) -> Result>; +} + +#[async_trait] +pub trait ReadonlyBackendHandler: UserReadableBackendHandler { + async fn list_users( + &self, + filters: Option, + get_groups: bool, + ) -> Result>; + async fn list_groups(&self, filters: Option) -> Result>; + async fn get_group_details(&self, group_id: GroupId) -> Result; +} + +#[async_trait] +pub trait UserWriteableBackendHandler: UserReadableBackendHandler { + async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; +} + +#[async_trait] +pub trait AdminBackendHandler: + UserWriteableBackendHandler + ReadonlyBackendHandler + UserWriteableBackendHandler +{ + async fn create_user(&self, request: CreateUserRequest) -> Result<()>; + async fn delete_user(&self, user_id: &UserId) -> Result<()>; + async fn add_user_to_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()>; + async fn remove_user_from_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()>; + async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; + async fn create_group(&self, group_name: &str) -> Result; + async fn delete_group(&self, group_id: GroupId) -> Result<()>; +} + +#[async_trait] +impl UserReadableBackendHandler for Handler { + async fn get_user_details(&self, user_id: &UserId) -> Result { + self.get_user_details(user_id).await + } + async fn get_user_groups(&self, user_id: &UserId) -> Result> { + self.get_user_groups(user_id).await + } +} + +#[async_trait] +impl ReadonlyBackendHandler for Handler { + async fn list_users( + &self, + filters: Option, + get_groups: bool, + ) -> Result> { + self.list_users(filters, get_groups).await + } + async fn list_groups(&self, filters: Option) -> Result> { + self.list_groups(filters).await + } + async fn get_group_details(&self, group_id: GroupId) -> Result { + self.get_group_details(group_id).await + } +} + +#[async_trait] +impl UserWriteableBackendHandler for Handler { + async fn update_user(&self, request: UpdateUserRequest) -> Result<()> { + self.update_user(request).await + } +} +#[async_trait] +impl AdminBackendHandler for Handler { + async fn create_user(&self, request: CreateUserRequest) -> Result<()> { + self.create_user(request).await + } + async fn delete_user(&self, user_id: &UserId) -> Result<()> { + self.delete_user(user_id).await + } + async fn add_user_to_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()> { + self.add_user_to_group(user_id, group_id).await + } + async fn remove_user_from_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()> { + self.remove_user_from_group(user_id, group_id).await + } + async fn update_group(&self, request: UpdateGroupRequest) -> Result<()> { + self.update_group(request).await + } + async fn create_group(&self, group_name: &str) -> Result { + self.create_group(group_name).await + } + async fn delete_group(&self, group_id: GroupId) -> Result<()> { + self.delete_group(group_id).await + } +} + +pub struct AccessControlledBackendHandler { + handler: Handler, +} + +impl Clone for AccessControlledBackendHandler { + fn clone(&self) -> Self { + Self { + handler: self.handler.clone(), + } + } +} + +impl AccessControlledBackendHandler { + pub fn unsafe_get_handler(&self) -> &Handler { + &self.handler + } +} + +impl AccessControlledBackendHandler { + pub fn new(handler: Handler) -> Self { + Self { handler } + } + + pub fn get_admin_handler( + &self, + validation_result: &ValidationResults, + ) -> Option<&impl AdminBackendHandler> { + validation_result.is_admin().then_some(&self.handler) + } + + pub fn get_readonly_handler( + &self, + validation_result: &ValidationResults, + ) -> Option<&impl ReadonlyBackendHandler> { + validation_result.can_read_all().then_some(&self.handler) + } + + pub fn get_writeable_handler( + &self, + validation_result: &ValidationResults, + user_id: &UserId, + ) -> Option<&impl UserWriteableBackendHandler> { + validation_result + .can_write(user_id) + .then_some(&self.handler) + } + + pub fn get_readable_handler( + &self, + validation_result: &ValidationResults, + user_id: &UserId, + ) -> Option<&impl UserReadableBackendHandler> { + validation_result.can_read(user_id).then_some(&self.handler) + } + + pub fn get_user_restricted_lister_handler( + &self, + validation_result: &ValidationResults, + ) -> UserRestrictedListerBackendHandler<'_, Handler> { + UserRestrictedListerBackendHandler { + handler: &self.handler, + user_filter: if validation_result.can_read_all() { + None + } else { + info!("Unprivileged search, limiting results"); + Some(validation_result.user.clone()) + }, + } + } + + pub async fn get_permissions_for_user(&self, user_id: UserId) -> Result { + let user_groups = self.handler.get_user_groups(&user_id).await?; + Ok(self.get_permissions_from_groups(user_id, user_groups.iter().map(|g| &g.display_name))) + } + + pub fn get_permissions_from_groups<'a, Groups: Iterator + Clone + 'a>( + &self, + user_id: UserId, + groups: Groups, + ) -> ValidationResults { + let is_in_group = |name| groups.clone().any(|g| g == name); + ValidationResults { + user: user_id, + permission: if is_in_group("lldap_admin") { + Permission::Admin + } else if is_in_group("lldap_password_manager") { + Permission::PasswordManager + } else if is_in_group("lldap_strict_readonly") { + Permission::Readonly + } else { + Permission::Regular + }, + } + } +} + +pub struct UserRestrictedListerBackendHandler<'a, Handler> { + handler: &'a Handler, + pub user_filter: Option, +} + +#[async_trait] +impl<'a, Handler: UserListerBackendHandler + Sync> UserListerBackendHandler + for UserRestrictedListerBackendHandler<'a, Handler> +{ + async fn list_users( + &self, + filters: Option, + get_groups: bool, + ) -> Result> { + let user_filter = self + .user_filter + .as_ref() + .map(|u| UserRequestFilter::UserId(u.clone())); + let filters = match (filters, user_filter) { + (None, None) => None, + (None, u) => u, + (f, None) => f, + (Some(f), Some(u)) => Some(UserRequestFilter::And(vec![f, u])), + }; + self.handler.list_users(filters, get_groups).await + } +} + +#[async_trait] +impl<'a, Handler: GroupListerBackendHandler + Sync> GroupListerBackendHandler + for UserRestrictedListerBackendHandler<'a, Handler> +{ + async fn list_groups(&self, filters: Option) -> Result> { + let group_filter = self + .user_filter + .as_ref() + .map(|u| GroupRequestFilter::Member(u.clone())); + let filters = match (filters, group_filter) { + (None, None) => None, + (None, u) => u, + (f, None) => f, + (Some(f), Some(u)) => Some(GroupRequestFilter::And(vec![f, u])), + }; + self.handler.list_groups(filters).await + } +} + +#[async_trait] +pub trait UserAndGroupListerBackendHandler: + UserListerBackendHandler + GroupListerBackendHandler +{ +} + +#[async_trait] +impl<'a, Handler: GroupListerBackendHandler + UserListerBackendHandler + Sync> + UserAndGroupListerBackendHandler for UserRestrictedListerBackendHandler<'a, Handler> +{ +} diff --git a/server/src/infra/auth_service.rs b/server/src/infra/auth_service.rs index 5dc0cc1..4b350e0 100644 --- a/server/src/infra/auth_service.rs +++ b/server/src/infra/auth_service.rs @@ -30,6 +30,7 @@ use crate::{ types::{GroupDetails, UserColumn, UserId}, }, infra::{ + access_control::{ReadonlyBackendHandler, UserReadableBackendHandler, ValidationResults}, tcp_backend_handler::*, tcp_server::{error_to_http_response, AppState, TcpError, TcpResult}, }, @@ -87,11 +88,10 @@ async fn get_refresh( where Backend: TcpBackendHandler + BackendHandler + 'static, { - let backend_handler = &data.backend_handler; let jwt_key = &data.jwt_key; let (refresh_token_hash, user) = get_refresh_token(request)?; let found = data - .backend_handler + .get_tcp_handler() .check_token(refresh_token_hash, &user) .await?; if !found { @@ -99,7 +99,8 @@ where "Invalid refresh token".to_string(), ))); } - Ok(backend_handler + Ok(data + .get_readonly_handler() .get_user_groups(&user) .await .map(|groups| create_jwt(jwt_key, user.to_string(), groups)) @@ -145,7 +146,7 @@ where .get("user_id") .ok_or_else(|| TcpError::BadRequest("Missing user ID".to_string()))?; let user_results = data - .backend_handler + .get_readonly_handler() .list_users( Some(UserRequestFilter::Or(vec![ UserRequestFilter::UserId(UserId::new(user_string)), @@ -163,7 +164,7 @@ where } let user = &user_results[0].user; let token = match data - .backend_handler + .get_tcp_handler() .start_password_reset(&user.user_id) .await? { @@ -216,7 +217,7 @@ where .get("token") .ok_or_else(|| TcpError::BadRequest("Missing reset token".to_owned()))?; let user_id = data - .backend_handler + .get_tcp_handler() .get_user_id_for_password_reset_token(token) .await .map_err(|e| { @@ -224,7 +225,7 @@ where TcpError::NotFoundError("Wrong or expired reset token".to_owned()) })?; let _ = data - .backend_handler + .get_tcp_handler() .delete_password_reset_token(token) .await; let groups = HashSet::new(); @@ -266,10 +267,10 @@ where Backend: TcpBackendHandler + BackendHandler + 'static, { let (refresh_token_hash, user) = get_refresh_token(request)?; - data.backend_handler + data.get_tcp_handler() .delete_refresh_token(refresh_token_hash) .await?; - let new_blacklisted_jwts = data.backend_handler.blacklist_jwts(&user).await?; + let new_blacklisted_jwts = data.get_tcp_handler().blacklist_jwts(&user).await?; let mut jwt_blacklist = data.jwt_blacklist.write().unwrap(); for jwt in new_blacklisted_jwts { jwt_blacklist.insert(jwt); @@ -320,7 +321,7 @@ async fn opaque_login_start( where Backend: OpaqueHandler + 'static, { - data.backend_handler + data.get_opaque_handler() .login_start(request.into_inner()) .await .map(|res| ApiResult::Left(web::Json(res))) @@ -337,8 +338,8 @@ where { // The authentication was successful, we need to fetch the groups to create the JWT // token. - let groups = data.backend_handler.get_user_groups(name).await?; - let (refresh_token, max_age) = data.backend_handler.create_refresh_token(name).await?; + let groups = data.get_readonly_handler().get_user_groups(name).await?; + let (refresh_token, max_age) = data.get_tcp_handler().create_refresh_token(name).await?; let token = create_jwt(&data.jwt_key, name.to_string(), groups); let refresh_token_plus_name = refresh_token + "+" + name.as_str(); @@ -374,7 +375,7 @@ where Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + 'static, { let name = data - .backend_handler + .get_opaque_handler() .login_finish(request.into_inner()) .await?; get_login_successful_response(&data, &name).await @@ -405,7 +406,7 @@ where name: user_id.clone(), password: request.password.clone(), }; - data.backend_handler.bind(bind_request).await?; + data.get_login_handler().bind(bind_request).await?; get_login_successful_response(&data, &user_id).await } @@ -431,7 +432,7 @@ where { let name = request.name.clone(); debug!(%name); - data.backend_handler.bind(request.into_inner()).await?; + data.get_login_handler().bind(request.into_inner()).await?; get_login_successful_response(&data, &name).await } @@ -474,7 +475,7 @@ where .into_inner(); let user_id = UserId::new(®istration_start_request.username); let user_is_admin = data - .backend_handler + .get_readonly_handler() .get_user_groups(&user_id) .await? .iter() @@ -485,7 +486,7 @@ where )); } Ok(data - .backend_handler + .get_opaque_handler() .registration_start(registration_start_request) .await?) } @@ -512,7 +513,7 @@ async fn opaque_register_finish( where Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + 'static, { - data.backend_handler + data.get_opaque_handler() .registration_finish(request.into_inner()) .await?; Ok(HttpResponse::Ok().finish()) @@ -586,64 +587,8 @@ where } } -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Permission { - Admin, - PasswordManager, - Readonly, - Regular, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ValidationResults { - pub user: UserId, - pub permission: Permission, -} - -impl ValidationResults { - #[cfg(test)] - pub fn admin() -> Self { - Self { - user: UserId::new("admin"), - permission: Permission::Admin, - } - } - - #[must_use] - pub fn is_admin(&self) -> bool { - self.permission == Permission::Admin - } - - #[must_use] - pub fn is_admin_or_readonly(&self) -> bool { - self.permission == Permission::Admin - || self.permission == Permission::Readonly - || self.permission == Permission::PasswordManager - } - - #[must_use] - pub fn can_read(&self, user: &UserId) -> bool { - self.permission == Permission::Admin - || self.permission == Permission::PasswordManager - || self.permission == Permission::Readonly - || &self.user == user - } - - #[must_use] - pub fn can_change_password(&self, user: &UserId, user_is_admin: bool) -> bool { - self.permission == Permission::Admin - || (self.permission == Permission::PasswordManager && !user_is_admin) - || &self.user == user - } - - #[must_use] - pub fn can_write(&self, user: &UserId) -> bool { - self.permission == Permission::Admin || &self.user == user - } -} - #[instrument(skip_all, level = "debug", err, ret)] -pub(crate) fn check_if_token_is_valid( +pub(crate) fn check_if_token_is_valid( state: &AppState, token_str: &str, ) -> Result { @@ -666,19 +611,10 @@ pub(crate) fn check_if_token_is_valid( if state.jwt_blacklist.read().unwrap().contains(&jwt_hash) { return Err(ErrorUnauthorized("JWT was logged out")); } - let is_in_group = |name| token.claims().groups.contains(name); - Ok(ValidationResults { - user: UserId::new(&token.claims().user), - permission: if is_in_group("lldap_admin") { - Permission::Admin - } else if is_in_group("lldap_password_manager") { - Permission::PasswordManager - } else if is_in_group("lldap_strict_readonly") { - Permission::Readonly - } else { - Permission::Regular - }, - }) + Ok(state.backend_handler.get_permissions_from_groups( + UserId::new(&token.claims().user), + token.claims().groups.iter(), + )) } pub fn configure_server(cfg: &mut web::ServiceConfig, enable_password_reset: bool) diff --git a/server/src/infra/graphql/api.rs b/server/src/infra/graphql/api.rs index b228993..eda2c25 100644 --- a/server/src/infra/graphql/api.rs +++ b/server/src/infra/graphql/api.rs @@ -1,29 +1,77 @@ use crate::{ - domain::handler::BackendHandler, + domain::{handler::BackendHandler, types::UserId}, infra::{ - auth_service::{check_if_token_is_valid, ValidationResults}, + access_control::{ + AccessControlledBackendHandler, AdminBackendHandler, ReadonlyBackendHandler, + UserReadableBackendHandler, UserWriteableBackendHandler, ValidationResults, + }, + auth_service::check_if_token_is_valid, cli::ExportGraphQLSchemaOpts, + graphql::{mutation::Mutation, query::Query}, tcp_server::AppState, }, }; use actix_web::{web, Error, HttpResponse}; use actix_web_httpauth::extractors::bearer::BearerAuth; -use juniper::{EmptySubscription, RootNode}; +use juniper::{EmptySubscription, FieldError, RootNode}; use juniper_actix::{graphiql_handler, graphql_handler, playground_handler}; - -use super::{mutation::Mutation, query::Query}; +use tracing::debug; pub struct Context { - pub handler: Box, + pub handler: AccessControlledBackendHandler, pub validation_result: ValidationResults, } +pub fn field_error_callback<'a>( + span: &'a tracing::Span, + error_message: &'a str, +) -> impl 'a + FnOnce() -> FieldError { + move || { + span.in_scope(|| debug!("Unauthorized")); + FieldError::from(error_message) + } +} + +impl Context { + #[cfg(test)] + pub fn new_for_tests(handler: Handler, validation_result: ValidationResults) -> Self { + Self { + handler: AccessControlledBackendHandler::new(handler), + validation_result, + } + } + + pub fn get_admin_handler(&self) -> Option<&impl AdminBackendHandler> { + self.handler.get_admin_handler(&self.validation_result) + } + + pub fn get_readonly_handler(&self) -> Option<&impl ReadonlyBackendHandler> { + self.handler.get_readonly_handler(&self.validation_result) + } + + pub fn get_writeable_handler( + &self, + user_id: &UserId, + ) -> Option<&impl UserWriteableBackendHandler> { + self.handler + .get_writeable_handler(&self.validation_result, user_id) + } + + pub fn get_readable_handler( + &self, + user_id: &UserId, + ) -> Option<&impl UserReadableBackendHandler> { + self.handler + .get_readable_handler(&self.validation_result, user_id) + } +} + impl juniper::Context for Context {} type Schema = RootNode<'static, Query, Mutation, EmptySubscription>>; -fn schema() -> Schema { +fn schema() -> Schema { Schema::new( Query::::new(), Mutation::::new(), @@ -58,7 +106,7 @@ async fn playground_route() -> Result { playground_handler("/api/graphql", None).await } -async fn graphql_route( +async fn graphql_route( req: actix_web::HttpRequest, mut payload: actix_web::web::Payload, data: web::Data>, @@ -67,7 +115,7 @@ async fn graphql_route( let bearer = BearerAuth::from_request(&req, &mut payload.0).await?; let validation_result = check_if_token_is_valid(&data, bearer.token())?; let context = Context:: { - handler: Box::new(data.backend_handler.clone()), + handler: data.backend_handler.clone(), validation_result, }; graphql_handler(&schema(), &context, req, payload).await @@ -75,7 +123,7 @@ async fn graphql_route( pub fn configure_endpoint(cfg: &mut web::ServiceConfig) where - Backend: BackendHandler + Sync + 'static, + Backend: BackendHandler + Clone + 'static, { let json_config = web::JsonConfig::default() .limit(4096) diff --git a/server/src/infra/graphql/mutation.rs b/server/src/infra/graphql/mutation.rs index 9a25013..5134334 100644 --- a/server/src/infra/graphql/mutation.rs +++ b/server/src/infra/graphql/mutation.rs @@ -1,6 +1,15 @@ -use crate::domain::{ - handler::{BackendHandler, CreateUserRequest, UpdateGroupRequest, UpdateUserRequest}, - types::{GroupId, JpegPhoto, UserId}, +use crate::{ + domain::{ + handler::{BackendHandler, CreateUserRequest, UpdateGroupRequest, UpdateUserRequest}, + types::{GroupId, JpegPhoto, UserId}, + }, + infra::{ + access_control::{ + AdminBackendHandler, ReadonlyBackendHandler, UserReadableBackendHandler, + UserWriteableBackendHandler, + }, + graphql::api::field_error_callback, + }, }; use anyhow::Context as AnyhowContext; use juniper::{graphql_object, FieldResult, GraphQLInputObject, GraphQLObject}; @@ -65,19 +74,18 @@ impl Success { } #[graphql_object(context = Context)] -impl Mutation { +impl Mutation { async fn create_user( context: &Context, user: CreateUserInput, ) -> FieldResult> { let span = debug_span!("[GraphQL mutation] create_user"); span.in_scope(|| { - debug!(?user.id); + debug!("{:?}", &user.id); }); - if !context.validation_result.is_admin() { - span.in_scope(|| debug!("Unauthorized")); - return Err("Unauthorized user creation".into()); - } + let handler = context + .get_admin_handler() + .ok_or_else(field_error_callback(&span, "Unauthorized user creation"))?; let user_id = UserId::new(&user.id); let avatar = user .avatar @@ -87,8 +95,7 @@ impl Mutation { .map(JpegPhoto::try_from) .transpose() .context("Provided image is not a valid JPEG")?; - context - .handler + handler .create_user(CreateUserRequest { user_id: user_id.clone(), email: user.email, @@ -99,8 +106,7 @@ impl Mutation { }) .instrument(span.clone()) .await?; - Ok(context - .handler + Ok(handler .get_user_details(&user_id) .instrument(span) .await @@ -115,13 +121,11 @@ impl Mutation { span.in_scope(|| { debug!(?name); }); - if !context.validation_result.is_admin() { - span.in_scope(|| debug!("Unauthorized")); - return Err("Unauthorized group creation".into()); - } - let group_id = context.handler.create_group(&name).await?; - Ok(context - .handler + let handler = context + .get_admin_handler() + .ok_or_else(field_error_callback(&span, "Unauthorized group creation"))?; + let group_id = handler.create_group(&name).await?; + Ok(handler .get_group_details(group_id) .instrument(span) .await @@ -137,10 +141,9 @@ impl Mutation { debug!(?user.id); }); let user_id = UserId::new(&user.id); - if !context.validation_result.can_write(&user_id) { - span.in_scope(|| debug!("Unauthorized")); - return Err("Unauthorized user update".into()); - } + let handler = context + .get_writeable_handler(&user_id) + .ok_or_else(field_error_callback(&span, "Unauthorized user update"))?; let avatar = user .avatar .map(base64::decode) @@ -149,8 +152,7 @@ impl Mutation { .map(JpegPhoto::try_from) .transpose() .context("Provided image is not a valid JPEG")?; - context - .handler + handler .update_user(UpdateUserRequest { user_id, email: user.email, @@ -172,16 +174,14 @@ impl Mutation { span.in_scope(|| { debug!(?group.id); }); - if !context.validation_result.is_admin() { - span.in_scope(|| debug!("Unauthorized")); - return Err("Unauthorized group update".into()); - } + let handler = context + .get_admin_handler() + .ok_or_else(field_error_callback(&span, "Unauthorized group update"))?; if group.id == 1 { span.in_scope(|| debug!("Cannot change admin group details")); return Err("Cannot change admin group details".into()); } - context - .handler + handler .update_group(UpdateGroupRequest { group_id: GroupId(group.id), display_name: group.display_name, @@ -200,12 +200,13 @@ impl Mutation { span.in_scope(|| { debug!(?user_id, ?group_id); }); - if !context.validation_result.is_admin() { - span.in_scope(|| debug!("Unauthorized")); - return Err("Unauthorized group membership modification".into()); - } - context - .handler + let handler = context + .get_admin_handler() + .ok_or_else(field_error_callback( + &span, + "Unauthorized group membership modification", + ))?; + handler .add_user_to_group(&UserId::new(&user_id), GroupId(group_id)) .instrument(span) .await?; @@ -221,17 +222,18 @@ impl Mutation { span.in_scope(|| { debug!(?user_id, ?group_id); }); - if !context.validation_result.is_admin() { - span.in_scope(|| debug!("Unauthorized")); - return Err("Unauthorized group membership modification".into()); - } + let handler = context + .get_admin_handler() + .ok_or_else(field_error_callback( + &span, + "Unauthorized group membership modification", + ))?; let user_id = UserId::new(&user_id); if context.validation_result.user == user_id && group_id == 1 { span.in_scope(|| debug!("Cannot remove admin rights for current user")); return Err("Cannot remove admin rights for current user".into()); } - context - .handler + handler .remove_user_from_group(&user_id, GroupId(group_id)) .instrument(span) .await?; @@ -244,19 +246,14 @@ impl Mutation { debug!(?user_id); }); let user_id = UserId::new(&user_id); - if !context.validation_result.is_admin() { - span.in_scope(|| debug!("Unauthorized")); - return Err("Unauthorized user deletion".into()); - } + let handler = context + .get_admin_handler() + .ok_or_else(field_error_callback(&span, "Unauthorized user deletion"))?; if context.validation_result.user == user_id { span.in_scope(|| debug!("Cannot delete current user")); return Err("Cannot delete current user".into()); } - context - .handler - .delete_user(&user_id) - .instrument(span) - .await?; + handler.delete_user(&user_id).instrument(span).await?; Ok(Success::new()) } @@ -265,16 +262,14 @@ impl Mutation { span.in_scope(|| { debug!(?group_id); }); - if !context.validation_result.is_admin() { - span.in_scope(|| debug!("Unauthorized")); - return Err("Unauthorized group deletion".into()); - } + let handler = context + .get_admin_handler() + .ok_or_else(field_error_callback(&span, "Unauthorized group deletion"))?; if group_id == 1 { span.in_scope(|| debug!("Cannot delete admin group")); return Err("Cannot delete admin group".into()); } - context - .handler + handler .delete_group(GroupId(group_id)) .instrument(span) .await?; diff --git a/server/src/infra/graphql/query.rs b/server/src/infra/graphql/query.rs index 7c97050..6422844 100644 --- a/server/src/infra/graphql/query.rs +++ b/server/src/infra/graphql/query.rs @@ -1,7 +1,13 @@ -use crate::domain::{ - handler::BackendHandler, - ldap::utils::map_user_field, - types::{GroupDetails, GroupId, UserColumn, UserId}, +use crate::{ + domain::{ + handler::BackendHandler, + ldap::utils::map_user_field, + types::{GroupDetails, GroupId, UserColumn, UserId}, + }, + infra::{ + access_control::{ReadonlyBackendHandler, UserReadableBackendHandler}, + graphql::api::field_error_callback, + }, }; use chrono::TimeZone; use juniper::{graphql_object, FieldResult, GraphQLInputObject}; @@ -112,7 +118,7 @@ impl Query { } #[graphql_object(context = Context)] -impl Query { +impl Query { fn api_version() -> &'static str { "1.0" } @@ -123,12 +129,13 @@ impl Query { debug!(?user_id); }); let user_id = UserId::new(&user_id); - if !context.validation_result.can_read(&user_id) { - span.in_scope(|| debug!("Unauthorized")); - return Err("Unauthorized access to user data".into()); - } - Ok(context - .handler + let handler = context + .get_readable_handler(&user_id) + .ok_or_else(field_error_callback( + &span, + "Unauthorized access to user data", + ))?; + Ok(handler .get_user_details(&user_id) .instrument(span) .await @@ -143,12 +150,13 @@ impl Query { span.in_scope(|| { debug!(?filters); }); - if !context.validation_result.is_admin_or_readonly() { - span.in_scope(|| debug!("Unauthorized")); - return Err("Unauthorized access to user list".into()); - } - Ok(context - .handler + let handler = context + .get_readonly_handler() + .ok_or_else(field_error_callback( + &span, + "Unauthorized access to user list", + ))?; + Ok(handler .list_users(filters.map(TryInto::try_into).transpose()?, false) .instrument(span) .await @@ -157,12 +165,13 @@ impl Query { async fn groups(context: &Context) -> FieldResult>> { let span = debug_span!("[GraphQL query] groups"); - if !context.validation_result.is_admin_or_readonly() { - span.in_scope(|| debug!("Unauthorized")); - return Err("Unauthorized access to group list".into()); - } - Ok(context - .handler + let handler = context + .get_readonly_handler() + .ok_or_else(field_error_callback( + &span, + "Unauthorized access to group list", + ))?; + Ok(handler .list_groups(None) .instrument(span) .await @@ -174,12 +183,13 @@ impl Query { span.in_scope(|| { debug!(?group_id); }); - if !context.validation_result.is_admin_or_readonly() { - span.in_scope(|| debug!("Unauthorized")); - return Err("Unauthorized access to group data".into()); - } - Ok(context - .handler + let handler = context + .get_readonly_handler() + .ok_or_else(field_error_callback( + &span, + "Unauthorized access to group data", + ))?; + Ok(handler .get_group_details(GroupId(group_id)) .instrument(span) .await @@ -205,7 +215,7 @@ impl Default for User { } #[graphql_object(context = Context)] -impl User { +impl User { fn id(&self) -> &str { self.user.user_id.as_str() } @@ -244,8 +254,10 @@ impl User { span.in_scope(|| { debug!(user_id = ?self.user.user_id); }); - Ok(context - .handler + let handler = context + .get_readable_handler(&self.user.user_id) + .expect("We shouldn't be able to get there without readable permission"); + Ok(handler .get_user_groups(&self.user.user_id) .instrument(span) .await @@ -283,7 +295,7 @@ pub struct Group { } #[graphql_object(context = Context)] -impl Group { +impl Group { fn id(&self) -> i32 { self.group_id } @@ -302,12 +314,13 @@ impl Group { span.in_scope(|| { debug!(name = %self.display_name); }); - if !context.validation_result.is_admin_or_readonly() { - span.in_scope(|| debug!("Unauthorized")); - return Err("Unauthorized access to group data".into()); - } - Ok(context - .handler + let handler = context + .get_readonly_handler() + .ok_or_else(field_error_callback( + &span, + "Unauthorized access to group data", + ))?; + Ok(handler .list_users( Some(DomainRequestFilter::MemberOfId(GroupId(self.group_id))), false, @@ -347,7 +360,9 @@ impl From for Group { #[cfg(test)] mod tests { use super::*; - use crate::{domain::handler::MockTestBackendHandler, infra::auth_service::ValidationResults}; + use crate::{ + domain::handler::MockTestBackendHandler, infra::access_control::ValidationResults, + }; use chrono::TimeZone; use juniper::{ execute, graphql_value, DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLType, @@ -406,10 +421,8 @@ mod tests { .with(eq(UserId::new("bob"))) .return_once(|_| Ok(groups)); - let context = Context:: { - handler: Box::new(mock), - validation_result: ValidationResults::admin(), - }; + let context = + Context::::new_for_tests(mock, ValidationResults::admin()); let schema = schema(Query::::new()); assert_eq!( @@ -486,10 +499,8 @@ mod tests { ]) }); - let context = Context:: { - handler: Box::new(mock), - validation_result: ValidationResults::admin(), - }; + let context = + Context::::new_for_tests(mock, ValidationResults::admin()); let schema = schema(Query::::new()); assert_eq!( diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index e24bcf0..1478ec8 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -12,7 +12,10 @@ use crate::{ opaque_handler::OpaqueHandler, types::{Group, JpegPhoto, UserAndGroups, UserId}, }, - infra::auth_service::{Permission, ValidationResults}, + infra::access_control::{ + AccessControlledBackendHandler, AdminBackendHandler, UserAndGroupListerBackendHandler, + UserReadableBackendHandler, ValidationResults, + }, }; use anyhow::Result; use ldap3_proto::proto::{ @@ -175,15 +178,27 @@ fn root_dse_response(base_dn: &str) -> LdapOp { }) } -pub struct LdapHandler { +pub struct LdapHandler { user_info: Option, - backend_handler: Backend, + backend_handler: AccessControlledBackendHandler, ldap_info: LdapInfo, } +impl LdapHandler { + pub fn get_login_handler(&self) -> &impl LoginHandler { + self.backend_handler.unsafe_get_handler() + } +} + +impl LdapHandler { + pub fn get_opaque_handler(&self) -> &impl OpaqueHandler { + self.backend_handler.unsafe_get_handler() + } +} + impl LdapHandler { pub fn new( - backend_handler: Backend, + backend_handler: AccessControlledBackendHandler, mut ldap_base_dn: String, ignored_user_attributes: Vec, ignored_group_attributes: Vec, @@ -206,6 +221,16 @@ impl LdapHandler Self { + Self::new( + AccessControlledBackendHandler::new(backend_handler), + ldap_base_dn.to_string(), + vec![], + vec![], + ) + } + #[instrument(skip_all, level = "debug")] pub async fn do_bind(&mut self, request: &LdapBindRequest) -> (LdapResultCode, String) { debug!("DN: {}", &request.dn); @@ -219,7 +244,7 @@ impl LdapHandler LdapHandler { - let user_groups = self.backend_handler.get_user_groups(&user_id).await; - let is_in_group = |name| { - user_groups - .as_ref() - .map(|groups| groups.iter().any(|g| g.display_name == name)) - .unwrap_or(false) - }; - self.user_info = Some(ValidationResults { - user: user_id, - permission: if is_in_group("lldap_admin") { - Permission::Admin - } else if is_in_group("lldap_password_manager") { - Permission::PasswordManager - } else if is_in_group("lldap_strict_readonly") { - Permission::Readonly - } else { - Permission::Regular - }, - }); + self.user_info = self + .backend_handler + .get_permissions_for_user(user_id) + .await + .ok(); debug!("Success!"); (LdapResultCode::Success, "".to_string()) } @@ -253,7 +264,12 @@ impl LdapHandler Result<()> { + async fn change_password( + &self, + backend_handler: &B, + user: &UserId, + password: &str, + ) -> Result<()> { use lldap_auth::*; let mut rng = rand::rngs::OsRng; let registration_start_request = @@ -262,7 +278,7 @@ impl LdapHandler LdapHandler LdapHandler { let user_is_admin = self .backend_handler + .get_readable_handler(credentials, &uid) + .expect("Unexpected permission error") .get_user_groups(&uid) .await .map_err(|e| LdapError { @@ -313,7 +331,10 @@ impl LdapHandler LdapHandler LdapResult> { - let user_info = self.user_info.as_ref().ok_or_else(|| LdapError { - code: LdapResultCode::InsufficentAccessRights, - message: "No user currently bound".to_string(), - })?; - Ok(if user_info.is_admin_or_readonly() { - None - } else { - Some(user_info.user.clone()) - }) - } - pub async fn do_search_or_dse( &mut self, request: &LdapSearchRequest, @@ -382,22 +391,22 @@ impl LdapHandler, ) -> LdapResult<(Option>, Option>)> { let dn_parts = parse_distinguished_name(&request.base.to_ascii_lowercase())?; let scope = get_search_scope(&self.ldap_info.base_dn, &dn_parts); debug!(?request.base, ?scope); // Disambiguate the lifetimes. - fn cast<'a, T, R, B: 'a>(x: T) -> T + fn cast<'a, T, R>(x: T) -> T where - T: Fn(&'a mut B, &'a LdapFilter) -> R + 'a, + T: Fn(&'a LdapFilter) -> R + 'a, { x } - let get_user_list = cast(|backend_handler: &mut Backend, filter: &LdapFilter| async { + let get_user_list = cast(|filter: &LdapFilter| async { let need_groups = request .attrs .iter() @@ -407,47 +416,27 @@ impl LdapHandler ( - Some(get_user_list(&mut self.backend_handler, &request.filter).await?), - Some(get_group_list(&mut self.backend_handler, &request.filter).await?), - ), - SearchScope::Users => ( - Some(get_user_list(&mut self.backend_handler, &request.filter).await?), - None, - ), - SearchScope::Groups => ( - None, - Some(get_group_list(&mut self.backend_handler, &request.filter).await?), + Some(get_user_list(&request.filter).await?), + Some(get_group_list(&request.filter).await?), ), + SearchScope::Users => (Some(get_user_list(&request.filter).await?), None), + SearchScope::Groups => (None, Some(get_group_list(&request.filter).await?)), SearchScope::User(filter) => { let filter = LdapFilter::And(vec![request.filter.clone(), filter]); - ( - Some(get_user_list(&mut self.backend_handler, &filter).await?), - None, - ) + (Some(get_user_list(&filter).await?), None) } SearchScope::Group(filter) => { let filter = LdapFilter::And(vec![request.filter.clone(), filter]); - ( - None, - Some(get_group_list(&mut self.backend_handler, &filter).await?), - ) + (None, Some(get_group_list(&filter).await?)) } SearchScope::Unknown => { warn!( @@ -468,10 +457,15 @@ impl LdapHandler LdapResult> { - let user_filter = self.get_user_permission_filter()?; - let user_filter = user_filter.as_ref(); - let (users, groups) = self.do_search_internal(request, &user_filter).await?; + pub async fn do_search(&self, request: &LdapSearchRequest) -> LdapResult> { + let user_info = self.user_info.as_ref().ok_or_else(|| LdapError { + code: LdapResultCode::InsufficentAccessRights, + message: "No user currently bound".to_string(), + })?; + let backend_handler = self + .backend_handler + .get_user_restricted_lister_handler(user_info); + let (users, groups) = self.do_search_internal(&backend_handler, request).await?; let mut results = Vec::new(); if let Some(users) = users { @@ -486,7 +480,7 @@ impl LdapHandler LdapHandler LdapResult> { - if !self + let backend_handler = self .user_info .as_ref() - .map(|u| u.is_admin()) - .unwrap_or(false) - { - return Err(LdapError { + .and_then(|u| self.backend_handler.get_admin_handler(u)) + .ok_or_else(|| LdapError { code: LdapResultCode::InsufficentAccessRights, message: "Unauthorized write".to_string(), - }); - } + })?; let user_id = get_user_id_from_distinguished_name( &request.dn, &self.ldap_info.base_dn, @@ -552,7 +543,7 @@ impl LdapHandler Result<()>; } #[async_trait] - impl GroupBackendHandler for TestBackendHandler { + impl GroupListerBackendHandler for TestBackendHandler { async fn list_groups(&self, filters: Option) -> Result>; + } + #[async_trait] + impl GroupBackendHandler for TestBackendHandler { async fn get_group_details(&self, group_id: GroupId) -> Result; async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; async fn create_group(&self, group_name: &str) -> Result; async fn delete_group(&self, group_id: GroupId) -> Result<()>; } #[async_trait] - impl UserBackendHandler for TestBackendHandler { + impl UserListerBackendHandler for TestBackendHandler { async fn list_users(&self, filters: Option, get_groups: bool) -> Result>; + } + #[async_trait] + impl UserBackendHandler for TestBackendHandler { async fn get_user_details(&self, user_id: &UserId) -> Result; async fn create_user(&self, request: CreateUserRequest) -> Result<()>; async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; @@ -768,8 +765,7 @@ mod tests { }); Ok(set) }); - let mut ldap_handler = - LdapHandler::new(mock, "dc=Example,dc=com".to_string(), vec![], vec![]); + let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=Example,dc=com"); let request = LdapBindRequest { dn: "uid=test,ou=people,dc=example,dc=coM".to_string(), cred: LdapBindCred::Simple("pass".to_string()), @@ -812,8 +808,7 @@ mod tests { mock.expect_get_user_groups() .with(eq(UserId::new("bob"))) .return_once(|_| Ok(HashSet::new())); - let mut ldap_handler = - LdapHandler::new(mock, "dc=eXample,dc=com".to_string(), vec![], vec![]); + let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=eXample,dc=com"); let request = LdapOp::BindRequest(LdapBindRequest { dn: "uid=bob,ou=people,dc=example,dc=com".to_string(), @@ -855,8 +850,7 @@ mod tests { }); Ok(set) }); - let mut ldap_handler = - LdapHandler::new(mock, "dc=example,dc=com".to_string(), vec![], vec![]); + let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=example,dc=com"); let request = LdapBindRequest { dn: "uid=test,ou=people,dc=example,dc=com".to_string(), @@ -997,8 +991,7 @@ mod tests { #[tokio::test] async fn test_bind_invalid_dn() { let mock = MockTestBackendHandler::new(); - let mut ldap_handler = - LdapHandler::new(mock, "dc=example,dc=com".to_string(), vec![], vec![]); + let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=example,dc=com"); let request = LdapBindRequest { dn: "cn=bob,dc=example,dc=com".to_string(), diff --git a/server/src/infra/ldap_server.rs b/server/src/infra/ldap_server.rs index 80b8cf4..31551f9 100644 --- a/server/src/infra/ldap_server.rs +++ b/server/src/infra/ldap_server.rs @@ -3,7 +3,10 @@ use crate::{ handler::{BackendHandler, LoginHandler}, opaque_handler::OpaqueHandler, }, - infra::{configuration::Configuration, ldap_handler::LdapHandler}, + infra::{ + access_control::AccessControlledBackendHandler, configuration::Configuration, + ldap_handler::LdapHandler, + }, }; use actix_rt::net::TcpStream; use actix_server::ServerBuilder; @@ -73,7 +76,7 @@ where let mut resp = FramedWrite::new(w, LdapCodec); let mut session = LdapHandler::new( - backend_handler, + AccessControlledBackendHandler::new(backend_handler), ldap_base_dn, ignored_user_attributes, ignored_group_attributes, @@ -145,7 +148,7 @@ pub fn build_ldap_server( server_builder: ServerBuilder, ) -> Result where - Backend: BackendHandler + LoginHandler + OpaqueHandler + 'static, + Backend: BackendHandler + LoginHandler + OpaqueHandler + Clone + 'static, { let context = ( backend_handler, diff --git a/server/src/infra/mod.rs b/server/src/infra/mod.rs index f0b85f9..33a2e58 100644 --- a/server/src/infra/mod.rs +++ b/server/src/infra/mod.rs @@ -1,3 +1,4 @@ +pub mod access_control; pub mod auth_service; pub mod cli; pub mod configuration; diff --git a/server/src/infra/tcp_backend_handler.rs b/server/src/infra/tcp_backend_handler.rs index f4930af..c153a96 100644 --- a/server/src/infra/tcp_backend_handler.rs +++ b/server/src/infra/tcp_backend_handler.rs @@ -4,7 +4,7 @@ use std::collections::HashSet; use crate::domain::{error::Result, types::UserId}; #[async_trait] -pub trait TcpBackendHandler { +pub trait TcpBackendHandler: Sync { async fn get_jwt_blacklist(&self) -> anyhow::Result>; async fn create_refresh_token(&self, user: &UserId) -> Result<(String, chrono::Duration)>; async fn check_token(&self, refresh_token_hash: u64, user: &UserId) -> Result; @@ -34,16 +34,22 @@ mockall::mock! { async fn bind(&self, request: BindRequest) -> Result<()>; } #[async_trait] - impl GroupBackendHandler for TestTcpBackendHandler { + impl GroupListerBackendHandler for TestTcpBackendHandler { async fn list_groups(&self, filters: Option) -> Result>; + } + #[async_trait] + impl GroupBackendHandler for TestTcpBackendHandler { async fn get_group_details(&self, group_id: GroupId) -> Result; async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; async fn create_group(&self, group_name: &str) -> Result; async fn delete_group(&self, group_id: GroupId) -> Result<()>; } #[async_trait] - impl UserBackendHandler for TestBackendHandler { + impl UserListerBackendHandler for TestBackendHandler { async fn list_users(&self, filters: Option, get_groups: bool) -> Result>; + } + #[async_trait] + impl UserBackendHandler for TestBackendHandler { async fn get_user_details(&self, user_id: &UserId) -> Result; async fn create_user(&self, request: CreateUserRequest) -> Result<()>; async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; diff --git a/server/src/infra/tcp_server.rs b/server/src/infra/tcp_server.rs index 166b65e..7179e7d 100644 --- a/server/src/infra/tcp_server.rs +++ b/server/src/infra/tcp_server.rs @@ -5,6 +5,7 @@ use crate::{ opaque_handler::OpaqueHandler, }, infra::{ + access_control::{AccessControlledBackendHandler, ReadonlyBackendHandler}, auth_service, configuration::{Configuration, MailOptions}, logging::CustomRootSpanBuilder, @@ -74,11 +75,11 @@ fn http_config( server_url: String, mail_options: MailOptions, ) where - Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Sync + 'static, + Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Clone + 'static, { let enable_password_reset = mail_options.enable_password_reset; cfg.app_data(web::Data::new(AppState:: { - backend_handler, + backend_handler: AccessControlledBackendHandler::new(backend_handler), jwt_key: Hmac::new_varkey(jwt_secret.unsecure().as_bytes()).unwrap(), jwt_blacklist: RwLock::new(jwt_blacklist), server_url, @@ -110,20 +111,41 @@ fn http_config( } pub(crate) struct AppState { - pub backend_handler: Backend, + pub backend_handler: AccessControlledBackendHandler, pub jwt_key: Hmac, pub jwt_blacklist: RwLock>, pub server_url: String, pub mail_options: MailOptions, } +impl AppState { + pub fn get_readonly_handler(&self) -> &impl ReadonlyBackendHandler { + self.backend_handler.unsafe_get_handler() + } +} +impl AppState { + pub fn get_tcp_handler(&self) -> &impl TcpBackendHandler { + self.backend_handler.unsafe_get_handler() + } +} +impl AppState { + pub fn get_opaque_handler(&self) -> &impl OpaqueHandler { + self.backend_handler.unsafe_get_handler() + } +} +impl AppState { + pub fn get_login_handler(&self) -> &impl LoginHandler { + self.backend_handler.unsafe_get_handler() + } +} + pub async fn build_tcp_server( config: &Configuration, backend_handler: Backend, server_builder: ServerBuilder, ) -> Result where - Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Sync + 'static, + Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Clone + 'static, { let jwt_secret = config.jwt_secret.clone(); let jwt_blacklist = backend_handler diff --git a/server/src/main.rs b/server/src/main.rs index 712d0a9..904487c 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -7,7 +7,10 @@ use std::time::Duration; use crate::{ domain::{ - handler::{CreateUserRequest, GroupBackendHandler, GroupRequestFilter, UserBackendHandler}, + handler::{ + CreateUserRequest, GroupBackendHandler, GroupListerBackendHandler, GroupRequestFilter, + UserBackendHandler, + }, sql_backend_handler::SqlBackendHandler, sql_opaque_handler::register_password, }, From 07de6062ca8f4ecb3f998c773ff87e949adf4c90 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Thu, 23 Feb 2023 15:39:50 +0100 Subject: [PATCH 42/62] server: update tokio --- Cargo.lock | 81 ++++++++++++++++++++++++++++----- server/Cargo.toml | 2 +- server/src/infra/ldap_server.rs | 2 +- 3 files changed, 71 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 733a60a..01e05dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2523,7 +2523,7 @@ dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", + "windows-sys 0.36.1", ] [[package]] @@ -2835,7 +2835,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-sys 0.36.1", ] [[package]] @@ -3405,7 +3405,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" dependencies = [ "lazy_static", - "windows-sys", + "windows-sys 0.36.1", ] [[package]] @@ -4179,22 +4179,22 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.19.2" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" +checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" dependencies = [ + "autocfg", "bytes", "libc", "memchr", "mio 0.8.4", "num_cpus", - "once_cell", "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "winapi", + "windows-sys 0.42.0", ] [[package]] @@ -4779,43 +4779,100 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", ] +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + [[package]] name = "windows_aarch64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + [[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + [[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + [[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + [[package]] name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + [[package]] name = "winreg" version = "0.10.1" diff --git a/server/Cargo.toml b/server/Cargo.toml index 824b569..3c9d100 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -85,7 +85,7 @@ version = "*" [dependencies.tokio] features = ["full"] -version = "1.17" +version = "1.25" [dependencies.uuid] features = ["v3"] diff --git a/server/src/infra/ldap_server.rs b/server/src/infra/ldap_server.rs index 31551f9..2961f57 100644 --- a/server/src/infra/ldap_server.rs +++ b/server/src/infra/ldap_server.rs @@ -67,7 +67,7 @@ async fn handle_ldap_stream( ) -> Result where Backend: BackendHandler + LoginHandler + OpaqueHandler + 'static, - Stream: tokio::io::AsyncRead + tokio::io::AsyncWrite, + Stream: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin, { use tokio_stream::StreamExt; let (r, w) = tokio::io::split(stream); From dce73f91efd54ee092251ea0b32f27fb045ef99c Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Thu, 23 Feb 2023 16:48:37 +0100 Subject: [PATCH 43/62] server: update actix, inline juniper-actix --- Cargo.lock | 1595 +++++++++++++----------------- Cargo.toml | 3 + server/Cargo.toml | 28 +- server/src/infra/auth_service.rs | 7 +- server/src/infra/graphql/api.rs | 129 ++- server/src/infra/tcp_server.rs | 7 +- 6 files changed, 820 insertions(+), 949 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 01e05dc..e784f0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" [[package]] name = "actix" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3720d0064a0ce5c0de7bd93bdb0a6caebab2a9b5668746145d7b3b0c5da02914" +checksum = "f728064aca1c318585bf4bb04ffcfac9e75e508ab4e8b1bd9ba5dfe04e2cbed5" dependencies = [ "actix-rt", "actix_derive", @@ -25,18 +25,18 @@ dependencies = [ "futures-util", "log", "once_cell", - "parking_lot 0.11.2", + "parking_lot 0.12.1", "pin-project-lite", "smallvec", "tokio", - "tokio-util 0.6.10", + "tokio-util", ] [[package]] name = "actix-codec" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a36c014a3e811624313b51a227b775ecba55d36ef9462bbaac7d4f13e54c9271" +checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe" dependencies = [ "bitflags", "bytes", @@ -46,14 +46,14 @@ dependencies = [ "memchr", "pin-project-lite", "tokio", - "tokio-util 0.6.10", + "tokio-util", ] [[package]] name = "actix-files" -version = "0.6.0-beta.6" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b864740ed79d26e6e3c33fd2a1e03a071daaa43c88e6900ff1f9378fca88ce" +checksum = "d832782fac6ca7369a70c9ee9a20554623c5e51c76e190ad151780ebea1cf689" dependencies = [ "actix-http", "actix-service", @@ -69,49 +69,45 @@ dependencies = [ "mime", "mime_guess", "percent-encoding", + "pin-project-lite", ] [[package]] name = "actix-http" -version = "3.0.0-beta.9" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01260589f1aafad11224002741eb37bc603b4ce55b4e3556d2b2122f9aac7c51" +checksum = "0070905b2c4a98d184c4e81025253cb192aa8a73827553f38e9410801ceb35bb" dependencies = [ "actix-codec", "actix-rt", "actix-service", - "actix-tls", "actix-utils", "ahash", - "base64", + "base64 0.21.0", "bitflags", - "brotli2", + "brotli", "bytes", "bytestring", "derive_more", "encoding_rs", "flate2", "futures-core", - "futures-util", "h2", "http", "httparse", - "itoa 0.4.8", + "httpdate", + "itoa", "language-tags", "local-channel", - "log", "mime", - "once_cell", "percent-encoding", - "pin-project", "pin-project-lite", "rand 0.8.5", - "regex", - "serde", - "sha-1", + "sha1", "smallvec", - "time 0.2.27", "tokio", + "tokio-util", + "tracing", "zstd", ] @@ -127,36 +123,22 @@ dependencies = [ [[package]] name = "actix-router" -version = "0.2.7" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad299af73649e1fc893e333ccf86f377751eb95ff875d095131574c6f43452c" +checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" dependencies = [ "bytestring", "http", - "log", - "regex", - "serde", -] - -[[package]] -name = "actix-router" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb60846b52c118f2f04a56cc90880a274271c489b2498623d58176f8ca21fa80" -dependencies = [ - "bytestring", - "firestorm", - "http", - "log", "regex", "serde", + "tracing", ] [[package]] name = "actix-rt" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ea16c295198e958ef31930a6ef37d0fb64e9ca3b6116e6b93a8bdae96ee1000" +checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" dependencies = [ "actix-macros", "futures-core", @@ -165,19 +147,20 @@ dependencies = [ [[package]] name = "actix-server" -version = "2.0.0-beta.5" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26369215fcc3b0176018b3b68756a8bcc275bb000e6212e454944913a1f9bf87" +checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327" dependencies = [ "actix-rt", "actix-service", "actix-utils", "futures-core", - "log", - "mio 0.7.14", + "futures-util", + "mio", "num_cpus", - "slab", + "socket2", "tokio", + "tracing", ] [[package]] @@ -193,28 +176,27 @@ dependencies = [ [[package]] name = "actix-tls" -version = "3.0.0-beta.5" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65b7bb60840962ef0332f7ea01a57d73a24d2cb663708511ff800250bbfef569" +checksum = "9fde0cf292f7cdc7f070803cb9a0d45c018441321a78b1042ffbbb81ec333297" dependencies = [ "actix-codec", "actix-rt", "actix-service", "actix-utils", - "derive_more", "futures-core", - "http", "log", - "tokio-rustls 0.22.0", - "tokio-util 0.6.10", - "webpki-roots 0.21.1", + "pin-project-lite", + "tokio-rustls", + "tokio-util", + "webpki-roots", ] [[package]] name = "actix-utils" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e491cbaac2e7fc788dfff99ff48ef317e23b3cf63dbaf7aaab6418f40f92aa94" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" dependencies = [ "local-waker", "pin-project-lite", @@ -222,14 +204,14 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.0.0-beta.8" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c503f726f895e55dac39adeafd14b5ee00cc956796314e9227fc7ae2e176f443" +checksum = "464e0fddc668ede5f26ec1f9557a8d44eda948732f40c6b0ad79126930eb775f" dependencies = [ "actix-codec", "actix-http", "actix-macros", - "actix-router 0.2.7", + "actix-router", "actix-rt", "actix-server", "actix-service", @@ -237,54 +219,37 @@ dependencies = [ "actix-web-codegen", "ahash", "bytes", + "bytestring", "cfg-if", "cookie", "derive_more", - "either", "encoding_rs", "futures-core", "futures-util", - "itoa 0.4.8", + "http", + "itoa", "language-tags", "log", "mime", "once_cell", - "paste", - "pin-project", + "pin-project-lite", "regex", "serde", "serde_json", "serde_urlencoded", "smallvec", "socket2", - "time 0.2.27", + "time 0.3.19", "url", ] -[[package]] -name = "actix-web-actors" -version = "4.0.0-beta.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7db5c2c78a2606e6634abee4973a4924221cfab66e48f23844256e4fb8ce0f42" -dependencies = [ - "actix", - "actix-codec", - "actix-http", - "actix-web", - "bytes", - "bytestring", - "futures-core", - "pin-project", - "tokio", -] - [[package]] name = "actix-web-codegen" -version = "0.5.0-rc.2" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d0976042e6ddc82c7d0dedd64d39959bc26d9bba098b2f6c32a73fbef784eaf" +checksum = "1fa9362663c8643d67b2d5eafba49e4cb2c8a053a29ed00a0bea121f17c76b13" dependencies = [ - "actix-router 0.5.0", + "actix-router", "proc-macro2", "quote", "syn", @@ -292,14 +257,17 @@ dependencies = [ [[package]] name = "actix-web-httpauth" -version = "0.6.0-beta.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "264d0eb4698d59493cafc96554c3919837115f8c4e9040a3790c2b55400ff758" +checksum = "6dda62cf04bc3a9ad2ea8f314f721951cfdb4cdacec4e984d20e77c7bb170991" dependencies = [ - "actix-service", + "actix-utils", "actix-web", - "base64", + "base64 0.13.1", + "futures-core", "futures-util", + "log", + "pin-project-lite", ] [[package]] @@ -315,9 +283,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.17.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" dependencies = [ "gimli", ] @@ -334,16 +302,16 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.8", "once_cell", "version_check", ] [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] @@ -354,6 +322,21 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -363,20 +346,11 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anyhow" -version = "1.0.58" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" +checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" [[package]] name = "anymap" @@ -410,25 +384,25 @@ checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" [[package]] name = "asn1-rs" -version = "0.3.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ff05a702273012438132f449575dbc804e27b2f3cbe3069aa237d26c98fa33" +checksum = "cf6690c370453db30743b373a60ba498fc0d6d83b11f4abfd87a84a075db5dd4" dependencies = [ "asn1-rs-derive", "asn1-rs-impl", "displaydoc 0.2.3", - "nom 7.1.1", + "nom 7.1.3", "num-traits", "rusticata-macros", "thiserror", - "time 0.3.11", + "time 0.3.19", ] [[package]] name = "asn1-rs-derive" -version = "0.1.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8b7511298d5b7784b40b092d9e9dcd3a627a5707e4b5e507931ab0d44eeebf" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" dependencies = [ "proc-macro2", "quote", @@ -449,19 +423,20 @@ dependencies = [ [[package]] name = "async-stream" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" +checksum = "ad445822218ce64be7a341abfb0b1ea43b5c23aa83902542a4542e78309d8e5e" dependencies = [ "async-stream-impl", "futures-core", + "pin-project-lite", ] [[package]] name = "async-stream-impl" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" +checksum = "e4655ae1a7b0cdf149156f780c5bf3f1352bc53cbd9e0a361a7ef7b22947e965" dependencies = [ "proc-macro2", "quote", @@ -470,9 +445,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.56" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" +checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" dependencies = [ "proc-macro2", "quote", @@ -503,7 +478,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -516,9 +491,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.65" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" dependencies = [ "addr2line", "cc", @@ -543,22 +518,22 @@ dependencies = [ ] [[package]] -name = "base-x" -version = "0.2.11" +name = "base64" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.13.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" [[package]] name = "base64ct" -version = "1.1.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b4d9b1225d28d360ec6a231d65af1fd99a2a095154c8040689617290569c5c" +checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" [[package]] name = "bincode" @@ -611,23 +586,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9" [[package]] -name = "brotli-sys" -version = "0.3.2" +name = "brotli" +version = "3.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4445dea95f4c2b41cde57cc9fee236ae4dbae88d8fcbdb4750fc1bb5d86aaecd" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" dependencies = [ - "cc", - "libc", + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", ] [[package]] -name = "brotli2" -version = "0.3.2" +name = "brotli-decompressor" +version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cb036c3eade309815c15ddbacec5b22c4d1f3983a774ab2eac2e3e9ea85568e" +checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" dependencies = [ - "brotli-sys", - "libc", + "alloc-no-stdlib", + "alloc-stdlib", ] [[package]] @@ -636,7 +612,7 @@ version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de0aa578035b938855a710ba58d43cfb4d435f3619f99236fb35922a574d6cb1" dependencies = [ - "base64", + "base64 0.13.1", "chrono", "hex", "lazy_static", @@ -655,9 +631,9 @@ checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" [[package]] name = "bytemuck" -version = "1.11.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5377c8865e74a160d21f29c2d40669f53286db6eab59b88540cbb12ffc8b835" +checksum = "c041d3eab048880cb0b86b256447da3f18859a163c3b8d8893f4e6368abe6393" [[package]] name = "byteorder" @@ -667,24 +643,24 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "bytestring" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b6a75fd3048808ef06af5cd79712be8111960adaf89d90250974b38fc3928a" +checksum = "f7f83e57d9154148e355404702e2694463241880b939570d7c97c014da7a69a1" dependencies = [ "bytes", ] [[package]] name = "cc" -version = "1.0.73" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" dependencies = [ "jobserver", ] @@ -712,16 +688,16 @@ dependencies = [ "num-integer", "num-traits", "serde", - "time 0.1.44", + "time 0.1.45", "wasm-bindgen", "winapi", ] [[package]] name = "clap" -version = "3.2.8" +version = "3.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "atty", "bitflags", @@ -731,16 +707,16 @@ dependencies = [ "once_cell", "strsim", "termcolor", - "textwrap", + "textwrap 0.16.0", ] [[package]] name = "clap_derive" -version = "3.2.7" +version = "3.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ - "heck 0.4.0", + "heck 0.4.1", "proc-macro-error", "proc-macro2", "quote", @@ -801,12 +777,6 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" -[[package]] -name = "const_fn" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" - [[package]] name = "constant_time_eq" version = "0.1.5" @@ -821,12 +791,12 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" [[package]] name = "cookie" -version = "0.15.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f1c7727e460397e56abc4bddc1d49e07a1ad78fc98eb2e1c8f032a58a2f80d" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ "percent-encoding", - "time 0.2.27", + "time 0.3.19", "version_check", ] @@ -848,9 +818,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "cpufeatures" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" dependencies = [ "libc", ] @@ -866,20 +836,20 @@ dependencies = [ [[package]] name = "cron" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76219e9243e100d5a37676005f08379297f8addfebc247613299600625c734d" +checksum = "1ff76b51e4c068c52bfd2866e1567bee7c567ae8f24ada09fd4307019e25eab7" dependencies = [ "chrono", - "nom 7.1.1", + "nom 7.1.3", "once_cell", ] [[package]] name = "crossbeam-channel" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" dependencies = [ "cfg-if", "crossbeam-utils", @@ -887,9 +857,9 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" dependencies = [ "cfg-if", "crossbeam-utils", @@ -897,12 +867,11 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.10" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -914,7 +883,7 @@ dependencies = [ "bitflags", "crossterm_winapi", "libc", - "mio 0.8.4", + "mio", "parking_lot 0.12.1", "signal-hook", "signal-hook-mio", @@ -992,9 +961,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.82" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453" +checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62" dependencies = [ "cc", "cxxbridge-flags", @@ -1004,9 +973,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.82" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0" +checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690" dependencies = [ "cc", "codespan-reporting", @@ -1019,15 +988,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.82" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71" +checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf" [[package]] name = "cxxbridge-macro" -version = "1.0.82" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470" +checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" dependencies = [ "proc-macro2", "quote", @@ -1071,9 +1040,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" +checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" [[package]] name = "der" @@ -1088,13 +1057,13 @@ dependencies = [ [[package]] name = "der-parser" -version = "7.0.0" +version = "8.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe398ac75057914d7d07307bf67dc7f3f574a26783b4fc7805a20ffa9f506e82" +checksum = "42d4bc9b0db0a0df9ae64634ac5bdefb7afcb534e182275ca0beadbe486701c1" dependencies = [ "asn1-rs", "displaydoc 0.2.3", - "nom 7.1.1", + "nom 7.1.3", "num-bigint", "num-traits", "rusticata-macros", @@ -1140,7 +1109,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "rustc_version 0.4.0", + "rustc_version", "syn", ] @@ -1201,12 +1170,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "discard" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" - [[package]] name = "displaydoc" version = "0.1.7" @@ -1243,31 +1206,31 @@ checksum = "4bb454f0228b18c7f4c3b0ebbee346ed9c52e7443b0999cd543ff3571205701d" [[package]] name = "either" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" [[package]] name = "email-encoding" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34dd14c63662e0206599796cd5e1ad0268ab2b9d19b868d6050d688eba2bbf98" +checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75" dependencies = [ - "base64", + "base64 0.21.0", "memchr", ] [[package]] name = "email_address" -version = "0.2.1" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8684b7c9cb4857dfa1e5b9629ef584ba618c9b93bae60f58cb23f4f271d0468e" +checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" [[package]] name = "encoding_rs" -version = "0.8.31" +version = "0.8.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" dependencies = [ "cfg-if", ] @@ -1302,18 +1265,18 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.7.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] [[package]] name = "figment" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790b4292c72618abbab50f787a477014fe15634f96291de45672ce46afe122df" +checksum = "4e56602b469b2201400dec66a66aec5a9b8761ee97cd1b8c96ab2483fcc16cc9" dependencies = [ "atomic", "pear", @@ -1332,17 +1295,11 @@ dependencies = [ "figment", ] -[[package]] -name = "firestorm" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c5f6c2c942da57e2aaaa84b8a521489486f14e75e7fa91dab70aba913975f98" - [[package]] name = "flate2" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" dependencies = [ "crc32fast", "miniz_oxide", @@ -1359,14 +1316,14 @@ dependencies = [ [[package]] name = "flume" -version = "0.10.13" +version = "0.10.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ceeb589a3157cac0ab8cc585feb749bd2cea5cb55a6ee802ad72d9fd38303da" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" dependencies = [ "futures-core", "futures-sink", "pin-project", - "spin 0.9.3", + "spin 0.9.5", ] [[package]] @@ -1377,25 +1334,33 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "matches", "percent-encoding", ] [[package]] name = "fragile" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85dcb89d2b10c5f6133de2efd8c11959ce9dbb46a2f7a4cab208c4eeda6ce1ab" +checksum = "b7464c5c4a3f014d9b2ec4073650e5c06596f385060af740fc45ad5a19f959e8" +dependencies = [ + "fragile 2.0.0", +] + +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" [[package]] name = "futures" -version = "0.3.21" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" dependencies = [ "futures-channel", "futures-core", @@ -1408,9 +1373,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.21" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" dependencies = [ "futures-core", "futures-sink", @@ -1418,9 +1383,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.21" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" [[package]] name = "futures-enum" @@ -1435,9 +1400,9 @@ dependencies = [ [[package]] name = "futures-executor" -version = "0.3.21" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" dependencies = [ "futures-core", "futures-task", @@ -1446,9 +1411,9 @@ dependencies = [ [[package]] name = "futures-intrusive" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62007592ac46aa7c2b6416f7deb9a8a8f63a01e0f1d6e1787d5630170db2b63e" +checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" dependencies = [ "futures-core", "lock_api", @@ -1457,15 +1422,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.21" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" [[package]] name = "futures-macro" -version = "0.3.21" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" dependencies = [ "proc-macro2", "quote", @@ -1474,21 +1439,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.21" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" +checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" [[package]] name = "futures-task" -version = "0.3.21" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" +checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" [[package]] name = "futures-util" -version = "0.3.21" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" dependencies = [ "futures-channel", "futures-core", @@ -1504,9 +1469,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "serde", "typenum", @@ -1535,9 +1500,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", "js-sys", @@ -1548,9 +1513,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.26.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" +checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" [[package]] name = "gloo" @@ -1597,9 +1562,9 @@ dependencies = [ [[package]] name = "gloo-timers" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" dependencies = [ "js-sys", "wasm-bindgen", @@ -1692,7 +1657,7 @@ checksum = "f290ecfa3bea3e8a157899dc8a1d96ee7dd6405c18c8ddd213fc58939d18a0e9" dependencies = [ "graphql-introspection-query", "graphql-parser 0.4.0", - "heck 0.4.0", + "heck 0.4.1", "lazy_static", "proc-macro2", "quote", @@ -1725,9 +1690,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.13" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" dependencies = [ "bytes", "fnv", @@ -1738,7 +1703,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util 0.7.3", + "tokio-util", "tracing", ] @@ -1777,9 +1742,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" dependencies = [ "unicode-segmentation", ] @@ -1793,6 +1758,15 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + [[package]] name = "hex" version = "0.4.3" @@ -1849,13 +1823,13 @@ dependencies = [ [[package]] name = "http" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes", "fnv", - "itoa 1.0.2", + "itoa", ] [[package]] @@ -1877,9 +1851,9 @@ checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" [[package]] name = "httparse" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" @@ -1889,9 +1863,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.19" +version = "0.14.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f" +checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c" dependencies = [ "bytes", "futures-channel", @@ -1902,7 +1876,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.2", + "itoa", "pin-project-lite", "socket2", "tokio", @@ -1913,15 +1887,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" dependencies = [ "http", "hyper", - "rustls 0.20.6", + "rustls", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls", ] [[package]] @@ -1965,6 +1939,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "if_chain" version = "1.0.2" @@ -1973,9 +1957,9 @@ checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" [[package]] name = "image" -version = "0.24.3" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e30ca2ecf7666107ff827a8e481de6a132a9b687ed3bb20bb1c144a36c00964" +checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" dependencies = [ "bytemuck", "byteorder", @@ -2013,60 +1997,54 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.5.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" [[package]] name = "itertools" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itoa" -version = "0.4.8" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - -[[package]] -name = "itoa" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" [[package]] name = "jobserver" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" dependencies = [ "libc", ] [[package]] name = "jpeg-decoder" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9478aa10f73e7528198d75109c8be5cd7d15fb530238040148d5f9a22d4c5b3b" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" [[package]] name = "js-sys" -version = "0.3.58" +version = "0.3.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" dependencies = [ "wasm-bindgen", ] [[package]] name = "juniper" -version = "0.15.10" +version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f478f229a8ab52ff242f3250c8b3b8fe0a59b5b934f9706b7bdbc980991a7b6" +checksum = "52adf17d43d0b526eed31fac15d9312941c5c2558ffbfb105811690b96d6e2f1" dependencies = [ "async-trait", "bson", @@ -2084,25 +2062,6 @@ dependencies = [ "uuid 0.8.2", ] -[[package]] -name = "juniper_actix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc44af18ae1f551076171e24eb453c52132a19c219d1f1a1c3068ab363b946b5" -dependencies = [ - "actix", - "actix-http", - "actix-web", - "actix-web-actors", - "anyhow", - "futures", - "http", - "juniper", - "serde", - "serde_json", - "thiserror", -] - [[package]] name = "juniper_codegen" version = "0.15.9" @@ -2121,7 +2080,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86e46349d67dc03bdbdb28da0337a355a53ca1d5156452722c36fe21d0e6389b" dependencies = [ - "base64", + "base64 0.13.1", "crypto-mac 0.10.1", "digest 0.9.0", "hmac 0.10.1", @@ -2130,6 +2089,21 @@ dependencies = [ "sha2 0.9.9", ] +[[package]] +name = "jwt" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f" +dependencies = [ + "base64 0.13.1", + "crypto-common", + "digest 0.10.6", + "hmac 0.12.1", + "serde", + "serde_json", + "sha2 0.10.6", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -2156,29 +2130,38 @@ dependencies = [ "nom 2.2.1", ] +[[package]] +name = "lber" +version = "0.4.1" +source = "git+https://github.com/inejge/ldap3/#11a66fd5c3df6ee2bae1237b93ba650a597f7805" +dependencies = [ + "bytes", + "nom 7.1.3", +] + [[package]] name = "ldap3" -version = "0.10.5" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef35dc747152dd47bdc6aaeb35a232f84cbc8d84ae4cb9673aea810a6570ab8f" +checksum = "c5cfbd3c59ca16d6671b002b8b3dd013cd825d9c77a1664a3135194d3270511e" dependencies = [ "async-trait", "bytes", "futures", "futures-util", "lazy_static", - "lber", + "lber 0.4.1", "log", - "nom 2.2.1", + "nom 7.1.3", "percent-encoding", "ring", - "rustls 0.20.6", + "rustls", "rustls-native-certs", "thiserror", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls", "tokio-stream", - "tokio-util 0.7.3", + "tokio-util", "url", "x509-parser", ] @@ -2190,39 +2173,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4162706b6f3b3d58f577990e22e9a0e03e2f9bedc2b8181d8abab2498da32003" dependencies = [ "bytes", - "lber", + "lber 0.3.0", "peg", - "tokio-util 0.7.3", + "tokio-util", "tracing", - "uuid 1.2.2", + "uuid 1.3.0", ] [[package]] name = "lettre" -version = "0.10.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eabca5e0b4d0e98e7f2243fb5b7520b6af2b65d8f87bcc86f2c75185a6ff243" +checksum = "d8033576bf9f051fce6cb92b6264114b4340896c352a9ff38b67bd4cde924635" dependencies = [ "async-trait", - "base64", + "base64 0.21.0", "email-encoding", "email_address", "fastrand", "futures-io", "futures-util", "httpdate", - "idna", + "idna 0.3.0", "mime", - "nom 7.1.1", + "nom 7.1.3", "once_cell", "quoted_printable", - "rustls 0.20.6", + "rustls", "rustls-pemfile", "serde", "socket2", "tokio", - "tokio-rustls 0.23.4", - "webpki-roots 0.22.4", + "tokio-rustls", + "webpki-roots", ] [[package]] @@ -2240,15 +2223,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.126" +version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "libm" -version = "0.2.2" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" +checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" [[package]] name = "libsqlite3-sys" @@ -2263,9 +2246,9 @@ dependencies = [ [[package]] name = "link-cplusplus" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" dependencies = [ "cc", ] @@ -2291,7 +2274,7 @@ dependencies = [ "actix-web-httpauth", "anyhow", "async-trait", - "base64", + "base64 0.13.1", "bincode", "chrono", "clap", @@ -2306,8 +2289,8 @@ dependencies = [ "image", "itertools", "juniper", - "juniper_actix", - "jwt", + "jwt 0.16.0", + "lber 0.4.1", "ldap3_proto", "lettre", "lldap_auth", @@ -2317,7 +2300,7 @@ dependencies = [ "orion", "rand 0.8.5", "reqwest", - "rustls 0.20.6", + "rustls", "rustls-pemfile", "sea-orm", "secstr", @@ -2326,19 +2309,19 @@ dependencies = [ "serde_json", "sha2 0.9.9", "thiserror", - "time 0.2.27", + "time 0.3.19", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls", "tokio-stream", - "tokio-util 0.7.3", + "tokio-util", "tracing", "tracing-actix-web", "tracing-attributes", "tracing-forest", "tracing-log", "tracing-subscriber", - "uuid 1.2.2", - "webpki-roots 0.22.4", + "uuid 1.3.0", + "webpki-roots", ] [[package]] @@ -2346,20 +2329,20 @@ name = "lldap_app" version = "0.4.2-alpha" dependencies = [ "anyhow", - "base64", + "base64 0.13.1", "chrono", "graphql_client 0.10.0", "http", "image", "indexmap", - "jwt", + "jwt 0.13.0", "lldap_auth", "rand 0.8.5", "serde", "serde_json", "url-escape", "validator", - "validator_derive", + "validator_derive 0.16.0", "wasm-bindgen", "web-sys", "yew", @@ -2377,7 +2360,7 @@ dependencies = [ "curve25519-dalek", "digest 0.9.0", "generic-array", - "getrandom 0.2.7", + "getrandom 0.2.8", "opaque-ke", "rand 0.8.5", "rust-argon2", @@ -2406,9 +2389,9 @@ checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" [[package]] name = "lock_api" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ "autocfg", "scopeguard", @@ -2434,9 +2417,9 @@ dependencies = [ [[package]] name = "matches" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "md-5" @@ -2458,7 +2441,7 @@ name = "migration-tool" version = "0.4.2-alpha" dependencies = [ "anyhow", - "base64", + "base64 0.13.1", "graphql_client 0.11.0", "ldap3", "lldap_auth", @@ -2494,45 +2477,23 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.5.3" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.7.14" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" -dependencies = [ - "libc", - "log", - "miow", - "ntapi", - "winapi", -] - -[[package]] -name = "mio" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.36.1", -] - -[[package]] -name = "miow" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", + "windows-sys 0.45.0", ] [[package]] @@ -2543,7 +2504,7 @@ checksum = "18d614ad23f9bb59119b8b5670a85c7ba92c5e9adf4385c81ea00c51c8be33d5" dependencies = [ "cfg-if", "downcast", - "fragile", + "fragile 1.2.2", "lazy_static", "mockall_derive", "predicates", @@ -2581,9 +2542,9 @@ dependencies = [ [[package]] name = "nom" -version = "7.1.1" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", @@ -2596,11 +2557,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] -name = "ntapi" -version = "0.3.7" +name = "nu-ansi-term" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ + "overload", "winapi", ] @@ -2676,46 +2638,37 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ + "hermit-abi 0.2.6", "libc", ] [[package]] name = "object" -version = "0.28.4" +version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424" +checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" dependencies = [ "memchr", ] [[package]] name = "oid-registry" -version = "0.4.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38e20717fa0541f39bd146692035c37bedfa532b3e5071b35761082407546b2a" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" dependencies = [ "asn1-rs", ] [[package]] name = "once_cell" -version = "1.12.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "opaque-debug" @@ -2726,9 +2679,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "opaque-ke" version = "0.6.1" -source = "git+https://github.com/nitnelave/opaque-ke/?branch=zeroize_1.5#308a8dfee7eb855923187d2b63d64a0aaf516304" +source = "git+https://github.com/nitnelave/opaque-ke/?branch=zeroize_1.5#2f7f3a694516a7d1be4182945bed3d910cad777a" dependencies = [ - "base64", + "base64 0.13.1", "curve25519-dalek", "digest 0.9.0", "displaydoc 0.1.7", @@ -2756,22 +2709,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6624905ddd92e460ff0685567539ed1ac985b2dee4c92c7edcd64fce905b00c" dependencies = [ "ct-codecs", - "getrandom 0.2.7", + "getrandom 0.2.8", "subtle", "zeroize", ] [[package]] name = "os_str_bytes" -version = "6.1.0" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" [[package]] name = "ouroboros" -version = "0.15.5" +version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbb50b356159620db6ac971c6d5c9ab788c9cc38a6f49619fca2a27acb062ca" +checksum = "e1358bd1558bd2a083fed428ffeda486fbfb323e698cdda7794259d592ca72db" dependencies = [ "aliasable", "ouroboros_macro", @@ -2779,9 +2732,9 @@ dependencies = [ [[package]] name = "ouroboros_macro" -version = "0.15.5" +version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0d9d1a6191c4f391f87219d1ea42b23f09ee84d64763cd05ee6ea88d9f384d" +checksum = "5f7d21ccd03305a674437ee1248f3ab5d4b1db095cf1caf49f1713ddf61956b7" dependencies = [ "Inflector", "proc-macro-error", @@ -2790,6 +2743,12 @@ dependencies = [ "syn", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.11.2" @@ -2798,7 +2757,7 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", - "parking_lot_core 0.8.5", + "parking_lot_core 0.8.6", ] [[package]] @@ -2808,14 +2767,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.3", + "parking_lot_core 0.9.7", ] [[package]] name = "parking_lot_core" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" dependencies = [ "cfg-if", "instant", @@ -2827,22 +2786,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.3" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-sys 0.36.1", + "windows-sys 0.45.0", ] [[package]] name = "paste" -version = "1.0.7" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" +checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" [[package]] name = "pear" @@ -2905,24 +2864,24 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pin-project" -version = "1.0.10" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.10" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", "quote", @@ -2965,15 +2924,15 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "predicates" @@ -2990,15 +2949,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb" +checksum = "72f883590242d3c6fc5bf50299011695fa6590c2c70eac95ee1bdb9a733ad1a2" [[package]] name = "predicates-tree" -version = "1.0.5" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032" +checksum = "54ff541861505aabf6ea722d2131ee980b8276e10a1297b94e896dd8b621850d" dependencies = [ "predicates-core", "termtree", @@ -3028,17 +2987,11 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - [[package]] name = "proc-macro2" -version = "1.0.40" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" dependencies = [ "unicode-ident", ] @@ -3058,18 +3011,18 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ "proc-macro2", ] [[package]] name = "quoted_printable" -version = "0.4.5" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fee2dce59f7a43418e3382c766554c614e06a552d53a8f07ef499ea4b332c0f" +checksum = "a24039f627d8285853cc90dcddf8c1ebfaa91f834566948872b225b9a28ed1b6" [[package]] name = "rand" @@ -3092,7 +3045,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -3112,7 +3065,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -3126,11 +3079,11 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.8", ] [[package]] @@ -3144,9 +3097,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] @@ -3157,16 +3110,16 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.8", "redox_syscall", "thiserror", ] [[package]] name = "regex" -version = "1.5.6" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" dependencies = [ "aho-corasick", "memchr", @@ -3184,9 +3137,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.26" +version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "remove_dir_all" @@ -3217,17 +3170,17 @@ dependencies = [ "bitflags", "crossterm", "once_cell", - "textwrap", + "textwrap 0.15.2", "unicode-segmentation", ] [[package]] name = "reqwest" -version = "0.11.12" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc" +checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9" dependencies = [ - "base64", + "base64 0.21.0", "bytes", "encoding_rs", "futures-core", @@ -3244,19 +3197,19 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.20.6", + "rustls", "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.22.4", + "webpki-roots", "winreg", ] @@ -3289,7 +3242,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core 0.6.3", + "rand_core 0.6.4", "smallvec", "subtle", "zeroize", @@ -3301,7 +3254,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" dependencies = [ - "base64", + "base64 0.13.1", "blake2b_simd", "constant_time_eq", "crossbeam-utils", @@ -3313,22 +3266,13 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver 0.9.0", -] - [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.12", + "semver", ] [[package]] @@ -3337,32 +3281,19 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" dependencies = [ - "nom 7.1.1", + "nom 7.1.3", ] [[package]] name = "rustls" -version = "0.19.1" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" -dependencies = [ - "base64", - "log", - "ring", - "sct 0.6.1", - "webpki 0.21.4", -] - -[[package]] -name = "rustls" -version = "0.20.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" dependencies = [ "log", "ring", - "sct 0.7.0", - "webpki 0.22.0", + "sct", + "webpki", ] [[package]] @@ -3379,33 +3310,32 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64", + "base64 0.21.0", ] [[package]] name = "rustversion" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" +checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" [[package]] name = "ryu" -version = "1.0.10" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" [[package]] name = "schannel" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" dependencies = [ - "lazy_static", - "windows-sys 0.36.1", + "windows-sys 0.42.0", ] [[package]] @@ -3416,19 +3346,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "scratch" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" - -[[package]] -name = "sct" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" -dependencies = [ - "ring", - "untrusted", -] +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" [[package]] name = "sct" @@ -3461,7 +3381,7 @@ dependencies = [ "thiserror", "tracing", "url", - "uuid 1.2.2", + "uuid 1.3.0", ] [[package]] @@ -3485,7 +3405,7 @@ checksum = "d2fbe015dbdaa7d8829d71c1e14fb6289e928ac256b93dfda543c85cd89d6f03" dependencies = [ "chrono", "sea-query-derive", - "uuid 1.2.2", + "uuid 1.3.0", ] [[package]] @@ -3497,7 +3417,7 @@ dependencies = [ "chrono", "sea-query", "sqlx", - "uuid 1.2.2", + "uuid 1.3.0", ] [[package]] @@ -3506,7 +3426,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63f62030c60f3a691f5fe251713b4e220b306e50a71e1d6f9cce1f24bb781978" dependencies = [ - "heck 0.4.0", + "heck 0.4.1", "proc-macro2", "quote", "syn", @@ -3537,9 +3457,9 @@ dependencies = [ [[package]] name = "secstr" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49fa8c1d89e7dc5e2776fbf507d8b088ff61bbaf83bf4da1cc9ed1c061358104" +checksum = "e04f657244f605c4cf38f6de5993e8bd050c8a303f86aeabff142d5c7c113e12" dependencies = [ "libc", "serde", @@ -3547,9 +3467,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.6.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" dependencies = [ "bitflags", "core-foundation", @@ -3560,9 +3480,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" dependencies = [ "core-foundation-sys", "libc", @@ -3570,48 +3490,33 @@ dependencies = [ [[package]] name = "semver" -version = "0.9.0" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1" - -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" [[package]] name = "serde" -version = "1.0.137" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" dependencies = [ "serde_derive", ] [[package]] name = "serde_bytes" -version = "0.11.7" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc50e8183eeeb6178dcb167ae34a8051d63535023ae38b5d8d12beae193d37b" +checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.137" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", @@ -3620,12 +3525,12 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.82" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" dependencies = [ "indexmap", - "itoa 1.0.2", + "itoa", "ryu", "serde", ] @@ -3637,33 +3542,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.2", + "itoa", "ryu", "serde", ] -[[package]] -name = "sha-1" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - -[[package]] -name = "sha1" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" -dependencies = [ - "sha1_smol", -] - [[package]] name = "sha1" version = "0.10.5" @@ -3675,12 +3558,6 @@ dependencies = [ "digest 0.10.6", ] -[[package]] -name = "sha1_smol" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" - [[package]] name = "sha2" version = "0.9.9" @@ -3716,9 +3593,9 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" dependencies = [ "libc", "signal-hook-registry", @@ -3731,24 +3608,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" dependencies = [ "libc", - "mio 0.8.4", + "mio", "signal-hook", ] [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] [[package]] name = "slab" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] [[package]] name = "smallvec" @@ -3758,11 +3638,13 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "smartstring" -version = "0.2.10" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e714dff2b33f2321fdcd475b71cec79781a692d846f37f415fb395a1d2bcd48e" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" dependencies = [ + "autocfg", "static_assertions", + "version_check", ] [[package]] @@ -3773,9 +3655,9 @@ checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" [[package]] name = "socket2" -version = "0.4.4" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" dependencies = [ "libc", "winapi", @@ -3789,9 +3671,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spin" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c530c2b0d0bf8b69304b39fe2001993e267461948b890cd037d8ad4293fa1a0d" +checksum = "7dccf47db1b41fa1573ed27ccf5e08e3ca771cb994f776668c5ebda893b248fc" dependencies = [ "lock_api", ] @@ -3808,12 +3690,12 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87e292b4291f154971a43c3774364e2cbcaec599d3f5bf6fa9d122885dbc38a" +checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e" dependencies = [ "itertools", - "nom 7.1.1", + "nom 7.1.3", "unicode_categories", ] @@ -3835,7 +3717,7 @@ checksum = "dcbc16ddba161afc99e14d1713a453747a2b07fc097d2009f4c300ec99286105" dependencies = [ "ahash", "atoi", - "base64", + "base64 0.13.1", "bitflags", "byteorder", "bytes", @@ -3858,7 +3740,7 @@ dependencies = [ "hkdf 0.12.3", "hmac 0.12.1", "indexmap", - "itoa 1.0.2", + "itoa", "libc", "libsqlite3-sys", "log", @@ -3870,11 +3752,11 @@ dependencies = [ "percent-encoding", "rand 0.8.5", "rsa", - "rustls 0.20.6", + "rustls", "rustls-pemfile", "serde", "serde_json", - "sha1 0.10.5", + "sha1", "sha2 0.10.6", "smallvec", "sqlformat", @@ -3883,8 +3765,8 @@ dependencies = [ "thiserror", "tokio-stream", "url", - "uuid 1.2.2", - "webpki-roots 0.22.4", + "uuid 1.3.0", + "webpki-roots", "whoami", ] @@ -3896,7 +3778,7 @@ checksum = "b850fa514dc11f2ee85be9d055c512aa866746adfacd1cb42d867d68e6a5b0d9" dependencies = [ "dotenvy", "either", - "heck 0.4.0", + "heck 0.4.1", "once_cell", "proc-macro2", "quote", @@ -3914,16 +3796,7 @@ checksum = "24c5b2d25fa654cc5f841750b8e1cdedbe21189bf9a9382ee90bfa9dd3562396" dependencies = [ "once_cell", "tokio", - "tokio-rustls 0.23.4", -] - -[[package]] -name = "standback" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" -dependencies = [ - "version_check", + "tokio-rustls", ] [[package]] @@ -3932,55 +3805,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "stdweb" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" -dependencies = [ - "discard", - "rustc_version 0.2.3", - "stdweb-derive", - "stdweb-internal-macros", - "stdweb-internal-runtime", - "wasm-bindgen", -] - -[[package]] -name = "stdweb-derive" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "serde_derive", - "syn", -] - -[[package]] -name = "stdweb-internal-macros" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" -dependencies = [ - "base-x", - "proc-macro2", - "quote", - "serde", - "serde_derive", - "serde_json", - "sha1 0.6.1", - "syn", -] - -[[package]] -name = "stdweb-internal-runtime" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" - [[package]] name = "stringprep" version = "0.1.2" @@ -4005,9 +3829,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.98" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +checksum = "d56e159d99e6c2b93995d171050271edb50ecc5288fbc7cc17de8fdce4e58c14" dependencies = [ "proc-macro2", "quote", @@ -4042,24 +3866,24 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.1.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" dependencies = [ "winapi-util", ] [[package]] name = "termtree" -version = "0.2.4" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b" +checksum = "95059e91184749cb66be6dc994f67f182b6d897cb3df74a5bf66b5e709295fd8" [[package]] name = "textwrap" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" dependencies = [ "smawk", "unicode-linebreak", @@ -4067,19 +3891,25 @@ dependencies = [ ] [[package]] -name = "thiserror" -version = "1.0.31" +name = "textwrap" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.31" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", @@ -4088,18 +3918,19 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.4" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ + "cfg-if", "once_cell", ] [[package]] name = "time" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" dependencies = [ "libc", "wasi 0.10.0+wasi-snapshot-preview1", @@ -4108,58 +3939,29 @@ dependencies = [ [[package]] name = "time" -version = "0.2.27" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" +checksum = "53250a3b3fed8ff8fd988587d8925d26a83ac3845d9e03b220b37f34c2b8d6c2" dependencies = [ - "const_fn", - "libc", - "standback", - "stdweb", - "time-macros 0.1.1", - "version_check", - "winapi", + "itoa", + "serde", + "time-core", + "time-macros", ] [[package]] -name = "time" -version = "0.3.11" +name = "time-core" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" -dependencies = [ - "itoa 1.0.2", - "libc", - "num_threads", - "time-macros 0.2.4", -] +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" [[package]] name = "time-macros" -version = "0.1.1" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +checksum = "a460aeb8de6dcb0f381e1ee05f1cd56fcf5a5f6eb8187ff3d8f0b11078d38b7c" dependencies = [ - "proc-macro-hack", - "time-macros-impl", -] - -[[package]] -name = "time-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" - -[[package]] -name = "time-macros-impl" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" -dependencies = [ - "proc-macro-hack", - "proc-macro2", - "quote", - "standback", - "syn", + "time-core", ] [[package]] @@ -4173,9 +3975,9 @@ dependencies = [ [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" @@ -4187,7 +3989,7 @@ dependencies = [ "bytes", "libc", "memchr", - "mio 0.8.4", + "mio", "num_cpus", "parking_lot 0.12.1", "pin-project-lite", @@ -4199,42 +4001,31 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.8.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "tokio-rustls" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" -dependencies = [ - "rustls 0.19.1", - "tokio", - "webpki 0.21.4", -] - [[package]] name = "tokio-rustls" version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" dependencies = [ - "rustls 0.20.6", + "rustls", "tokio", - "webpki 0.22.0", + "webpki", ] [[package]] name = "tokio-stream" -version = "0.1.9" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" +checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" dependencies = [ "futures-core", "pin-project-lite", @@ -4243,23 +4034,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.10" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "log", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" dependencies = [ "bytes", "futures-core", @@ -4271,9 +4048,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ "serde", ] @@ -4286,9 +4063,9 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.35" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "log", @@ -4299,22 +4076,21 @@ dependencies = [ [[package]] name = "tracing-actix-web" -version = "0.4.0-beta.11" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0e529f2e4537b0f71c6bb734489680f780fcb97b7419b5500565714a641a250" +checksum = "4082e4d81173e0b7ad3cfb71e9eaef0dd0cbb7b139fdb56394f488a3b0760b23" dependencies = [ "actix-web", - "futures", + "pin-project", "tracing", - "tracing-futures", - "uuid 0.8.2", + "uuid 1.3.0", ] [[package]] name = "tracing-attributes" -version = "0.1.21" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2", "quote", @@ -4323,9 +4099,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", "valuable", @@ -4333,9 +4109,9 @@ dependencies = [ [[package]] name = "tracing-forest" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db74d83f3fcda3ca1355dd91294098df02cc03d54e6cce81e40a18671c3fd7a" +checksum = "119324027fc01804d9f83aefb7d80fda2e8fbe7c28e0acc59187cbd751a12915" dependencies = [ "chrono", "smallvec", @@ -4345,16 +4121,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "pin-project", - "tracing", -] - [[package]] name = "tracing-log" version = "0.1.3" @@ -4368,13 +4134,13 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.11" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" dependencies = [ - "ansi_term", - "lazy_static", "matchers", + "nu-ansi-term", + "once_cell", "regex", "sharded-slab", "smallvec", @@ -4386,15 +4152,15 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "typenum" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "uncased" @@ -4416,51 +4182,52 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.8" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58" [[package]] name = "unicode-ident" -version = "1.0.1" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" [[package]] name = "unicode-linebreak" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f" +checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" dependencies = [ + "hashbrown 0.12.3", "regex", ] [[package]] name = "unicode-normalization" -version = "0.1.20" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dee68f85cab8cf68dec42158baf3a79a1cdc065a8b103025965d6ccb7f6cbd" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-width" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "unicode-xid" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "unicode_categories" @@ -4485,13 +4252,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.2.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", - "idna", - "matches", + "idna 0.3.0", "percent-encoding", ] @@ -4509,16 +4275,14 @@ name = "uuid" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" -dependencies = [ - "getrandom 0.2.7", -] [[package]] name = "uuid" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" +checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" dependencies = [ + "getrandom 0.2.8", "md-5", ] @@ -4528,14 +4292,14 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d0f08911ab0fee2c5009580f04615fa868898ee57de10692a45da0c3bcc3e5e" dependencies = [ - "idna", + "idna 0.2.3", "lazy_static", "regex", "serde", "serde_derive", "serde_json", "url", - "validator_types", + "validator_types 0.14.0", ] [[package]] @@ -4551,7 +4315,23 @@ dependencies = [ "quote", "regex", "syn", - "validator_types", + "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", + "validator_types 0.16.0", ] [[package]] @@ -4564,6 +4344,16 @@ dependencies = [ "syn", ] +[[package]] +name = "validator_types" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "valuable" version = "0.1.0" @@ -4618,9 +4408,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.81" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" dependencies = [ "cfg-if", "serde", @@ -4630,13 +4420,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.81" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" dependencies = [ "bumpalo", - "lazy_static", "log", + "once_cell", "proc-macro2", "quote", "syn", @@ -4645,9 +4435,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.31" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" +checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" dependencies = [ "cfg-if", "js-sys", @@ -4657,9 +4447,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.81" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4667,9 +4457,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.81" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" dependencies = [ "proc-macro2", "quote", @@ -4680,30 +4470,20 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.81" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" [[package]] name = "web-sys" -version = "0.3.58" +version = "0.3.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" +checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" dependencies = [ "js-sys", "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "webpki" version = "0.22.0" @@ -4716,27 +4496,18 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.21.1" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" dependencies = [ - "webpki 0.21.4", -] - -[[package]] -name = "webpki-roots" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" -dependencies = [ - "webpki 0.22.0", + "webpki", ] [[package]] name = "whoami" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524b58fa5a20a2fb3014dd6358b70e6579692a56ef6fce928834e488f42f65e8" +checksum = "45dbc71f0cdca27dc261a9bd37ddec174e4a0af2b900b890f378460f745426e3" dependencies = [ "wasm-bindgen", "web-sys", @@ -4773,19 +4544,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-sys" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" -dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", -] - [[package]] name = "windows-sys" version = "0.42.0" @@ -4793,12 +4551,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.42.1", - "windows_i686_gnu 0.42.1", - "windows_i686_msvc 0.42.1", - "windows_x86_64_gnu 0.42.1", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.42.1", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -4807,48 +4589,24 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" -[[package]] -name = "windows_aarch64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" - [[package]] name = "windows_aarch64_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" -[[package]] -name = "windows_i686_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" - [[package]] name = "windows_i686_gnu" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" -[[package]] -name = "windows_i686_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" - [[package]] name = "windows_i686_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" -[[package]] -name = "windows_x86_64_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" - [[package]] name = "windows_x86_64_gnu" version = "0.42.1" @@ -4861,12 +4619,6 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" -[[package]] -name = "windows_x86_64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" - [[package]] name = "windows_x86_64_msvc" version = "0.42.1" @@ -4884,20 +4636,20 @@ dependencies = [ [[package]] name = "x509-parser" -version = "0.13.2" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9bace5b5589ffead1afb76e43e34cff39cd0f3ce7e170ae0c29e53b88eb1c" +checksum = "e0ecbeb7b67ce215e40e3cc7f2ff902f94a223acf44995934763467e7b1febc8" dependencies = [ "asn1-rs", - "base64", + "base64 0.13.1", "data-encoding", "der-parser", "lazy_static", - "nom 7.1.1", + "nom 7.1.3", "oid-registry", "rusticata-macros", "thiserror", - "time 0.3.11", + "time 0.3.19", ] [[package]] @@ -4995,7 +4747,7 @@ version = "0.1.8" source = "git+https://github.com/jfbilodeau/yew_form?rev=67050812695b7a8a90b81b0637e347fc6629daed#67050812695b7a8a90b81b0637e347fc6629daed" dependencies = [ "validator", - "validator_derive", + "validator_derive 0.14.0", "yew", ] @@ -5034,9 +4786,9 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.3.2" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" +checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" dependencies = [ "proc-macro2", "quote", @@ -5046,18 +4798,18 @@ dependencies = [ [[package]] name = "zstd" -version = "0.7.0+zstd.1.4.9" +version = "0.12.3+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9428752481d8372e15b1bf779ea518a179ad6c771cca2d2c60e4fbff3cc2cd52" +checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "3.1.0+zstd.1.4.9" +version = "6.0.4+zstd.1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa1926623ad7fe406e090555387daf73db555b948134b4d73eac5eb08fb666d" +checksum = "7afb4b54b8910cf5447638cb54bf4e8a65cbedd783af98b98c62ffe91f185543" dependencies = [ "libc", "zstd-sys", @@ -5065,10 +4817,11 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "1.5.0+zstd.1.4.9" +version = "2.0.7+zstd.1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e6c094340240369025fc6b731b054ee2a834328fa584310ac96aa4baebdc465" +checksum = "94509c3ba2fe55294d752b79842c530ccfab760192521df74a081a78d2b3c7f5" dependencies = [ "cc", "libc", + "pkg-config", ] diff --git a/Cargo.toml b/Cargo.toml index 85e1dd1..f9096f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,6 @@ default-members = ["server"] [patch.crates-io.opaque-ke] git = 'https://github.com/nitnelave/opaque-ke/' branch = 'zeroize_1.5' + +[patch.crates-io.lber] +git = 'https://github.com/inejge/ldap3/' diff --git a/server/Cargo.toml b/server/Cargo.toml index 3c9d100..5ea7d83 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -5,14 +5,14 @@ name = "lldap" version = "0.4.2-alpha" [dependencies] -actix = "0.12" -actix-files = "0.6.0-beta.6" -actix-http = "=3.0.0-beta.9" -actix-rt = "2.2.0" -actix-server = "=2.0.0-beta.5" -actix-service = "2.0.0" -actix-web = "=4.0.0-beta.8" -actix-web-httpauth = "0.6.0-beta.2" +actix = "0.13" +actix-files = "0.6" +actix-http = "3" +actix-rt = "2" +actix-server = "2" +actix-service = "2" +actix-web = "4.3" +actix-web-httpauth = "0.8" anyhow = "*" async-trait = "0.1" base64 = "0.13" @@ -25,9 +25,9 @@ futures-util = "*" hmac = "0.10" http = "*" itertools = "0.10.1" -juniper = "0.15.10" -juniper_actix = "0.4.0" -jwt = "0.13" +juniper = "0.15" +jwt = "0.16" +lber = "0.4.1" ldap3_proto = ">=0.3.1" log = "*" orion = "0.16" @@ -36,12 +36,12 @@ serde = "*" serde_json = "1" sha2 = "0.9" thiserror = "*" -time = "0.2" +time = "0.3" tokio-rustls = "0.23" tokio-stream = "*" tokio-util = "0.7.3" tracing = "*" -tracing-actix-web = "0.4.0-beta.7" +tracing-actix-web = "0.7" tracing-attributes = "^0.1.21" tracing-log = "*" rustls-pemfile = "1.0.0" @@ -97,7 +97,7 @@ version = "^0.1.4" [dependencies.actix-tls] features = ["default", "rustls"] -version = "=3.0.0-beta.5" +version = "3" [dependencies.image] features = ["jpeg"] diff --git a/server/src/infra/auth_service.rs b/server/src/infra/auth_service.rs index 4b350e0..558af2b 100644 --- a/server/src/infra/auth_service.rs +++ b/server/src/infra/auth_service.rs @@ -451,14 +451,15 @@ where #[instrument(skip_all, level = "debug")] async fn opaque_register_start( request: actix_web::HttpRequest, - mut payload: actix_web::web::Payload, + payload: actix_web::web::Payload, data: web::Data>, ) -> TcpResult where Backend: BackendHandler + OpaqueHandler + 'static, { use actix_web::FromRequest; - let validation_result = BearerAuth::from_request(&request, &mut payload.0) + let inner_payload = &mut payload.into_inner(); + let validation_result = BearerAuth::from_request(&request, inner_payload) .await .ok() .and_then(|bearer| check_if_token_is_valid(&data, bearer.token()).ok()) @@ -468,7 +469,7 @@ where let registration_start_request = web::Json::::from_request( &request, - &mut payload.0, + inner_payload, ) .await .map_err(|e| TcpError::BadRequest(format!("{:#?}", e)))? diff --git a/server/src/infra/graphql/api.rs b/server/src/infra/graphql/api.rs index eda2c25..096b666 100644 --- a/server/src/infra/graphql/api.rs +++ b/server/src/infra/graphql/api.rs @@ -11,10 +11,17 @@ use crate::{ tcp_server::AppState, }, }; -use actix_web::{web, Error, HttpResponse}; +use actix_web::FromRequest; +use actix_web::HttpMessage; +use actix_web::{error::JsonPayloadError, web, Error, HttpRequest, HttpResponse}; use actix_web_httpauth::extractors::bearer::BearerAuth; -use juniper::{EmptySubscription, FieldError, RootNode}; -use juniper_actix::{graphiql_handler, graphql_handler, playground_handler}; +use juniper::{ + http::{ + graphiql::graphiql_source, playground::playground_source, GraphQLBatchRequest, + GraphQLRequest, + }, + EmptySubscription, FieldError, RootNode, ScalarValue, +}; use tracing::debug; pub struct Context { @@ -100,25 +107,129 @@ pub fn export_schema(opts: ExportGraphQLSchemaOpts) -> anyhow::Result<()> { } async fn graphiql_route() -> Result { - graphiql_handler("/api/graphql", None).await + let html = graphiql_source("/api/graphql", None); + Ok(HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(html)) } async fn playground_route() -> Result { - playground_handler("/api/graphql", None).await + let html = playground_source("/api/graphql", None); + Ok(HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(html)) +} + +#[derive(serde::Deserialize, Clone, PartialEq, Debug)] +struct GetGraphQLRequest { + query: String, + #[serde(rename = "operationName")] + operation_name: Option, + variables: Option, +} + +impl From for GraphQLRequest +where + S: ScalarValue, +{ + fn from(get_req: GetGraphQLRequest) -> Self { + let GetGraphQLRequest { + query, + operation_name, + variables, + } = get_req; + let variables = variables.map(|s| serde_json::from_str(&s).unwrap()); + Self::new(query, operation_name, variables) + } +} + +/// Actix GraphQL Handler for GET requests +pub async fn get_graphql_handler( + schema: &juniper::RootNode<'static, Query, Mutation, Subscription, S>, + context: &CtxT, + req: HttpRequest, +) -> Result +where + Query: juniper::GraphQLTypeAsync, + Query::TypeInfo: Sync, + Mutation: juniper::GraphQLTypeAsync, + Mutation::TypeInfo: Sync, + Subscription: juniper::GraphQLSubscriptionType, + Subscription::TypeInfo: Sync, + CtxT: Sync, + S: ScalarValue + Send + Sync, +{ + let get_req = web::Query::::from_query(req.query_string())?; + let req = GraphQLRequest::from(get_req.into_inner()); + let gql_response = req.execute(schema, context).await; + let body_response = serde_json::to_string(&gql_response)?; + let mut response = match gql_response.is_ok() { + true => HttpResponse::Ok(), + false => HttpResponse::BadRequest(), + }; + Ok(response + .content_type("application/json") + .body(body_response)) +} + +/// Actix GraphQL Handler for POST requests +pub async fn post_graphql_handler( + schema: &juniper::RootNode<'static, Query, Mutation, Subscription, S>, + context: &CtxT, + req: HttpRequest, + mut payload: actix_http::Payload, +) -> Result +where + Query: juniper::GraphQLTypeAsync, + Query::TypeInfo: Sync, + Mutation: juniper::GraphQLTypeAsync, + Mutation::TypeInfo: Sync, + Subscription: juniper::GraphQLSubscriptionType, + Subscription::TypeInfo: Sync, + CtxT: Sync, + S: ScalarValue + Send + Sync, +{ + let req = match req.content_type() { + "application/json" => { + let body = String::from_request(&req, &mut payload).await?; + serde_json::from_str::>(&body) + .map_err(JsonPayloadError::Deserialize) + } + "application/graphql" => { + let body = String::from_request(&req, &mut payload).await?; + Ok(GraphQLBatchRequest::Single(GraphQLRequest::new( + body, None, None, + ))) + } + _ => Err(JsonPayloadError::ContentType), + }?; + let gql_batch_response = req.execute(schema, context).await; + let gql_response = serde_json::to_string(&gql_batch_response)?; + let mut response = match gql_batch_response.is_ok() { + true => HttpResponse::Ok(), + false => HttpResponse::BadRequest(), + }; + Ok(response.content_type("application/json").body(gql_response)) } async fn graphql_route( req: actix_web::HttpRequest, - mut payload: actix_web::web::Payload, + payload: actix_web::web::Payload, data: web::Data>, ) -> Result { - use actix_web::FromRequest; - let bearer = BearerAuth::from_request(&req, &mut payload.0).await?; + let mut inner_payload = payload.into_inner(); + let bearer = BearerAuth::from_request(&req, &mut inner_payload).await?; let validation_result = check_if_token_is_valid(&data, bearer.token())?; let context = Context:: { handler: data.backend_handler.clone(), validation_result, }; - graphql_handler(&schema(), &context, req, payload).await + let schema = &schema(); + let context = &context; + match *req.method() { + actix_http::Method::POST => post_graphql_handler(schema, context, req, inner_payload).await, + actix_http::Method::GET => get_graphql_handler(schema, context, req).await, + _ => Err(actix_web::error::UrlGenerationError::ResourceNotFound.into()), + } } pub fn configure_endpoint(cfg: &mut web::ServiceConfig) diff --git a/server/src/infra/tcp_server.rs b/server/src/infra/tcp_server.rs index 7179e7d..d12951c 100644 --- a/server/src/infra/tcp_server.rs +++ b/server/src/infra/tcp_server.rs @@ -85,7 +85,10 @@ fn http_config( server_url, mail_options, })) - .route("/health", web::get().to(|| HttpResponse::Ok().finish())) + .route( + "/health", + web::get().to(|| async { HttpResponse::Ok().finish() }), + ) .service( web::scope("/auth") .configure(|cfg| auth_service::configure_server::(cfg, enable_password_reset)), @@ -165,7 +168,7 @@ where let jwt_blacklist = jwt_blacklist.clone(); let server_url = server_url.clone(); let mail_options = mail_options.clone(); - HttpServiceBuilder::new() + HttpServiceBuilder::default() .finish(map_config( App::new() .wrap(tracing_actix_web::TracingLogger::::new()) From 28607c47442325e0eed2674f5950d70cd7637458 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Thu, 23 Feb 2023 16:58:22 +0100 Subject: [PATCH 44/62] server: update various dependencies --- Cargo.lock | 41 +++++++++++++++---------- server/Cargo.toml | 18 +++++------ server/src/domain/handler.rs | 6 +++- server/src/domain/sql_opaque_handler.rs | 9 +++--- server/src/domain/types.rs | 7 +++-- server/src/infra/graphql/mutation.rs | 5 +-- server/src/infra/tcp_server.rs | 4 +-- 7 files changed, 53 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e784f0e..e6fff7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1005,9 +1005,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.12.4" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c" +checksum = "c0808e1bd8671fb44a113a14e13497557533369847788fa2ae912b6ebfce9fa8" dependencies = [ "darling_core", "darling_macro", @@ -1015,9 +1015,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.12.4" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36" +checksum = "001d80444f28e193f30c2f293455da62dcf9a6b29918a4253152ae2b1de592cb" dependencies = [ "fnv", "ident_case", @@ -1029,9 +1029,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.12.4" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a" +checksum = "b36230598a2d5de7ec1c6f51f72d8a99a9208daff41de2084d06e3fd3ea56685" dependencies = [ "darling_core", "quote", @@ -1071,18 +1071,18 @@ dependencies = [ [[package]] name = "derive_builder" -version = "0.10.2" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d13202debe11181040ae9063d739fa32cfcaaebe2275fe387703460ae2365b30" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" dependencies = [ "derive_builder_macro", ] [[package]] name = "derive_builder_core" -version = "0.10.2" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" dependencies = [ "darling", "proc-macro2", @@ -1092,9 +1092,9 @@ dependencies = [ [[package]] name = "derive_builder_macro" -version = "0.10.2" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58a94ace95092c5acb1e97a7e846b310cfbd499652f72297da7493f618a98d73" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" dependencies = [ "derive_builder_core", "syn", @@ -1272,6 +1272,12 @@ dependencies = [ "instant", ] +[[package]] +name = "fiat-crypto" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a214f5bb88731d436478f3ae1f8a277b62124089ba9fb67f4f93fb100ef73c90" + [[package]] name = "figment" version = "0.10.8" @@ -2274,7 +2280,7 @@ dependencies = [ "actix-web-httpauth", "anyhow", "async-trait", - "base64 0.13.1", + "base64 0.21.0", "bincode", "chrono", "clap", @@ -2284,7 +2290,7 @@ dependencies = [ "figment_file_provider_adapter", "futures", "futures-util", - "hmac 0.10.1", + "hmac 0.12.1", "http", "image", "itertools", @@ -2307,7 +2313,7 @@ dependencies = [ "serde", "serde_bytes", "serde_json", - "sha2 0.9.9", + "sha2 0.10.6", "thiserror", "time 0.3.19", "tokio", @@ -2704,11 +2710,12 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "orion" -version = "0.16.1" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6624905ddd92e460ff0685567539ed1ac985b2dee4c92c7edcd64fce905b00c" +checksum = "f2baf7fd2e326e3895c681176788dd227fcd8369350e53c570592d8563fecbb6" dependencies = [ "ct-codecs", + "fiat-crypto", "getrandom 0.2.8", "subtle", "zeroize", diff --git a/server/Cargo.toml b/server/Cargo.toml index 5ea7d83..94ee525 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -15,37 +15,37 @@ actix-web = "4.3" actix-web-httpauth = "0.8" anyhow = "*" async-trait = "0.1" -base64 = "0.13" +base64 = "*" bincode = "1.3" cron = "*" -derive_builder = "0.10.2" +derive_builder = "0.12" figment_file_provider_adapter = "0.1" futures = "*" futures-util = "*" -hmac = "0.10" +hmac = "0.12" http = "*" -itertools = "0.10.1" +itertools = "0.10" juniper = "0.15" jwt = "0.16" lber = "0.4.1" ldap3_proto = ">=0.3.1" log = "*" -orion = "0.16" +orion = "0.17" rustls = "0.20" serde = "*" serde_json = "1" -sha2 = "0.9" +sha2 = "0.10" thiserror = "*" time = "0.3" tokio-rustls = "0.23" tokio-stream = "*" -tokio-util = "0.7.3" +tokio-util = "0.7" tracing = "*" tracing-actix-web = "0.7" tracing-attributes = "^0.1.21" tracing-log = "*" -rustls-pemfile = "1.0.0" -serde_bytes = "0.11.7" +rustls-pemfile = "1" +serde_bytes = "0.11" webpki-roots = "*" [dependencies.chrono] diff --git a/server/src/domain/handler.rs b/server/src/domain/handler.rs index 7ba11bb..ded7f7d 100644 --- a/server/src/domain/handler.rs +++ b/server/src/domain/handler.rs @@ -211,6 +211,8 @@ mockall::mock! { #[cfg(test)] mod tests { + use base64::Engine; + use super::*; #[test] fn test_uuid_time() { @@ -233,7 +235,9 @@ mod tests { #[test] fn test_jpeg_try_from_bytes() { let base64_raw = "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////2wBDAf//////////////////////////////////////////////////////////////////////////////////////wAARCADqATkDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAECA//EACQQAQEBAAIBBAMBAQEBAAAAAAABESExQQISUXFhgZGxocHw/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAH/xAAWEQEBAQAAAAAAAAAAAAAAAAAAEQH/2gAMAwEAAhEDEQA/AMriLyCKgg1gQwCgs4FTMOdutepjQak+FzMSVqgxZdRdPPIIvH5WzzGdBriphtTeAXg2ZjKA1pqKDUGZca3foBek8gFv8Ie3fKdA1qb8s7hoL6eLVt51FsAnql3Ut1M7AWbflLMDkEMX/F6/YjK/pADFQAUNA6alYagKk72m/j9p4Bq2fDDSYKLNXPNLoHE/NT6RYC31cJxZ3yWVM+aBYi/S2ZgiAsnYJx5D21vPmqrm3PTfpQQwyAC8JZvSKDni41ZrMuUVVl+Uz9w9v/1QWrZsZ5nFPHYH+JZyureQSF5M+fJ0CAfwRAVRBQA1DAWVUayoJUWoDpsxntPsueBV4+VxhdyAtv8AjOLGpIDMLbeGvbF4iozJfr/WukAVABAXAQXEAAASzVAZdO2WNordm+emFl7XcQSNZiFtv0C9w90nhJf4mA1u+GcJFwIyAqL/AOovwgGNfSRqdIrNa29M0gKCAojU9PAMjWXpckEJFNFEAAXEUBABYz6rZ0ureQc9vyt9XxDF2QAXtABcQAs0AZywkvluJbyipifas52DcyxjlZweAO0xri/hc+wZOEKIu6nSyeToVZyWXwvCg53gW81QQ7aTNAn5dGZJPs1UXURQAUEMCXQLZE93PRZ5hPTgNMrbIzKCm52LZwCs+2M8w2g3sjPuZAXb4IsMAUACzVUGM4/K+md6vEXUUyM5PDR0IxYe6ramih0VNBrS4xoqN8Q1BFQk3yqyAsioioAAKgDSJL4/jQIn5igLrPqtOuf6oOaxbMoAltUAhhIoJiiggrPu+AaOIxtAX3JbaAIaLwi4t9X4T3fg2AFtqcrUUarP20zUDAmqoE0WRBZPNVUVEAAAAVAC8kvih2DSKxOdBqs7Z0l0gI0mKAC4AuHE7ZtBriM+744QAAAAABAFsveIttBICyaikvy1+r/Cen5rWQHIBQa4rIDRqSl5qDWqziqgAAAATA7BpGdqXb2C2+J/UgAtRQBSQtkBWb6vhLbQAAAAAEBRAAAAAUbm+GZNdPxAP+ql2Tjwx7/wIgZ8iKvBk+CJoCXii9gaqZ/qqihAAAEVABGkBFUwBftNkZ3QW34QAAABFAQAVAAAAAARVkl8gs/43sk1jL45LvHArepk+E9XTG35oLqsmIKmLAEygKg0y1AFQBUXwgAAAoBC34S3UAAABAVAAAAAABAUQAVABdRQa1PcYyit2z58M8C4ouM2NXpOEGeWtNZUatiAIoAKIoCoAoG4C9MW6dgIoAIAAAAAAACKWAgL0CAAAALiANCKioNLgM1CrLihmTafkt1EF3SZ5ZVUW4mnIKvAi5fhEURVDWVQBRAAAAAAAAQFRVyAyulgAqCKlF8IqLsEgC9mGoC+IusqCrv5ZEUVOk1RuJfwSLOOkGFi4XPCoYYrNiKauosBGi9ICstM1UAAAAAAFQ0VcTBAXUGgIqGoKhKAzRRUQUAwxoSrGRpkQA/qiosOL9oJptMRRVZa0VUqSiChE6BqMgCwqKqIogAIAqKCKgKoogg0lBFuIKgAAAKNRlf2gqsftsEtZWoAAqAACKoMqAAeSoqp39kL2AqLOlE8rEBFQARYALhigrNC9gGmooLp4TweEQFFBFAECgIoAu0ifIAqAAA//9k="; - let base64_jpeg = base64::decode(base64_raw).unwrap(); + let base64_jpeg = base64::engine::general_purpose::STANDARD + .decode(base64_raw) + .unwrap(); JpegPhoto::try_from(base64_jpeg).unwrap(); } } diff --git a/server/src/domain/sql_opaque_handler.rs b/server/src/domain/sql_opaque_handler.rs index 5a5667b..9f92b7a 100644 --- a/server/src/domain/sql_opaque_handler.rs +++ b/server/src/domain/sql_opaque_handler.rs @@ -7,6 +7,7 @@ use super::{ types::UserId, }; use async_trait::async_trait; +use base64::Engine; use lldap_auth::opaque; use sea_orm::{ActiveModelTrait, ActiveValue, EntityTrait, QuerySelect}; use secstr::SecUtf8; @@ -129,7 +130,7 @@ impl OpaqueHandler for SqlOpaqueHandler { let encrypted_state = orion::aead::seal(&secret_key, &bincode::serialize(&server_data)?)?; Ok(login::ServerLoginStartResponse { - server_data: base64::encode(encrypted_state), + server_data: base64::engine::general_purpose::STANDARD.encode(encrypted_state), credential_response: start_response.message, }) } @@ -142,7 +143,7 @@ impl OpaqueHandler for SqlOpaqueHandler { server_login, } = bincode::deserialize(&orion::aead::open( &secret_key, - &base64::decode(&request.server_data)?, + &base64::engine::general_purpose::STANDARD.decode(&request.server_data)?, )?)?; // Finish the login: this makes sure the client data is correct, and gives a session key we // don't need. @@ -170,7 +171,7 @@ impl OpaqueHandler for SqlOpaqueHandler { }; let encrypted_state = orion::aead::seal(&secret_key, &bincode::serialize(&server_data)?)?; Ok(registration::ServerRegistrationStartResponse { - server_data: base64::encode(encrypted_state), + server_data: base64::engine::general_purpose::STANDARD.encode(encrypted_state), registration_response: start_response.message, }) } @@ -183,7 +184,7 @@ impl OpaqueHandler for SqlOpaqueHandler { let secret_key = self.get_orion_secret_key()?; let registration::ServerData { username } = bincode::deserialize(&orion::aead::open( &secret_key, - &base64::decode(&request.server_data)?, + &base64::engine::general_purpose::STANDARD.decode(&request.server_data)?, )?)?; let password_file = diff --git a/server/src/domain/types.rs b/server/src/domain/types.rs index 99ee6f4..0c1d1aa 100644 --- a/server/src/domain/types.rs +++ b/server/src/domain/types.rs @@ -1,3 +1,4 @@ +use base64::Engine; use chrono::{NaiveDateTime, TimeZone}; use sea_orm::{ entity::IntoActiveValue, @@ -224,13 +225,15 @@ impl TryFrom for JpegPhoto { type Error = anyhow::Error; fn try_from(string: String) -> anyhow::Result { // The String format is in base64. - >::try_from(base64::decode(string.as_str())?) + >::try_from( + base64::engine::general_purpose::STANDARD.decode(string.as_str())?, + ) } } impl From<&JpegPhoto> for String { fn from(val: &JpegPhoto) -> Self { - base64::encode(&val.0) + base64::engine::general_purpose::STANDARD.encode(&val.0) } } diff --git a/server/src/infra/graphql/mutation.rs b/server/src/infra/graphql/mutation.rs index 5134334..dfaca57 100644 --- a/server/src/infra/graphql/mutation.rs +++ b/server/src/infra/graphql/mutation.rs @@ -12,6 +12,7 @@ use crate::{ }, }; use anyhow::Context as AnyhowContext; +use base64::Engine; use juniper::{graphql_object, FieldResult, GraphQLInputObject, GraphQLObject}; use tracing::{debug, debug_span, Instrument}; @@ -89,7 +90,7 @@ impl Mutation { let user_id = UserId::new(&user.id); let avatar = user .avatar - .map(base64::decode) + .map(|bytes| base64::engine::general_purpose::STANDARD.decode(bytes)) .transpose() .context("Invalid base64 image")? .map(JpegPhoto::try_from) @@ -146,7 +147,7 @@ impl Mutation { .ok_or_else(field_error_callback(&span, "Unauthorized user update"))?; let avatar = user .avatar - .map(base64::decode) + .map(|bytes| base64::engine::general_purpose::STANDARD.decode(bytes)) .transpose() .context("Invalid base64 image")? .map(JpegPhoto::try_from) diff --git a/server/src/infra/tcp_server.rs b/server/src/infra/tcp_server.rs index d12951c..00d131e 100644 --- a/server/src/infra/tcp_server.rs +++ b/server/src/infra/tcp_server.rs @@ -18,7 +18,7 @@ use actix_server::ServerBuilder; use actix_service::map_config; use actix_web::{dev::AppConfig, web, App, HttpResponse}; use anyhow::{Context, Result}; -use hmac::{Hmac, NewMac}; +use hmac::Hmac; use sha2::Sha512; use std::collections::HashSet; use std::path::PathBuf; @@ -80,7 +80,7 @@ fn http_config( let enable_password_reset = mail_options.enable_password_reset; cfg.app_data(web::Data::new(AppState:: { backend_handler: AccessControlledBackendHandler::new(backend_handler), - jwt_key: Hmac::new_varkey(jwt_secret.unsecure().as_bytes()).unwrap(), + jwt_key: hmac::Mac::new_from_slice(jwt_secret.unsecure().as_bytes()).unwrap(), jwt_blacklist: RwLock::new(jwt_blacklist), server_url, mail_options, From 1b91cc8ac202f0fb57653b7962cd3ade35ad1a7d Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Tue, 28 Feb 2023 09:00:45 +0100 Subject: [PATCH 45/62] server: update clap and mockall --- Cargo.lock | 157 ++++++++++++++---------- server/Cargo.toml | 4 +- server/src/infra/cli.rs | 21 ++-- server/src/infra/configuration.rs | 2 +- server/src/infra/mail.rs | 6 +- server/src/infra/tcp_backend_handler.rs | 41 ------- 6 files changed, 111 insertions(+), 120 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e6fff7b..29a0199 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -472,17 +472,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -695,26 +684,24 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.23" +version = "4.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +checksum = "ec0b0588d44d4d63a87dbd75c136c166bbfd9a86a31cb89e09906521c7d3f5e3" dependencies = [ - "atty", "bitflags", "clap_derive", "clap_lex", - "indexmap", + "is-terminal", "once_cell", "strsim", "termcolor", - "textwrap 0.16.0", ] [[package]] name = "clap_derive" -version = "3.2.18" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" +checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8" dependencies = [ "heck 0.4.1", "proc-macro-error", @@ -725,9 +712,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09" dependencies = [ "os_str_bytes", ] @@ -1125,10 +1112,10 @@ dependencies = [ ] [[package]] -name = "difference" -version = "2.0.0" +name = "difflib" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "digest" @@ -1200,9 +1187,9 @@ checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0" [[package]] name = "downcast" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb454f0228b18c7f4c3b0ebbee346ed9c52e7443b0999cd543ff3571205701d" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" [[package]] name = "either" @@ -1235,6 +1222,27 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -1313,9 +1321,9 @@ dependencies = [ [[package]] name = "float-cmp" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1267f4ac4f343772758f7b1bdcbe767c218bbab93bb432acbf5162bbf85a6c4" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" dependencies = [ "num-traits", ] @@ -1347,15 +1355,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fragile" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7464c5c4a3f014d9b2ec4073650e5c06596f385060af740fc45ad5a19f959e8" -dependencies = [ - "fragile 2.0.0", -] - [[package]] name = "fragile" version = "2.0.0" @@ -1755,15 +1754,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - [[package]] name = "hermit-abi" version = "0.2.6" @@ -1773,6 +1763,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "hex" version = "0.4.3" @@ -2001,12 +1997,34 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "io-lifetimes" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" +dependencies = [ + "libc", + "windows-sys 0.45.0", +] + [[package]] name = "ipnet" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" +[[package]] +name = "is-terminal" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.45.0", +] + [[package]] name = "itertools" version = "0.10.5" @@ -2265,6 +2283,12 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + [[package]] name = "lldap" version = "0.4.2-alpha" @@ -2504,13 +2528,13 @@ dependencies = [ [[package]] name = "mockall" -version = "0.9.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d614ad23f9bb59119b8b5670a85c7ba92c5e9adf4385c81ea00c51c8be33d5" +checksum = "50e4a1c770583dac7ab5e2f6c139153b783a53a1bbee9729613f193e59828326" dependencies = [ "cfg-if", "downcast", - "fragile 1.2.2", + "fragile", "lazy_static", "mockall_derive", "predicates", @@ -2519,9 +2543,9 @@ dependencies = [ [[package]] name = "mockall_derive" -version = "0.9.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd4234635bca06fc96c7368d038061e0aae1b00a764dc817e900dc974e3deea" +checksum = "832663583d5fa284ca8810bf7015e46c9fff9622d3cf34bd1eea5003fec06dd0" dependencies = [ "cfg-if", "proc-macro2", @@ -2943,12 +2967,13 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "predicates" -version = "1.0.8" +version = "2.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49cfaf7fdaa3bfacc6fa3e7054e65148878354a5cfddcf661df4c851f8021df" +checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" dependencies = [ - "difference", + "difflib", "float-cmp", + "itertools", "normalize-line-endings", "predicates-core", "regex", @@ -3177,7 +3202,7 @@ dependencies = [ "bitflags", "crossterm", "once_cell", - "textwrap 0.15.2", + "textwrap", "unicode-segmentation", ] @@ -3291,6 +3316,20 @@ dependencies = [ "nom 7.1.3", ] +[[package]] +name = "rustix" +version = "0.36.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.45.0", +] + [[package]] name = "rustls" version = "0.20.8" @@ -3836,9 +3875,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.108" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56e159d99e6c2b93995d171050271edb50ecc5288fbc7cc17de8fdce4e58c14" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -3897,12 +3936,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" - [[package]] name = "thiserror" version = "1.0.38" diff --git a/server/Cargo.toml b/server/Cargo.toml index 94ee525..309a4a7 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -54,7 +54,7 @@ version = "*" [dependencies.clap] features = ["std", "color", "suggestions", "derive", "env"] -version = "3.1.15" +version = "4" [dependencies.figment] features = ["env", "toml"] @@ -115,4 +115,4 @@ default-features = false features = ["rustls-tls-webpki-roots"] [dev-dependencies] -mockall = "0.9.1" +mockall = "0.11" diff --git a/server/src/infra/cli.rs b/server/src/infra/cli.rs index ab1ba42..0b373a7 100644 --- a/server/src/infra/cli.rs +++ b/server/src/infra/cli.rs @@ -1,4 +1,4 @@ -use clap::Parser; +use clap::{builder::EnumValueParser, Parser}; use lettre::message::Mailbox; use serde::{Deserialize, Serialize}; @@ -95,7 +95,7 @@ pub struct TestEmailOpts { } #[derive(Debug, Parser, Clone)] -#[clap(next_help_heading = Some("LDAPS"), setting = clap::AppSettings::DeriveDisplayOrder)] +#[clap(next_help_heading = Some("LDAPS"))] pub struct LdapsOpts { /// Enable LDAPS. Default: false. #[clap(long, env = "LLDAP_LDAPS_OPTIONS__ENABLED")] @@ -114,17 +114,16 @@ pub struct LdapsOpts { pub ldaps_key_file: Option, } -clap::arg_enum! { -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, clap::ValueEnum)] +#[serde(rename_all = "UPPERCASE")] pub enum SmtpEncryption { - NONE, - TLS, - STARTTLS, -} + None, + Tls, + StartTls, } #[derive(Debug, Parser, Clone)] -#[clap(next_help_heading = Some("SMTP"), setting = clap::AppSettings::DeriveDisplayOrder)] +#[clap(next_help_heading = Some("SMTP"))] pub struct SmtpOpts { /// Sender email address. #[clap(long, env = "LLDAP_SMTP_OPTIONS__FROM")] @@ -151,10 +150,10 @@ pub struct SmtpOpts { pub smtp_password: Option, /// Whether TLS should be used to connect to SMTP. - #[clap(long, env = "LLDAP_SMTP_OPTIONS__TLS_REQUIRED", setting=clap::ArgSettings::Hidden)] + #[clap(long, env = "LLDAP_SMTP_OPTIONS__TLS_REQUIRED", hide = true)] pub smtp_tls_required: Option, - #[clap(long, env = "LLDAP_SMTP_OPTIONS__ENCRYPTION", possible_values = SmtpEncryption::variants(), case_insensitive = true)] + #[clap(long, env = "LLDAP_SMTP_OPTIONS__ENCRYPTION", value_parser = EnumValueParser::::new(), ignore_case = true)] pub smtp_encryption: Option, } diff --git a/server/src/infra/configuration.rs b/server/src/infra/configuration.rs index 11e517c..f5e805a 100644 --- a/server/src/infra/configuration.rs +++ b/server/src/infra/configuration.rs @@ -29,7 +29,7 @@ pub struct MailOptions { pub user: String, #[builder(default = r#"SecUtf8::from("")"#)] pub password: SecUtf8, - #[builder(default = "SmtpEncryption::TLS")] + #[builder(default = "SmtpEncryption::Tls")] pub smtp_encryption: SmtpEncryption, /// Deprecated. #[builder(default = "None")] diff --git a/server/src/infra/mail.rs b/server/src/infra/mail.rs index e446f58..d71760e 100644 --- a/server/src/infra/mail.rs +++ b/server/src/infra/mail.rs @@ -27,11 +27,11 @@ async fn send_email(to: Mailbox, subject: &str, body: String, options: &MailOpti .body(body), )?; let mut mailer = match options.smtp_encryption { - SmtpEncryption::NONE => { + SmtpEncryption::None => { AsyncSmtpTransport::::builder_dangerous(&options.server) } - SmtpEncryption::TLS => AsyncSmtpTransport::::relay(&options.server)?, - SmtpEncryption::STARTTLS => { + SmtpEncryption::Tls => AsyncSmtpTransport::::relay(&options.server)?, + SmtpEncryption::StartTls => { AsyncSmtpTransport::::starttls_relay(&options.server)? } }; diff --git a/server/src/infra/tcp_backend_handler.rs b/server/src/infra/tcp_backend_handler.rs index c153a96..e01e531 100644 --- a/server/src/infra/tcp_backend_handler.rs +++ b/server/src/infra/tcp_backend_handler.rs @@ -20,44 +20,3 @@ pub trait TcpBackendHandler: Sync { async fn delete_password_reset_token(&self, token: &str) -> Result<()>; } - -#[cfg(test)] -use crate::domain::{handler::*, types::*}; -#[cfg(test)] -mockall::mock! { - pub TestTcpBackendHandler{} - impl Clone for TestTcpBackendHandler { - fn clone(&self) -> Self; - } - #[async_trait] - impl LoginHandler for TestTcpBackendHandler { - async fn bind(&self, request: BindRequest) -> Result<()>; - } - #[async_trait] - impl GroupListerBackendHandler for TestTcpBackendHandler { - async fn list_groups(&self, filters: Option) -> Result>; - } - #[async_trait] - impl GroupBackendHandler for TestTcpBackendHandler { - async fn get_group_details(&self, group_id: GroupId) -> Result; - async fn update_group(&self, request: UpdateGroupRequest) -> Result<()>; - async fn create_group(&self, group_name: &str) -> Result; - async fn delete_group(&self, group_id: GroupId) -> Result<()>; - } - #[async_trait] - impl UserListerBackendHandler for TestBackendHandler { - async fn list_users(&self, filters: Option, get_groups: bool) -> Result>; - } - #[async_trait] - impl UserBackendHandler for TestBackendHandler { - async fn get_user_details(&self, user_id: &UserId) -> Result; - async fn create_user(&self, request: CreateUserRequest) -> Result<()>; - async fn update_user(&self, request: UpdateUserRequest) -> Result<()>; - async fn delete_user(&self, user_id: &UserId) -> Result<()>; - async fn get_user_groups(&self, user_id: &UserId) -> Result>; - async fn add_user_to_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()>; - async fn remove_user_from_group(&self, user_id: &UserId, group_id: GroupId) -> Result<()>; - } - #[async_trait] - impl BackendHandler for TestTcpBackendHandler {} -} From 2593606f1638c2708a9c7db08b0190391ad125c2 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Wed, 1 Mar 2023 17:25:01 +0100 Subject: [PATCH 46/62] docs: add docs about scripting --- README.md | 37 ++++++++++++------- docs/cookie.png | Bin 0 -> 62063 bytes docs/scripting.md | 90 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 12 deletions(-) create mode 100644 docs/cookie.png create mode 100644 docs/scripting.md diff --git a/README.md b/README.md index 55ad2ad..a660011 100644 --- a/README.md +++ b/README.md @@ -269,26 +269,27 @@ folder for help with: ### vs OpenLDAP -OpenLDAP is a monster of a service that implements all of LDAP and all of its -extensions, plus some of its own. That said, if you need all that flexibility, -it might be what you need! Note that installation can be a bit painful -(figuring out how to use `slapd`) and people have mixed experiences following -tutorials online. If you don't configure it properly, you might end up storing -passwords in clear, so a breach of your server would reveal all the stored -passwords! +[OpenLDAP](https://www.openldap.org) is a monster of a service that implements +all of LDAP and all of its extensions, plus some of its own. That said, if you +need all that flexibility, it might be what you need! Note that installation +can be a bit painful (figuring out how to use `slapd`) and people have mixed +experiences following tutorials online. If you don't configure it properly, you +might end up storing passwords in clear, so a breach of your server would +reveal all the stored passwords! OpenLDAP doesn't come with a UI: if you want a web interface, you'll have to -install one (not that many that look nice) and configure it. +install one (not that many look nice) and configure it. LLDAP is much simpler to setup, has a much smaller image (10x smaller, 20x if you add PhpLdapAdmin), and comes packed with its own purpose-built web UI. +However, it's not as flexible as OpenLDAP. ### vs FreeIPA -FreeIPA is the one-stop shop for identity management: LDAP, Kerberos, NTP, DNS, -Samba, you name it, it has it. In addition to user management, it also does -security policies, single sign-on, certificate management, linux account -management and so on. +[FreeIPA](http://www.freeipa.org) is the one-stop shop for identity management: +LDAP, Kerberos, NTP, DNS, Samba, you name it, it has it. In addition to user +management, it also does security policies, single sign-on, certificate +management, linux account management and so on. If you need all of that, go for it! Keep in mind that a more complex system is more complex to maintain, though. @@ -297,6 +298,18 @@ LLDAP is much lighter to run (<10 MB RAM including the DB), easier to configure (no messing around with DNS or security policies) and simpler to use. It also comes conveniently packed in a docker container. +### vs Kanidm + +[Kanidm](https://kanidm.com) is an up-and-coming Rust identity management +platform, covering all your bases: OAuth, Linux accounts, SSH keys, Radius, +WebAuthn. It comes with a (read-only) LDAPS server. + +It's fairly easy to install and does much more; but their LDAP server is +read-only, and by having more moving parts it is inherently more complex. If +you don't need to modify the users through LDAP and you're planning on +installing something like [KeyCloak](https://www.keycloak.org) to provide +modern identity protocols, check out Kanidm. + ## I can't log in! If you just set up the server, can get to the login page but the password you diff --git a/docs/cookie.png b/docs/cookie.png new file mode 100644 index 0000000000000000000000000000000000000000..dc1ae544870aeff31a5f96a83778360cf6f49ee6 GIT binary patch literal 62063 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 literal 0 HcmV?d00001 diff --git a/docs/scripting.md b/docs/scripting.md new file mode 100644 index 0000000..af81c2b --- /dev/null +++ b/docs/scripting.md @@ -0,0 +1,90 @@ +# Scripting + +Programmatically accessing LLDAP can be done either through the LDAP protocol, +or via the GraphQL API. + +## LDAP + +Most _read-only_ queries about users and groups are supported. Anything not +supported would be considered a missing feature or a bug. + +Most _modification_ queries are not supported, except for creating users and +changing the password (through the extended password operation). Those could be +added in the future, on a case-by-case basis. + +Most _meta_-queries about the LDAP server itself are not supported and are out +of scope. That includes anything that touches the schema, for instance. LLDAP +still supports basic RootDSE queries. + +Anonymous bind is not supported. + +## GraphQL + +The best way to interact with LLDAP programmatically is via the GraphQL +interface. You can use any language that has a GraphQL library (most of them +do), and use the [GraphQL Schema](../schema.graphql) to guide your queries. + +### Getting a token + +You'll need a JWT (authentication token) to issue GraphQL queries. Your view of +the system will be limited by the rights of your user. In particular, regular +users can only see themselves and the groups they belong to (but not other +members of these groups, for instance). + +#### Manually + +Log in to the web front-end of LLDAP. Then open the developer tools (F12), find +the "Storage > Cookies", and you'll find the "token" cookie with your JWT. + +![Cookies menu with a JWT](cookie.png) + +#### Automatically + +The easiest way is to send a json POST request to `/auth/simple/login` with +`{"username": "john", "password": "1234"}` in the body. +Then you'll receive a JSON response with: + +``` +{ + "token": "eYbat...", + "refreshToken": "3bCka...", +} +``` + +### Using the token + +You can use the token directly, either as a cookie, or as a bearer auth token +(add an "Authorization" header with contents `"Bearer "`). + +The JWT is valid for 1 day (unless you log out explicitly). +You can use the refresh token to query `/auth/refresh` and get another JWT. The +refresh token is valid for 30 days. + +### Testing your GraphQL queries + +You can go to `/api/graphql/playground` to test your queries and explore the +data in the playground. You'll need to provide the JWT in the headers: + +``` +{ "Authorization": "Bearer abcdef123..." } +``` + +Then you can enter your query, for instance: + +```graphql +{ + user(userId:"admin") { + displayName + } + groups { + id + displayName + users { + id + email + } + } +} +``` + +The schema is on the right, along with some basic docs. From 9e479d38fe602094171ba5a87ab688ce27f492fd Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Sun, 5 Mar 2023 12:45:30 +0100 Subject: [PATCH 47/62] app: get rid of rollup, gzip the wasm --- .github/workflows/Dockerfile.dev | 13 +++++-------- Cargo.toml | 6 ++++++ Dockerfile | 3 +-- app/build.sh | 20 +++++-------------- app/index.html | 2 +- app/index_local.html | 2 +- app/src/infra/modal.rs | 10 +++++----- app/{ => static}/main.js | 2 +- server/src/infra/tcp_server.rs | 33 ++++++++++++++++++++++++-------- 9 files changed, 50 insertions(+), 41 deletions(-) rename app/{ => static}/main.js (62%) diff --git a/.github/workflows/Dockerfile.dev b/.github/workflows/Dockerfile.dev index 7bca4e4..8e23543 100644 --- a/.github/workflows/Dockerfile.dev +++ b/.github/workflows/Dockerfile.dev @@ -6,20 +6,17 @@ ENV PATH="/opt/aarch64-linux-musl-cross/:/opt/aarch64-linux-musl-cross/bin/:/opt ### Install build deps x86_64 RUN apt update && \ - apt install -y --no-install-recommends curl git wget build-essential make perl pkg-config curl tar jq musl-tools && \ + apt install -y --no-install-recommends curl git wget build-essential make perl pkg-config curl tar jq musl-tools gzip && \ curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && \ apt update && \ apt install -y --no-install-recommends nodejs && \ apt clean && \ - rm -rf /var/lib/apt/lists/* && \ - npm install -g npm && \ - npm install -g yarn && \ - npm install -g pnpm + rm -rf /var/lib/apt/lists/* ### Install build deps aarch64 build RUN dpkg --add-architecture arm64 && \ apt update && \ - apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libc6-arm64-cross libc6-dev-arm64-cross && \ + apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libc6-arm64-cross libc6-dev-arm64-cross gzip && \ apt clean && \ rm -rf /var/lib/apt/lists/* && \ rustup target add aarch64-unknown-linux-gnu @@ -27,7 +24,7 @@ RUN dpkg --add-architecture arm64 && \ ### armhf deps RUN dpkg --add-architecture armhf && \ apt update && \ - apt install -y gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf libc6-armhf-cross libc6-dev-armhf-cross && \ + apt install -y gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf libc6-armhf-cross libc6-dev-armhf-cross gzip && \ apt clean && \ rm -rf /var/lib/apt/lists/* && \ rustup target add armv7-unknown-linux-gnueabihf @@ -43,6 +40,6 @@ RUN wget -c https://musl.cc/x86_64-linux-musl-cross.tgz && \ ### Add musl target RUN rustup target add x86_64-unknown-linux-musl && \ rustup target add aarch64-unknown-linux-musl - + CMD ["bash"] diff --git a/Cargo.toml b/Cargo.toml index f9096f4..2c6830c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,12 @@ members = [ default-members = ["server"] +[profile.release] +lto = true + +[profile.release.package.lldap_app] +opt-level = 's' + [patch.crates-io.opaque-ke] git = 'https://github.com/nitnelave/opaque-ke/' branch = 'zeroize_1.5' diff --git a/Dockerfile b/Dockerfile index c82aff3..4407e30 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN set -x \ --uid 10001 \ app \ # Install required packages - && apk add npm openssl-dev musl-dev make perl curl + && apk add openssl-dev musl-dev make perl curl gzip USER app WORKDIR /app @@ -19,7 +19,6 @@ WORKDIR /app RUN set -x \ # Install build tools && RUSTFLAGS=-Ctarget-feature=-crt-static cargo install wasm-pack cargo-chef \ - && npm install rollup \ && rustup target add wasm32-unknown-unknown # Prepare the dependency list. diff --git a/app/build.sh b/app/build.sh index 10ef9f3..dde5d3f 100755 --- a/app/build.sh +++ b/app/build.sh @@ -6,22 +6,12 @@ then >&2 echo '`wasm-pack` not found. Try running `cargo install wasm-pack`' exit 1 fi - -wasm-pack build --target web - -ROLLUP_BIN=$(which rollup 2>/dev/null) -if [ -f ../node_modules/rollup/dist/bin/rollup ] +if ! which gzip > /dev/null 2>&1 then - ROLLUP_BIN=../node_modules/rollup/dist/bin/rollup -elif [ -f node_modules/rollup/dist/bin/rollup ] -then - ROLLUP_BIN=node_modules/rollup/dist/bin/rollup -fi - -if [ -z "$ROLLUP_BIN" ] -then - >&2 echo '`rollup` not found. Try running `npm install rollup`' + >&2 echo '`gzip` not found.' exit 1 fi -$ROLLUP_BIN ./main.js --format iife --file ./pkg/bundle.js --globals bootstrap:bootstrap +wasm-pack build --target web --release + +gzip -9 -f pkg/lldap_app_bg.wasm diff --git a/app/index.html b/app/index.html index ea3b6fc..cbe820c 100644 --- a/app/index.html +++ b/app/index.html @@ -4,7 +4,7 @@ LLDAP Administration - + LLDAP Administration - + Modal; - #[wasm_bindgen(method)] + #[wasm_bindgen(method, js_namespace = bootstrap)] pub fn show(this: &Modal); - #[wasm_bindgen(method)] + #[wasm_bindgen(method, js_namespace = bootstrap)] pub fn hide(this: &Modal); } diff --git a/app/main.js b/app/static/main.js similarity index 62% rename from app/main.js rename to app/static/main.js index 8ab5e77..4dc13f5 100644 --- a/app/main.js +++ b/app/static/main.js @@ -1,4 +1,4 @@ -import init, { run_app } from './pkg/lldap_app.js'; +import init, { run_app } from '/pkg/lldap_app.js'; async function main() { await init('/pkg/lldap_app_bg.wasm'); run_app(); diff --git a/server/src/infra/tcp_server.rs b/server/src/infra/tcp_server.rs index 00d131e..c20358d 100644 --- a/server/src/infra/tcp_server.rs +++ b/server/src/infra/tcp_server.rs @@ -13,10 +13,10 @@ use crate::{ }, }; use actix_files::{Files, NamedFile}; -use actix_http::HttpServiceBuilder; +use actix_http::{header, HttpServiceBuilder}; use actix_server::ServerBuilder; use actix_service::map_config; -use actix_web::{dev::AppConfig, web, App, HttpResponse}; +use actix_web::{dev::AppConfig, guard, middleware, web, App, HttpResponse, Responder}; use anyhow::{Context, Result}; use hmac::Hmac; use sha2::Sha512; @@ -67,6 +67,16 @@ pub(crate) fn error_to_http_response(error: TcpError) -> HttpResponse { .body(error.to_string()) } +async fn wasm_handler() -> actix_web::Result { + Ok( + actix_files::NamedFile::open_async("./app/pkg/lldap_app_bg.wasm.gz") + .await? + .customize() + .insert_header(header::ContentEncoding::Gzip) + .insert_header((header::CONTENT_TYPE, "application/wasm")), + ) +} + fn http_config( cfg: &mut web::ServiceConfig, backend_handler: Backend, @@ -99,6 +109,13 @@ fn http_config( .wrap(auth_service::CookieToHeaderTranslatorFactory) .configure(super::graphql::api::configure_endpoint::), ) + .service( + web::resource("/pkg/lldap_app_bg.wasm").route( + web::route() + .wrap(middleware::Compress::default()) + .to(wasm_handler), + ), + ) // Serve the /pkg path with the compiled WASM app. .service(Files::new("/pkg", "./app/pkg")) // Serve static files @@ -106,11 +123,7 @@ fn http_config( // Serve static fonts .service(Files::new("/static/fonts", "./app/static/fonts")) // Default to serve index.html for unknown routes, to support routing. - .service( - web::scope("/") - .route("", web::get().to(index)) // this is necessary because the below doesn't match a request for "/" - .route(".*", web::get().to(index)), - ); + .default_service(web::route().guard(guard::Get()).to(index)); } pub(crate) struct AppState { @@ -157,6 +170,7 @@ where .context("while getting the jwt blacklist")?; let server_url = config.http_url.clone(); let mail_options = config.smtp_options.clone(); + let verbose = config.verbose; info!("Starting the API/web server on port {}", config.http_port); server_builder .bind( @@ -171,7 +185,10 @@ where HttpServiceBuilder::default() .finish(map_config( App::new() - .wrap(tracing_actix_web::TracingLogger::::new()) + .wrap(actix_web::middleware::Condition::new( + verbose, + tracing_actix_web::TracingLogger::::new(), + )) .configure(move |cfg| { http_config( cfg, From 9e038f52185a690f6ac3bb3185c79686e69b2e67 Mon Sep 17 00:00:00 2001 From: Dedy Martadinata S Date: Fri, 17 Mar 2023 22:23:53 +0700 Subject: [PATCH 48/62] docker: use correct username for chown --- .github/workflows/Dockerfile.ci.alpine | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Dockerfile.ci.alpine b/.github/workflows/Dockerfile.ci.alpine index 504a2e4..ef610a6 100644 --- a/.github/workflows/Dockerfile.ci.alpine +++ b/.github/workflows/Dockerfile.ci.alpine @@ -98,8 +98,8 @@ RUN apk add --no-cache tini ca-certificates bash tzdata && \ "$USER" && \ mkdir -p /data && \ chown $USER:$USER /data -COPY --from=lldap --chown=$CONTAINERUSER:$CONTAINERUSER /lldap /app -COPY --from=lldap --chown=$CONTAINERUSER:$CONTAINERUSER /docker-entrypoint.sh /docker-entrypoint.sh +COPY --from=lldap --chown=$USER:$USER /lldap /app +COPY --from=lldap --chown=$USER:$USER /docker-entrypoint.sh /docker-entrypoint.sh VOLUME ["/data"] WORKDIR /app ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"] From c817b31dfc8bbdd6a3555c61180731b45a5a6ca6 Mon Sep 17 00:00:00 2001 From: Austin Alvarado Date: Fri, 17 Mar 2023 10:49:24 -0600 Subject: [PATCH 49/62] docs: Add DB migration docs --- docs/database_migration.md | 57 +++++++++++++++++++++++++++++++ lldap_config.docker_template.toml | 10 +++--- 2 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 docs/database_migration.md diff --git a/docs/database_migration.md b/docs/database_migration.md new file mode 100644 index 0000000..0d8259f --- /dev/null +++ b/docs/database_migration.md @@ -0,0 +1,57 @@ +# Migration + +Existing servers can migrate from one database backend to another. This page includes guidance for migrating from SQLite - similar concepts apply when migrating from databases of other types. + +NOTE: [pgloader](https://github.com/dimitri/pgloader) is a tool that can easily migrate to PostgreSQL from other databases. Consider it if your target database is PostgreSQL + +The process is as follows: + +1. Create a dump of existing data. +2. Change all `CREATE TABLE ...` lines to `DELETE FROM tablename;`. We will later have LLDAP create the schema for us, so we want to clear out existing data to replace it with the original data. +3. Do any syntax fixes for the target db syntax +4. Change your LLDAP config database_url to point to the new target and restart. +5. After LLDAP has started, stop it. +6. Execute the manicured dump file against the new database. + +The steps below assume you already have PostgreSQL or MySQL set up with an empty database for LLDAP to use. + +## Create a dump + +First, we must dump the existing data to a file. The dump must be tweaked slightly according to your target db. See below for commands + +### PostgreSQL + +PostgreSQL uses a different hex string format and doesn't support `PRAGMA`. + +``` +sqlite3 /path/to/lldap/config/users.db .dump | \ +sed -r -e "s/X'([[:xdigit:]]+'[^'])/'\\\x\\1/g" \ +-e 's/^CREATE TABLE IF NOT EXISTS "([^"]*)".*/DELETE FROM \1;/' \ +-e '/^PRAGMA.*/d' > /path/to/dump.sql +``` + +### MySQL + +MySQL doesn't support `PRAGMA`. + +``` +sqlite3 /path/to/lldap/config/users.db .dump | \ +-e 's/^CREATE TABLE IF NOT EXISTS "([^"]*)".*/DELETE FROM \1;/' \ +-e '/^PRAGMA.*/d' > /path/to/dump.sql +``` + +## Generate New Schema + +Modify your `database_url` in `lldap_config.toml` (or `LLDAP_DATABASE_URL` in the env) to point to your new database. Restart LLDAP and check the logs to ensure there were no errors connecting and creating the tables. After that, stop LLDAP. Now we can import our original data! + +### PostgreSQL + +`psql -d -U -W < /path/to/dump.sql` + +### MySQL + +`mysql -u < -p < /path/to/dump.sql` + +## Finish + +If all succeeds, you're all set to start LLDAP with your new database! \ No newline at end of file diff --git a/lldap_config.docker_template.toml b/lldap_config.docker_template.toml index 02e2aac..a27f9d3 100644 --- a/lldap_config.docker_template.toml +++ b/lldap_config.docker_template.toml @@ -75,16 +75,16 @@ #ldap_user_pass = "REPLACE_WITH_PASSWORD" ## Database URL. -## This encodes the type of database (SQlite, Mysql and so -## on), the path, the user, password, and sometimes the mode (when +## This encodes the type of database (SQlite, MySQL, or PostgreSQL) +## , the path, the user, password, and sometimes the mode (when ## relevant). -## Note: Currently, only SQlite is supported. SQlite should come with -## "?mode=rwc" to create the DB if not present. +## Note: SQlite should come with "?mode=rwc" to create the DB +## if not present. ## Example URLs: ## - "postgres://postgres-user:password@postgres-server/my-database" ## - "mysql://mysql-user:password@mysql-server/my-database" ## -## This can be overridden with the DATABASE_URL env variable. +## This can be overridden with the LLDAP_DATABASE_URL env variable. database_url = "sqlite:///data/users.db?mode=rwc" ## Private key file. From 313fe3e0b7f9411d9d9a28a7d1829719179d3742 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Fri, 17 Mar 2023 17:59:25 +0100 Subject: [PATCH 50/62] clippy: fix new warning --- app/src/components/change_password.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/src/components/change_password.rs b/app/src/components/change_password.rs index eabec53..dd43369 100644 --- a/app/src/components/change_password.rs +++ b/app/src/components/change_password.rs @@ -16,19 +16,14 @@ use yew_router::{ route::Route, }; -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Default)] enum OpaqueData { + #[default] None, Login(opaque::client::login::ClientLogin), Registration(opaque::client::registration::ClientRegistration), } -impl Default for OpaqueData { - fn default() -> Self { - OpaqueData::None - } -} - impl OpaqueData { fn take(&mut self) -> Self { std::mem::take(self) From 7f76e2095dfd650bd2756f4e64b8412b7cdce098 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Mar 2023 20:59:23 +0000 Subject: [PATCH 51/62] build(deps): bump actions/checkout from 3.3.0 to 3.4.0 Bumps [actions/checkout](https://github.com/actions/checkout) from 3.3.0 to 3.4.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3.3.0...v3.4.0) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/docker-build-static.yml | 6 +++--- .github/workflows/rust.yml | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/docker-build-static.yml b/.github/workflows/docker-build-static.yml index c7a4604..735174f 100644 --- a/.github/workflows/docker-build-static.yml +++ b/.github/workflows/docker-build-static.yml @@ -66,7 +66,7 @@ jobs: image: nitnelave/rust-dev:latest steps: - name: Checkout repository - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v3.4.0 - uses: actions/cache@v3 with: path: | @@ -113,7 +113,7 @@ jobs: CARGO_HOME: ${GITHUB_WORKSPACE}/.cargo steps: - name: Checkout repository - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v3.4.0 - uses: actions/cache@v3 with: path: | @@ -217,7 +217,7 @@ jobs: packages: write steps: - name: Checkout repository - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v3.4.0 - name: Download all artifacts uses: actions/download-artifact@v3 with: diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a5952d1..2d779e9 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -34,7 +34,7 @@ jobs: steps: - name: Checkout sources - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v3.4.0 - uses: Swatinem/rust-cache@v2 - name: Build run: cargo build --verbose --workspace @@ -53,7 +53,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v3.4.0 - uses: Swatinem/rust-cache@v2 @@ -70,7 +70,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v3.4.0 - uses: Swatinem/rust-cache@v2 @@ -87,7 +87,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3.3.0 + uses: actions/checkout@v3.4.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 From 07523219d1f9981cb4b89fbf0efac8d5e6f523d8 Mon Sep 17 00:00:00 2001 From: amiga23 Date: Sat, 18 Mar 2023 00:07:40 +0100 Subject: [PATCH 52/62] docs(dex): Fix group search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The userAttr needs to be the full DN, otherwise the search does not work: ``` ❯ ldapsearch -x -H ldap://localhost:3890 -D "cn=admin,ou=people,dc=example,dc=com" -b "ou=groups,dc=example,dc=com" -W "member=bob" Enter LDAP Password: # extended LDIF # # LDAPv3 # base with scope subtree # filter: member=bob # requesting: ALL # # search result search: 2 result: 53 Server is unwilling to perform text: Unsupported group filter: while parsing a user ID: Missing DN value # numResponses: 1 ``` --- example_configs/dex_config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example_configs/dex_config.yml b/example_configs/dex_config.yml index 0c566ec..93bbf9c 100644 --- a/example_configs/dex_config.yml +++ b/example_configs/dex_config.yml @@ -27,6 +27,6 @@ connectors: baseDN: ou=groups,dc=example,dc=com filter: "(objectClass=groupOfUniqueNames)" userMatchers: - - userAttr: uid + - userAttr: DN groupAttr: member - nameAttr: displayName + nameAttr: cn From f44e8b76596be3a32167797b9820f171eb6da34c Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Wed, 8 Mar 2023 14:27:47 +0100 Subject: [PATCH 53/62] app: wrap template arguments in braces To prepare for the migration to yew 1.19 --- app/src/components/add_group_member.rs | 9 +++--- app/src/components/add_user_to_group.rs | 9 +++--- app/src/components/app.rs | 25 ++++++++------- app/src/components/change_password.rs | 19 +++++------ app/src/components/create_group.rs | 9 +++--- app/src/components/create_user.rs | 33 ++++++++++---------- app/src/components/delete_group.rs | 16 +++++----- app/src/components/delete_user.rs | 16 +++++----- app/src/components/group_details.rs | 18 +++++------ app/src/components/group_table.rs | 10 +++--- app/src/components/login.rs | 15 ++++----- app/src/components/logout.rs | 3 +- app/src/components/remove_user_from_group.rs | 5 +-- app/src/components/reset_password_step1.rs | 13 ++++---- app/src/components/reset_password_step2.rs | 17 +++++----- app/src/components/select.rs | 8 ++--- app/src/components/user_details.rs | 27 ++++++++-------- app/src/components/user_details_form.rs | 25 ++++++++------- app/src/components/user_table.rs | 11 ++++--- 19 files changed, 153 insertions(+), 135 deletions(-) diff --git a/app/src/components/add_group_member.rs b/app/src/components/add_group_member.rs index 61b9938..0ac4e23 100644 --- a/app/src/components/add_group_member.rs +++ b/app/src/components/add_group_member.rs @@ -149,18 +149,19 @@ impl Component for AddGroupMemberComponent { } fn view(&self) -> Html { + let link = &self.common; if let Some(user_list) = &self.user_list { let to_add_user_list = self.get_selectable_user_list(user_list); #[allow(unused_braces)] let make_select_option = |user: User| { html_nested! { - + } }; html! {

- { to_add_user_list .into_iter() @@ -172,8 +173,8 @@ impl Component for AddGroupMemberComponent {
diff --git a/app/src/components/add_user_to_group.rs b/app/src/components/add_user_to_group.rs index 3d7d3f0..1130ffb 100644 --- a/app/src/components/add_user_to_group.rs +++ b/app/src/components/add_user_to_group.rs @@ -162,18 +162,19 @@ impl Component for AddUserToGroupComponent { } fn view(&self) -> Html { + let link = &self.common; if let Some(group_list) = &self.group_list { let to_add_group_list = self.get_selectable_group_list(group_list); #[allow(unused_braces)] let make_select_option = |group: Group| { html_nested! { - + } }; html! {
- { to_add_group_list .into_iter() @@ -185,8 +186,8 @@ impl Component for AddUserToGroupComponent {
diff --git a/app/src/components/app.rs b/app/src/components/app.rs index e20fed7..69ef406 100644 --- a/app/src/components/app.rs +++ b/app/src/components/app.rs @@ -128,7 +128,7 @@ impl Component for App {
- render = Router::render(move |s| Self::dispatch_route(s, &link, is_admin, password_reset_enabled)) + render={Router::render(move |s| Self::dispatch_route(s, &link, is_admin, password_reset_enabled))} />
@@ -198,7 +198,7 @@ impl App { ) -> Html { match switch { AppRoute::Login => html! { - + }, AppRoute::CreateUser => html! { @@ -206,7 +206,7 @@ impl App { AppRoute::Index | AppRoute::ListUsers => html! {
- + {"Create a user"} @@ -218,20 +218,20 @@ impl App { AppRoute::ListGroups => html! {
- + {"Create a group"}
}, AppRoute::GroupDetails(group_id) => html! { - + }, AppRoute::UserDetails(username) => html! { - + }, AppRoute::ChangePassword(username) => html! { - + }, AppRoute::StartResetPassword => match password_reset_enabled { Some(true) => html! { }, @@ -242,7 +242,7 @@ impl App { None => html! {}, }, AppRoute::FinishResetPassword(token) => match password_reset_enabled { - Some(true) => html! { }, + Some(true) => html! { }, Some(false) => { App::dispatch_route(AppRoute::Login, link, is_admin, password_reset_enabled) } @@ -252,6 +252,7 @@ impl App { } fn view_banner(&self) -> Html { + let link = &self.link; html! {
@@ -266,7 +267,7 @@ impl App {
  • + route={AppRoute::ListUsers}> {"Users"} @@ -274,7 +275,7 @@ impl App {
  • + route={AppRoute::ListGroups}> {"Groups"} @@ -312,13 +313,13 @@ impl App {
  • + route={AppRoute::UserDetails(user_id.clone())}> {"View details"}
  • - +
  • diff --git a/app/src/components/change_password.rs b/app/src/components/change_password.rs index dd43369..6b08822 100644 --- a/app/src/components/change_password.rs +++ b/app/src/components/change_password.rs @@ -212,6 +212,7 @@ impl Component for ChangePasswordForm { fn view(&self) -> Html { let is_admin = self.common.is_admin; + let link = &self.common; type Field = yew_form::Field; html! { <> @@ -239,14 +240,14 @@ impl Component for ChangePasswordForm {
    + oninput={link.callback(|_| Msg::FormUpdate)} />
    {&self.form.field_message("old_password")}
    @@ -262,14 +263,14 @@ impl Component for ChangePasswordForm {
    + oninput={link.callback(|_| Msg::FormUpdate)} />
    {&self.form.field_message("password")}
    @@ -284,14 +285,14 @@ impl Component for ChangePasswordForm {
    + oninput={link.callback(|_| Msg::FormUpdate)} />
    {&self.form.field_message("confirm_password")}
    @@ -301,14 +302,14 @@ impl Component for ChangePasswordForm { + route={AppRoute::UserDetails(self.common.username.clone())}> {"Back"} diff --git a/app/src/components/create_group.rs b/app/src/components/create_group.rs index d3e9b45..8dcafec 100644 --- a/app/src/components/create_group.rs +++ b/app/src/components/create_group.rs @@ -97,6 +97,7 @@ impl Component for CreateGroupForm { } fn view(&self) -> Html { + let link = &self.common; type Field = yew_form::Field; html! {
    @@ -113,13 +114,13 @@ impl Component for CreateGroupForm {
    + oninput={link.callback(|_| Msg::Update)} />
    {&self.form.field_message("groupname")}
    @@ -129,8 +130,8 @@ impl Component for CreateGroupForm { diff --git a/app/src/components/create_user.rs b/app/src/components/create_user.rs index b7ece1c..ba39b97 100644 --- a/app/src/components/create_user.rs +++ b/app/src/components/create_user.rs @@ -190,6 +190,7 @@ impl Component for CreateUserForm { } fn view(&self) -> Html { + let link = &self.common; type Field = yew_form::Field; html! {
    @@ -206,13 +207,13 @@ impl Component for CreateUserForm {
    + oninput={link.callback(|_| Msg::Update)} />
    {&self.form.field_message("username")}
    @@ -227,14 +228,14 @@ impl Component for CreateUserForm {
    + oninput={link.callback(|_| Msg::Update)} />
    {&self.form.field_message("email")}
    @@ -247,13 +248,13 @@ impl Component for CreateUserForm {
    + oninput={link.callback(|_| Msg::Update)} />
    {&self.form.field_message("display_name")}
    @@ -266,13 +267,13 @@ impl Component for CreateUserForm {
    + oninput={link.callback(|_| Msg::Update)} />
    {&self.form.field_message("first_name")}
    @@ -285,13 +286,13 @@ impl Component for CreateUserForm {
    + oninput={link.callback(|_| Msg::Update)} />
    {&self.form.field_message("last_name")}
    @@ -304,14 +305,14 @@ impl Component for CreateUserForm {
    + oninput={link.callback(|_| Msg::Update)} />
    {&self.form.field_message("password")}
    @@ -324,14 +325,14 @@ impl Component for CreateUserForm {
    + oninput={link.callback(|_| Msg::Update)} />
    {&self.form.field_message("confirm_password")}
    @@ -340,9 +341,9 @@ impl Component for CreateUserForm {
    diff --git a/app/src/components/delete_group.rs b/app/src/components/delete_group.rs index 881f55f..0934976 100644 --- a/app/src/components/delete_group.rs +++ b/app/src/components/delete_group.rs @@ -109,12 +109,13 @@ impl Component for DeleteGroup { } fn view(&self) -> Html { + let link = &self.common; html! { <> {self.show_modal()} @@ -125,14 +126,15 @@ impl Component for DeleteGroup { impl DeleteGroup { fn show_modal(&self) -> Html { + let link = &self.common; html! {
    } } + fn view_user_menu(&self, ctx: &Context) -> Html { + if let Some((user_id, _)) = &self.user_info { + let link = ctx.link(); + html! { + + } + } else { + html! {} + } + } + fn view_footer(&self) -> Html { html! {
    diff --git a/app/src/components/change_password.rs b/app/src/components/change_password.rs index 1e65046..d6d59d3 100644 --- a/app/src/components/change_password.rs +++ b/app/src/components/change_password.rs @@ -1,21 +1,18 @@ use crate::{ - components::router::{AppRoute, NavButton}, + components::router::{AppRoute, Link}, infra::{ api::HostService, common_component::{CommonComponent, CommonComponentParts}, }, }; -use anyhow::{anyhow, bail, Context, Result}; +use anyhow::{anyhow, bail, Result}; use gloo_console::error; use lldap_auth::*; use validator_derive::Validate; use yew::prelude::*; use yew_form::Form; use yew_form_derive::Model; -use yew_router::{ - agent::{RouteAgentDispatcher, RouteRequest}, - route::Route, -}; +use yew_router::{prelude::History, scope_ext::RouterScopeExt}; #[derive(PartialEq, Eq, Default)] enum OpaqueData { @@ -57,7 +54,6 @@ pub struct ChangePasswordForm { common: CommonComponentParts, form: Form, opaque_data: OpaqueData, - route_dispatcher: RouteAgentDispatcher, } #[derive(Clone, PartialEq, Eq, Properties)] @@ -76,15 +72,20 @@ pub enum Msg { } impl CommonComponent for ChangePasswordForm { - fn handle_msg(&mut self, msg: ::Message) -> Result { + fn handle_msg( + &mut self, + ctx: &Context, + msg: ::Message, + ) -> Result { + use anyhow::Context; match msg { Msg::FormUpdate => Ok(true), Msg::Submit => { if !self.form.validate() { bail!("Check the form for errors"); } - if self.common.is_admin { - self.handle_msg(Msg::SubmitNewPassword) + if ctx.props().is_admin { + self.handle_msg(ctx, Msg::SubmitNewPassword) } else { let old_password = self.form.model().old_password; if old_password.is_empty() { @@ -96,14 +97,14 @@ impl CommonComponent for ChangePasswordForm { .context("Could not initialize login")?; self.opaque_data = OpaqueData::Login(login_start_request.state); let req = login::ClientLoginStartRequest { - username: self.common.username.clone(), + username: ctx.props().username.clone(), login_start_request: login_start_request.message, }; self.common.call_backend( - HostService::login_start, - req, + ctx, + HostService::login_start(req), Msg::AuthenticationStartResponse, - )?; + ); Ok(true) } } @@ -122,7 +123,7 @@ impl CommonComponent for ChangePasswordForm { } _ => panic!("Unexpected data in opaque_data field"), }; - self.handle_msg(Msg::SubmitNewPassword) + self.handle_msg(ctx, Msg::SubmitNewPassword) } Msg::SubmitNewPassword => { let mut rng = rand::rngs::OsRng; @@ -131,15 +132,15 @@ impl CommonComponent for ChangePasswordForm { opaque::client::registration::start_registration(&new_password, &mut rng) .context("Could not initiate password change")?; let req = registration::ClientRegistrationStartRequest { - username: self.common.username.clone(), + username: ctx.props().username.clone(), registration_start_request: registration_start_request.message, }; self.opaque_data = OpaqueData::Registration(registration_start_request.state); self.common.call_backend( - HostService::register_start, - req, + ctx, + HostService::register_start(req), Msg::RegistrationStartResponse, - )?; + ); Ok(true) } Msg::RegistrationStartResponse(res) => { @@ -159,22 +160,20 @@ impl CommonComponent for ChangePasswordForm { registration_upload: registration_finish.message, }; self.common.call_backend( - HostService::register_finish, - req, + ctx, + HostService::register_finish(req), Msg::RegistrationFinishResponse, - ) + ); } _ => panic!("Unexpected data in opaque_data field"), - }?; + }; Ok(false) } Msg::RegistrationFinishResponse(response) => { - self.common.cancel_task(); if response.is_ok() { - self.route_dispatcher - .send(RouteRequest::ChangeRoute(Route::from( - AppRoute::UserDetails(self.common.username.clone()), - ))); + ctx.link().history().unwrap().push(AppRoute::UserDetails { + user_id: ctx.props().username.clone(), + }); } response?; Ok(true) @@ -191,26 +190,21 @@ impl Component for ChangePasswordForm { type Message = Msg; type Properties = Props; - fn create(props: Self::Properties, link: ComponentLink) -> Self { + fn create(_: &Context) -> Self { ChangePasswordForm { - common: CommonComponentParts::::create(props, link), + common: CommonComponentParts::::create(), form: yew_form::Form::::new(FormModel::default()), opaque_data: OpaqueData::None, - route_dispatcher: RouteAgentDispatcher::new(), } } - fn update(&mut self, msg: Self::Message) -> ShouldRender { - CommonComponentParts::::update(self, msg) + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + CommonComponentParts::::update(self, ctx, msg) } - fn change(&mut self, props: Self::Properties) -> ShouldRender { - self.common.change(props) - } - - fn view(&self) -> Html { - let is_admin = self.common.is_admin; - let link = &self.common; + fn view(&self, ctx: &Context) -> Html { + let is_admin = ctx.props().is_admin; + let link = ctx.link(); type Field = yew_form::Field; html! { <> @@ -305,12 +299,12 @@ impl Component for ChangePasswordForm { {"Save changes"} - + to={AppRoute::UserDetails{user_id: ctx.props().username.clone()}}> {"Back"} - +
    diff --git a/app/src/components/create_group.rs b/app/src/components/create_group.rs index c1b5f05..a7e6641 100644 --- a/app/src/components/create_group.rs +++ b/app/src/components/create_group.rs @@ -8,10 +8,7 @@ use graphql_client::GraphQLQuery; use validator_derive::Validate; use yew::prelude::*; use yew_form_derive::Model; -use yew_router::{ - agent::{RouteAgentDispatcher, RouteRequest}, - route::Route, -}; +use yew_router::{prelude::History, scope_ext::RouterScopeExt}; #[derive(GraphQLQuery)] #[graphql( @@ -24,7 +21,6 @@ pub struct CreateGroup; pub struct CreateGroupForm { common: CommonComponentParts, - route_dispatcher: RouteAgentDispatcher, form: yew_form::Form, } @@ -41,7 +37,11 @@ pub enum Msg { } impl CommonComponent for CreateGroupForm { - fn handle_msg(&mut self, msg: ::Message) -> Result { + fn handle_msg( + &mut self, + ctx: &Context, + msg: ::Message, + ) -> Result { match msg { Msg::Update => Ok(true), Msg::SubmitForm => { @@ -53,6 +53,7 @@ impl CommonComponent for CreateGroupForm { name: model.groupname, }; self.common.call_graphql::( + ctx, req, Msg::CreateGroupResponse, "Error trying to create group", @@ -64,8 +65,7 @@ impl CommonComponent for CreateGroupForm { "Created group '{}'", &response?.create_group.display_name )); - self.route_dispatcher - .send(RouteRequest::ChangeRoute(Route::from(AppRoute::ListGroups))); + ctx.link().history().unwrap().push(AppRoute::ListGroups); Ok(true) } } @@ -80,24 +80,19 @@ impl Component for CreateGroupForm { type Message = Msg; type Properties = (); - fn create(props: Self::Properties, link: ComponentLink) -> Self { + fn create(_: &Context) -> Self { Self { - common: CommonComponentParts::::create(props, link), - route_dispatcher: RouteAgentDispatcher::new(), + common: CommonComponentParts::::create(), form: yew_form::Form::::new(CreateGroupModel::default()), } } - fn update(&mut self, msg: Self::Message) -> ShouldRender { - CommonComponentParts::::update(self, msg) + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + CommonComponentParts::::update(self, ctx, msg) } - fn change(&mut self, props: Self::Properties) -> ShouldRender { - self.common.change(props) - } - - fn view(&self) -> Html { - let link = &self.common; + fn view(&self, ctx: &Context) -> Html { + let link = ctx.link(); type Field = yew_form::Field; html! {
    diff --git a/app/src/components/create_user.rs b/app/src/components/create_user.rs index 7ee0fab..34de174 100644 --- a/app/src/components/create_user.rs +++ b/app/src/components/create_user.rs @@ -5,17 +5,14 @@ use crate::{ common_component::{CommonComponent, CommonComponentParts}, }, }; -use anyhow::{bail, Context, Result}; +use anyhow::{bail, Result}; use gloo_console::log; use graphql_client::GraphQLQuery; use lldap_auth::{opaque, registration}; use validator_derive::Validate; use yew::prelude::*; use yew_form_derive::Model; -use yew_router::{ - agent::{RouteAgentDispatcher, RouteRequest}, - route::Route, -}; +use yew_router::{prelude::History, scope_ext::RouterScopeExt}; #[derive(GraphQLQuery)] #[graphql( @@ -28,7 +25,6 @@ pub struct CreateUser; pub struct CreateUserForm { common: CommonComponentParts, - route_dispatcher: RouteAgentDispatcher, form: yew_form::Form, } @@ -73,7 +69,11 @@ pub enum Msg { } impl CommonComponent for CreateUserForm { - fn handle_msg(&mut self, msg: ::Message) -> Result { + fn handle_msg( + &mut self, + ctx: &Context, + msg: ::Message, + ) -> Result { match msg { Msg::Update => Ok(true), Msg::SubmitForm => { @@ -93,6 +93,7 @@ impl CommonComponent for CreateUserForm { }, }; self.common.call_graphql::( + ctx, req, Msg::CreateUserResponse, "Error trying to create user", @@ -122,12 +123,11 @@ impl CommonComponent for CreateUserForm { registration_start_request: message, }; self.common - .call_backend(HostService::register_start, req, move |r| { + .call_backend(ctx, HostService::register_start(req), move |r| { Msg::RegistrationStartResponse((state, r)) - }) - .context("Error trying to create user")?; + }); } else { - self.update(Msg::SuccessfulCreation); + self.update(ctx, Msg::SuccessfulCreation); } Ok(false) } @@ -143,22 +143,19 @@ impl CommonComponent for CreateUserForm { server_data: response.server_data, registration_upload: registration_upload.message, }; - self.common - .call_backend( - HostService::register_finish, - req, - Msg::RegistrationFinishResponse, - ) - .context("Error trying to register user")?; + self.common.call_backend( + ctx, + HostService::register_finish(req), + Msg::RegistrationFinishResponse, + ); Ok(false) } Msg::RegistrationFinishResponse(response) => { response?; - self.handle_msg(Msg::SuccessfulCreation) + self.handle_msg(ctx, Msg::SuccessfulCreation) } Msg::SuccessfulCreation => { - self.route_dispatcher - .send(RouteRequest::ChangeRoute(Route::from(AppRoute::ListUsers))); + ctx.link().history().unwrap().push(AppRoute::ListUsers); Ok(true) } } @@ -173,24 +170,19 @@ impl Component for CreateUserForm { type Message = Msg; type Properties = (); - fn create(props: Self::Properties, link: ComponentLink) -> Self { + fn create(_: &Context) -> Self { Self { - common: CommonComponentParts::::create(props, link), - route_dispatcher: RouteAgentDispatcher::new(), + common: CommonComponentParts::::create(), form: yew_form::Form::::new(CreateUserModel::default()), } } - fn update(&mut self, msg: Self::Message) -> ShouldRender { - CommonComponentParts::::update(self, msg) + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + CommonComponentParts::::update(self, ctx, msg) } - fn change(&mut self, props: Self::Properties) -> ShouldRender { - self.common.change(props) - } - - fn view(&self) -> Html { - let link = &self.common; + fn view(&self, ctx: &Context) -> Html { + let link = &ctx.link(); type Field = yew_form::Field; html! {
    diff --git a/app/src/components/delete_group.rs b/app/src/components/delete_group.rs index 0934976..1fe3ce9 100644 --- a/app/src/components/delete_group.rs +++ b/app/src/components/delete_group.rs @@ -39,16 +39,21 @@ pub enum Msg { } impl CommonComponent for DeleteGroup { - fn handle_msg(&mut self, msg: ::Message) -> Result { + fn handle_msg( + &mut self, + ctx: &Context, + msg: ::Message, + ) -> Result { match msg { Msg::ClickedDeleteGroup => { self.modal.as_ref().expect("modal not initialized").show(); } Msg::ConfirmDeleteGroup => { - self.update(Msg::DismissModal); + self.update(ctx, Msg::DismissModal); self.common.call_graphql::( + ctx, delete_group_query::Variables { - group_id: self.common.group.id, + group_id: ctx.props().group.id, }, Msg::DeleteGroupResponse, "Error trying to delete group", @@ -58,12 +63,8 @@ impl CommonComponent for DeleteGroup { self.modal.as_ref().expect("modal not initialized").hide(); } Msg::DeleteGroupResponse(response) => { - self.common.cancel_task(); response?; - self.common - .props - .on_group_deleted - .emit(self.common.group.id); + ctx.props().on_group_deleted.emit(ctx.props().group.id); } } Ok(true) @@ -78,15 +79,15 @@ impl Component for DeleteGroup { type Message = Msg; type Properties = DeleteGroupProps; - fn create(props: Self::Properties, link: ComponentLink) -> Self { + fn create(_: &Context) -> Self { Self { - common: CommonComponentParts::::create(props, link), + common: CommonComponentParts::::create(), node_ref: NodeRef::default(), modal: None, } } - fn rendered(&mut self, first_render: bool) { + fn rendered(&mut self, _: &Context, first_render: bool) { if first_render { self.modal = Some(Modal::new( self.node_ref @@ -96,20 +97,17 @@ impl Component for DeleteGroup { } } - fn update(&mut self, msg: Self::Message) -> ShouldRender { + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { CommonComponentParts::::update_and_report_error( self, + ctx, msg, - self.common.on_error.clone(), + ctx.props().on_error.clone(), ) } - fn change(&mut self, props: Self::Properties) -> ShouldRender { - self.common.change(props) - } - - fn view(&self) -> Html { - let link = &self.common; + fn view(&self, ctx: &Context) -> Html { + let link = &ctx.link(); html! { <> - {self.show_modal()} + {self.show_modal(ctx)} } } } impl DeleteGroup { - fn show_modal(&self) -> Html { - let link = &self.common; + fn show_modal(&self, ctx: &Context) -> Html { + let link = &ctx.link(); html! {