65 Commits

Author SHA1 Message Date
Valentin Tolmer
32f28d664e Bump to version 0.4.1 2022-10-10 17:46:34 +02:00
Hobbabobba
412f4fa644 example_config: add Docuwiki 2022-10-09 13:11:26 +02:00
dependabot[bot]
4ffa565e51 build(deps): bump actions/checkout from 2 to 3.1.0 (#314)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.1.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/v2...v3.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: nitnelave <valentin.tolmer@gmail.com>
2022-10-08 06:33:30 +02:00
Hobbabobba
2f9ea4f10f example_configs: add hedgedoc
Co-authored-by: nitnelave <valentin.tolmer@gmail.com>
2022-10-07 21:19:55 +02:00
Dedy Martadinata
123fdc5baf docker: use the locally-downloaded assets
Change the index to the local one to use the locally-downloaded fonts and css.
2022-10-07 19:22:20 +02:00
Valentin Tolmer
5402aa5aa2 server: Silence error message when creating DB
Fixes #300
2022-09-30 15:12:15 +02:00
Valentin Tolmer
8069516283 server: Add support for PKCS1 keys
Fixes #288
2022-09-30 13:56:03 +02:00
Valentin Tolmer
6c21f2ef4b clippy: fix warning by implementing Eq 2022-09-27 06:54:29 +02:00
Valentin Tolmer
516893f1f7 server: Fix query building of chained ands/ors
Fixes #303
2022-09-27 05:14:57 +02:00
Marco Dura
1660cb1fbb example_config: grafana fix typos and attributes 2022-09-22 15:22:30 +02:00
Valentin Tolmer
7e1ce10df1 server: allow every config value to be specified as a file
By using https://crates.io/crates/figment_file_provider_adapter

Fixes https://github.com/nitnelave/lldap/issues/263
2022-09-14 11:16:50 +02:00
arcoast
b6ee918ca9 example_configs: add Airsonic 2022-09-10 14:32:56 +02:00
Valentin Tolmer
24efd61464 github: fix static wasm-pack build 2022-09-10 13:12:20 +02:00
Valentin Tolmer
0b6b274cfa example_configs: add Nextcloud 2022-09-10 12:47:41 +02:00
Alexander Olsson
8b01271e94 readme: Fix typo
Co-authored-by: alol <alexander.olsson@flaxplax.com>
2022-08-16 16:53:35 -05:00
Valentin Tolmer
d536addf0a migration-tool: misc cleanup 2022-08-09 13:03:28 +02:00
Valentin Tolmer
2ca083541e migration-tool: Import users' avatars 2022-08-09 13:03:28 +02:00
Valentin Tolmer
686bdc0cb1 app: Add support for modifying an avatar 2022-08-09 13:03:28 +02:00
Valentin Tolmer
60c594438c ldap: Stop returning empty attributes 2022-08-09 13:03:28 +02:00
Valentin Tolmer
b130965264 ldap: return user's avatar 2022-08-09 13:03:28 +02:00
Valentin Tolmer
697a64991d server: Change attribute values to bytes 2022-08-09 13:03:28 +02:00
Valentin Tolmer
3acc448048 server: Add support for users' avatars in GrahpQL 2022-08-09 13:03:28 +02:00
Valentin Tolmer
0e3c5120da app: Switch yew_form dependency back to main repo 2022-08-09 13:03:28 +02:00
Valentin Tolmer
7707367c35 migration-tool: Extract the JWT from the JSON response
The response used to contain just the JWT, but now it's wrapped in JSON.

Fixes #282.
2022-08-03 22:34:19 +02:00
Valentin Tolmer
122e08790f docker: fix tag typo 2022-08-01 18:26:47 +02:00
Valentin Tolmer
64556fc744 server: stop returning "dn" as an attribute
It's already part of the base response

Fixes #254.
2022-08-01 18:26:47 +02:00
Valentin Tolmer
134a9366f5 server: create private key with 400 permissions
Fixes #261.
2022-08-01 17:43:37 +02:00
Dedy Martadinata S
f69b729eb2 Update docker-build-static.yml 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
2ac47d5c85 cleanup Dockerfile.dev 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
26d3d84de0 use lldap dev image 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
b413935932 some cleanup 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
e6ae726304 manual fetch aarch64 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
520277b611 update tag 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
8cdfedddbd add os in tag 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
5312400a3f add Dockerfile.dev 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
551f5abc4b manual fetch 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
10d826fc46 using script 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
252bd6cf39 use custom dev image 2022-08-01 17:31:13 +02:00
Dedy Martadinata Supriyadi
ba44dea7b6 add ca-cert 2022-08-01 17:31:13 +02:00
Dedy Martadinata S
b9c823e01a Build using musl 2022-08-01 17:31:13 +02:00
Valentin Tolmer
c108921dcf server: Add a log message when search is restricted
Fixes #264.
2022-08-01 14:02:24 +02:00
Valentin Tolmer
36eed1e091 README: Document the build process, add systemd service
Fixes #269.
2022-08-01 09:14:39 +02:00
Valentin Tolmer
897704fab3 server: Fix extra error message when DB doesn't exist
Fixes #270
2022-08-01 09:14:39 +02:00
Valentin Tolmer
9f70910283 docs: Update the docker config template to add smtp_encryption 2022-08-01 09:14:39 +02:00
dependabot[bot]
3e3c9b97ae build(deps): bump juniper from 0.15.9 to 0.15.10
Bumps [juniper](https://github.com/graphql-rust/juniper) from 0.15.9 to 0.15.10.
- [Release notes](https://github.com/graphql-rust/juniper/releases)
- [Changelog](https://github.com/graphql-rust/juniper/blob/master/release.toml)
- [Commits](https://github.com/graphql-rust/juniper/compare/juniper-v0.15.9...juniper-v0.15.10)

---
updated-dependencies:
- dependency-name: juniper
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-30 19:22:59 +02:00
Valentin Tolmer
8c1ea11b95 server: add an option to use STARTTLS for smtp 2022-07-30 15:58:58 +02:00
Valentin Tolmer
cd0ab378ef server: deprecate smtp.tls_required, add smtp_encryption 2022-07-30 15:58:58 +02:00
Matteo Bonora
5a27ae4862 readme: Add version to docker-compose examples 2022-07-29 10:56:24 +02:00
Jun-Cheol Park
05719642ca Fix: Change input filed to password type in change_password ui (#273) 2022-07-26 11:07:44 +02:00
Iván Izaguirre
5c584536b5 frontend: Add UUID and creation date
This exposes the new info in the GraphQL API, and adds it to the frontend.
2022-07-21 12:10:37 +02:00
Valentin Tolmer
4ba0db4e9e migration_tool: Switch from OpenSSL to Rustls 2022-07-15 15:49:15 +02:00
Valentin Tolmer
5e4ed9ee17 docker: remove libssl-dev 2022-07-15 15:49:15 +02:00
Valentin Tolmer
c399ff2bfa server: switch from OpenSSL to Rustls 2022-07-15 15:49:15 +02:00
Frank Moskal
9e37a06514 server: allow admin email to be set via config 2022-07-13 14:32:35 +02:00
Valentin Tolmer
294ce77a47 server: Fix misc clippy warnings 2022-07-13 12:43:51 +02:00
Dedy Martadinata S
24c6b4a879 docker: Remove debian build, revert to default alpine 2022-07-13 11:41:26 +02:00
Jacob Yundt
2c2696a8c3 docker: Add support for SMTP password file (#240)
Similar to LLDAP_JWT_SECRET and LLDAP_LDAP_USER_PASS, add option to use an
ENV variable to specify the file of the SMTP password:
LLDAP_SMTP_OPTIONS__PASSWORD_FILE
2022-07-13 10:52:40 +02:00
Dedy Martadinata S
479d1e7635 readme: Add details about latest tag 2022-07-13 10:38:31 +02:00
Dedy Martadinata S
3a723460e5 docker: Add volume support 2022-07-13 08:09:36 +02:00
Valentin Tolmer
8011756658 readme: Correct RAM estimate 2022-07-12 12:37:27 +02:00
Dedy Martadinata S
46546dac27 docker: Add support for UID:GID
Adds support for the UID/GID env variables in Docker via `gosu`.
2022-07-12 10:37:08 +02:00
Dedy Martadinata S
9a869a1474 add bash, entrypoint deps. 2022-07-11 16:09:22 +02:00
Sebastian Thiel
09797695aa Fix typo in README.md 2022-07-11 15:38:11 +02:00
Dedy Martadinata S
4f2cf45427 docker: build static binaries, add alpine targets 2022-07-11 15:36:59 +02:00
Valentin Tolmer
901eb7f469 example_configs: Add XBackBone 2022-07-11 12:30:46 +02:00
56 changed files with 1916 additions and 685 deletions

106
.github/workflows/Dockerfile.ci.alpine vendored Normal file
View File

@@ -0,0 +1,106 @@
FROM debian:bullseye AS lldap
ARG DEBIAN_FRONTEND=noninteractive
ARG TARGETPLATFORM
RUN apt update && apt install -y wget
WORKDIR /dim
COPY bin/ bin/
COPY web/ web/
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 && \
chmod +x target/lldap && \
chmod +x target/migration-tool && \
ls -la target/ . && \
pwd \
; fi
RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
mv bin/aarch64-bin/lldap target/lldap && \
mv bin/aarch64-bin/migration-tool target/migration-tool && \
chmod +x target/lldap && \
chmod +x target/migration-tool && \
ls -la target/ . && \
pwd \
; fi
RUN if [ "${TARGETPLATFORM}" = "linux/arm/v7" ]; then \
mv bin/armhf-bin/lldap target/lldap && \
mv bin/armhf-bin/migration-tool target/migration-tool && \
chmod +x target/lldap && \
chmod +x target/migration-tool && \
ls -la target/ . && \
pwd \
; fi
# Web and App dir
COPY docker-entrypoint.sh /docker-entrypoint.sh
COPY lldap_config.docker_template.toml /lldap/
COPY web/index_local.html web/index.html
RUN cp target/lldap /lldap/ && \
cp target/migration-tool /lldap/ && \
cp -R web/index.html \
web/pkg \
web/static \
/lldap/app/
WORKDIR /lldap
RUN set -x \
&& for file in $(cat /lldap/app/static/libraries.txt); do wget -P app/static "$file"; done \
&& for file in $(cat /lldap/app/static/fonts/fonts.txt); do wget -P app/static/fonts "$file"; done \
&& chmod a+r -R .
FROM alpine:3.16
WORKDIR /app
ENV UID=1000
ENV GID=1000
ENV USER=lldap
ENV GOSU_VERSION 1.14
# Fetch gosu from git
RUN set -eux; \
\
apk add --no-cache --virtual .gosu-deps \
ca-certificates \
dpkg \
gnupg \
; \
\
dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
\
# verify the signature
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
command -v gpgconf && gpgconf --kill all || :; \
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
\
# clean up fetch dependencies
apk del --no-network .gosu-deps; \
\
chmod +x /usr/local/bin/gosu; \
# verify that the binary works
gosu --version; \
gosu nobody true
RUN apk add --no-cache tini ca-certificates bash && \
addgroup -g $GID $USER && \
adduser \
--disabled-password \
--gecos "" \
--home "$(pwd)" \
--ingroup "$USER" \
--no-create-home \
--uid "$UID" \
"$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
VOLUME ["/data"]
WORKDIR /app
ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"]
CMD ["run", "--config-file", "/data/lldap_config.toml"]

View File

@@ -39,6 +39,7 @@ RUN if [ "${TARGETPLATFORM}" = "linux/arm/v7" ]; then \
# Web and App dir
COPY docker-entrypoint.sh /docker-entrypoint.sh
COPY lldap_config.docker_template.toml /lldap/
COPY web/index_local.html web/index.html
RUN cp target/lldap /lldap/ && \
cp target/migration-tool /lldap/ && \
cp -R web/index.html \
@@ -46,23 +47,25 @@ RUN cp target/lldap /lldap/ && \
web/static \
/lldap/app/
WORKDIR /lldap
RUN set -x \
&& for file in $(cat /lldap/app/static/libraries.txt); do wget -P app/static "$file"; done \
&& for file in $(cat /lldap/app/static/fonts/fonts.txt); do wget -P app/static/fonts "$file"; done \
&& chmod a+r -R .
FROM debian:bullseye
&& chmod a+r -R .
FROM debian:bullseye-slim
ENV UID=1000
ENV GID=1000
ENV USER=lldap
RUN apt update && \
apt install -y --no-install-recommends tini ca-certificates && \
apt install -y --no-install-recommends tini openssl ca-certificates gosu && \
apt clean && \
rm -rf /var/lib/apt/lists/* && \
groupadd -g $GID $USER && useradd --system -m -g $USER --uid $UID $USER
COPY --from=lldap --chown=$CONTAINERUSER:$CONTAINERUSER /lldap /app
COPY --from=lldap --chown=$CONTAINERUSER:$CONTAINERUSER /docker-entrypoint.sh /docker-entrypoint.sh
groupadd -g $GID $USER && useradd --system -m -g $USER --uid $UID $USER && \
mkdir -p /data && chown $USER:$USER /data
COPY --from=lldap --chown=$USER:$USER /lldap /app
COPY --from=lldap --chown=$USER:$USER /docker-entrypoint.sh /docker-entrypoint.sh
VOLUME ["/data"]
WORKDIR /app
USER $USER
ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"]
CMD ["run", "--config-file", "/data/lldap_config.toml"]

34
.github/workflows/Dockerfile.dev vendored Normal file
View File

@@ -0,0 +1,34 @@
FROM rust:1.62-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"
### 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 && \
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
### 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 clean && \
rm -rf /var/lib/apt/lists/* && \
rustup target add aarch64-unknown-linux-gnu
### 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 && \
wget -c https://musl.cc/aarch64-linux-musl-cross.tgz && \
tar zxf ./aarch64-linux-musl-cross.tgz -C /opt && \
rm ./x86_64-linux-musl-cross.tgz && \
rm ./aarch64-linux-musl-cross.tgz
CMD ["bash"]

View File

@@ -1,4 +1,4 @@
name: Docker
name: Docker Static
on:
push:
@@ -15,13 +15,9 @@ on:
msg:
description: "Set message"
default: "Manual trigger"
env:
CARGO_TERM_COLOR: always
RUSTC_WRAPPER: sccache
SCCACHE_DIR: $GITHUB_WORKSPACE/.sccache
SCCACHE_VERSION: v0.3.0
LINK: https://github.com/mozilla/sccache/releases/download
# In total 5 jobs, all of the jobs are containerized
# ---
@@ -31,7 +27,7 @@ env:
### Install nodejs from nodesource repo
### install wasm
### install rollup
### run app/build.sh
### run app/build.sh
### upload artifacts
# builds-armhf, build-aarch64, build-amd64 create binary for respective arch
@@ -43,8 +39,6 @@ env:
## the CARGO_ env
#CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER: arm-linux-gnueabihf-gcc
#OPENSSL_INCLUDE_DIR: "/usr/include/openssl/"
#OPENSSL_LIB_DIR: "/usr/lib/arm-linux-gnueabihf/"
# This will determine which architecture lib will be used.
# build-ui,builds-armhf, build-aarch64, build-amd64 will upload artifacts will be used next job
@@ -54,50 +48,45 @@ env:
# 1-bullseye, 1.61-bullseye, 1.61.0-bullseye, bullseye, 1, 1.61, 1.61.0, latest
# cache
## .sccache
## cargo
## target
jobs:
build-ui:
runs-on: ubuntu-latest
container:
image: rust:1.61
container:
image: rust:1.62
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -Ctarget-feature=-crt-static
RUSTFLAGS: -Ctarget-feature=+crt-static
steps:
- name: install runtime
run: apt update && apt install -y gcc-x86-64-linux-gnu g++-x86-64-linux-gnu libc6-dev libssl-dev
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: Install sccache (ubuntu-latest)
run: |
SCCACHE_FILE=sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl
mkdir -p $HOME/.local/bin
curl -L "$LINK/$SCCACHE_VERSION/$SCCACHE_FILE.tar.gz" | tar xz
mv -f $SCCACHE_FILE/sccache $HOME/.local/bin/sccache
chmod +x $HOME/.local/bin/sccache
echo "$HOME/.local/bin" >> $GITHUB_PATH
- uses: actions/cache@v3
with:
path: |
.sccache
/usr/local/cargo
/usr/local/cargo/bin
/usr/local/cargo/registry/index
/usr/local/cargo/registry/cache
/usr/local/cargo/git/db
target
key: lldap-ui-${{ github.sha }}
key: lldap-ui-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
lldap-ui-
- name: Checkout repository
uses: actions/checkout@v2
- name: install cargo wasm
run: cargo install wasm-pack
uses: actions/checkout@v3.1.0
- name: install rollup nodejs
run: npm install -g rollup
- name: install wasm-pack with cargo
run: cargo install wasm-pack || true
env:
RUSTFLAGS: ""
- name: build frontend
run: ./app/build.sh
- name: check path
@@ -107,45 +96,39 @@ jobs:
with:
name: ui
path: app/
build-armhf:
runs-on: ubuntu-latest
container:
image: rust:1.61
container:
image: rust:1.62
env:
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER: arm-linux-gnueabihf-gcc
OPENSSL_INCLUDE_DIR: "/usr/include/openssl/"
OPENSSL_LIB_DIR: "/usr/lib/arm-linux-gnueabihf/"
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 libssl-dev:armhf tar
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: Install sccache (ubuntu-latest)
run: |
SCCACHE_FILE=sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl
mkdir -p $HOME/.local/bin
curl -L "$LINK/$SCCACHE_VERSION/$SCCACHE_FILE.tar.gz" | tar xz
mv -f $SCCACHE_FILE/sccache $HOME/.local/bin/sccache
chmod +x $HOME/.local/bin/sccache
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3.1.0
- uses: actions/cache@v3
with:
path: |
.sccache
/usr/local/cargo
.cargo/bin
.cargo/registry/index
.cargo/registry/cache
.cargo/git/db
target
key: lldap-bin-armhf-${{ github.sha }}
key: lldap-bin-armhf-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
lldap-bin-armhf-
- name: compile armhf
@@ -166,113 +149,106 @@ jobs:
build-aarch64:
runs-on: ubuntu-latest
container:
image: rust:1.61
container:
##################################################################################
# Github actions currently timeout when downloading musl-gcc #
# Using lldap dev image based on rust:1.62-slim-bullseye and musl-gcc bundled #
# Only for Job build aarch64 and amd64 #
###################################################################################
#image: rust:1.62
image: nitnelave/rust-dev:latest
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
OPENSSL_INCLUDE_DIR: "/usr/include/openssl/"
OPENSSL_LIB_DIR: "/usr/lib/aarch64-linux-gnu/"
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: aarch64-linux-musl-gcc
CARGO_TERM_COLOR: always
RUSTFLAGS: -Ctarget-feature=-crt-static
RUSTFLAGS: -Ctarget-feature=+crt-static
CARGO_HOME: ${GITHUB_WORKSPACE}/.cargo
steps:
- name: add arm64 architecture
run: dpkg --add-architecture arm64
- name: install runtime
run: apt update && apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libc6-arm64-cross libc6-dev-arm64-cross libssl-dev:arm64 tar
- name: Checkout repository
uses: actions/checkout@v3.1.0
- name: smoke test
run: rustc --version
- name: Checkout repository
uses: actions/checkout@v2
- name: add arm64 target
run: rustup target add aarch64-unknown-linux-gnu
- name: smoke test
run: rustc --version
- name: Install sccache (ubuntu-latest)
run: |
SCCACHE_FILE=sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl
mkdir -p $HOME/.local/bin
curl -L "$LINK/$SCCACHE_VERSION/$SCCACHE_FILE.tar.gz" | tar xz
mv -f $SCCACHE_FILE/sccache $HOME/.local/bin/sccache
chmod +x $HOME/.local/bin/sccache
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3.1.0
- uses: actions/cache@v3
with:
path: |
.sccache
/usr/local/cargo
.cargo/bin
.cargo/registry/index
.cargo/registry/cache
.cargo/git/db
target
key: lldap-bin-aarch64-${{ github.sha }}
key: lldap-bin-aarch64-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
lldap-bin-aarch64-
- name: compile aarch64
run: cargo build --target=aarch64-unknown-linux-gnu --release -p lldap -p migration-tool
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-gnu/release/
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-gnu/release/lldap
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-gnu/release/migration-tool
path: target/aarch64-unknown-linux-musl/release/migration-tool
build-amd64:
runs-on: ubuntu-latest
container:
image: rust:1.61
container:
# image: rust:1.62
image: nitnelave/rust-dev:latest
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -Ctarget-feature=-crt-static
RUSTFLAGS: -Ctarget-feature=+crt-static
CARGO_HOME: ${GITHUB_WORKSPACE}/.cargo
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER: x86_64-linux-musl-gcc
steps:
- name: install runtime
run: apt update && apt install -y gcc-x86-64-linux-gnu g++-x86-64-linux-gnu libc6-dev libssl-dev tar
- name: smoke test
run: rustc --version
- name: Install sccache (ubuntu-latest)
run: |
SCCACHE_FILE=sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl
mkdir -p $HOME/.local/bin
curl -L "$LINK/$SCCACHE_VERSION/$SCCACHE_FILE.tar.gz" | tar xz
mv -f $SCCACHE_FILE/sccache $HOME/.local/bin/sccache
chmod +x $HOME/.local/bin/sccache
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Checkout repository
uses: actions/checkout@v2
- name: cargo & sscache cache
uses: actions/cache@v3
uses: actions/checkout@v3.1.0
- uses: actions/cache@v3
with:
path: |
.sccache
/usr/local/cargo
.cargo/bin
.cargo/registry/index
.cargo/registry/cache
.cargo/git/db
target
key: lldap-bin-amd64-${{ github.sha }}
key: lldap-bin-amd64-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
lldap-bin-amd64-
#- name: add cargo chef
# run: cargo install cargo-chef
#- name: chef prepare
# run: cargo chef prepare --recipe-path recipe.json
#- name: cook?
# run: cargo chef cook --release --recipe-path recipe.json
- name: compile amd64
run: cargo build --target=x86_64-unknown-linux-gnu --release -p lldap -p migration-tool
- 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-gnu/release/
run: ls -al target/x86_64-unknown-linux-musl/release/
- name: upload amd64 lldap artifacts
uses: actions/upload-artifact@v3
with:
name: amd64-lldap-bin
path: target/x86_64-unknown-linux-gnu/release/lldap
path: target/x86_64-unknown-linux-musl/release/lldap
- name: upload amd64 migration-tool artifacts
uses: actions/upload-artifact@v3
with:
name: amd64-migration-tool-bin
path: target/x86_64-unknown-linux-gnu/release/migration-tool
path: target/x86_64-unknown-linux-musl/release/migration-tool
build-docker-image:
@@ -283,9 +259,11 @@ jobs:
contents: read
packages: write
steps:
- name: install rsync
run: sudo apt update && sudo apt install -y rsync
- name: fetch repo
uses: actions/checkout@v2
uses: actions/checkout@v3.1.0
- name: Download armhf lldap artifacts
uses: actions/download-artifact@v3
with:
@@ -296,7 +274,7 @@ jobs:
with:
name: armhf-migration-tool-bin
path: bin/armhf-bin
- name: Download aarch64 lldap artifacts
uses: actions/download-artifact@v3
with:
@@ -306,8 +284,8 @@ jobs:
uses: actions/download-artifact@v3
with:
name: aarch64-migration-tool-bin
path: bin/aarch64-bin
path: bin/aarch64-bin
- name: Download amd64 lldap artifacts
uses: actions/download-artifact@v3
with:
@@ -318,20 +296,20 @@ jobs:
with:
name: amd64-migration-tool-bin
path: bin/amd64-bin
- name: check bin path
run: ls -al bin/
- name: Download llap ui artifacts
uses: actions/download-artifact@v3
with:
name: ui
path: web
- name: setup qemu
uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
@@ -354,33 +332,62 @@ jobs:
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: parse tag
uses: gacts/github-slug@v1
id: slug
- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push latest
######################
#### latest build ####
######################
- name: Build and push latest alpine
if: github.event_name != 'release'
uses: docker/build-push-action@v3
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
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
- name: Build and push latest debian
if: github.event_name != 'release'
uses: docker/build-push-action@v3
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
file: ./.github/workflows/Dockerfile.ci
tags: nitnelave/lldap:latest
#cache-from: type=gha
#cache-to: type=gha,mode=max
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
- name: Build and push release
#######################
#### release build ####
#######################
- name: Build and push release alpine
if: github.event_name == 'release'
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
# Tag as latest, stable, semver, major, major.minor and major.minor.patch.
file: ./.github/workflows/Dockerfile.ci.alpine
tags: nitnelave/lldap:stable, nitnelave/lldap:stable-alpine, nitnelave/lldap:v${{ steps.slug.outputs.version-semantic }}, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}.${{ steps.slug.outputs.version-patch }}, nitnelave/lldap:v${{ steps.slug.outputs.version-semantic }}-alpine, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}-alpine, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}-alpine.${{ steps.slug.outputs.version-minor }}-alpine, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}.${{ steps.slug.outputs.version-patch }}-alpine
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
- name: Build and push release debian
if: github.event_name == 'release'
uses: docker/build-push-action@v3
with:
@@ -388,18 +395,14 @@ jobs:
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
# Tag as latest, stable, semver, major, major.minor and major.minor.patch.
file: ./.github/workflows/Dockerfile.ci
tags: nitnelave/lldap:stable, 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 }}
#cache-from: type=gha
#cache-to: type=gha,mode=max
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: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
run: rsync -r /tmp/.buildx-cache-new /tmp/.buildx-cache --delete
- name: Update repo description
if: github.event_name != 'pull_request'
uses: peter-evans/dockerhub-description@v3

View File

@@ -34,7 +34,7 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@v3
uses: actions/checkout@v3.1.0
- uses: Swatinem/rust-cache@v1
- name: Build
run: cargo build --verbose --workspace
@@ -53,7 +53,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
uses: actions/checkout@v3.1.0
- uses: Swatinem/rust-cache@v1
@@ -70,7 +70,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
uses: actions/checkout@v3.1.0
- uses: Swatinem/rust-cache@v1
@@ -87,7 +87,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
uses: actions/checkout@v3.1.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

View File

@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.4.1] - 2022-10-10
### Added
- Added support for STARTTLS for SMTP.
- Added support for user profile pictures, including importing them from OpenLDAP.
- Added support for every config value to be specified in a file.
- Added support for PKCS1 keys.
### Changed
- The `dn` attribute is no longer returned as an attribute (it's still part of the response).
- Empty attributes are no longer returned.
- The docker image now uses the locally-downloaded assets.
## [0.4.0] - 2022-07-08
### Breaking

597
Cargo.lock generated
View File

@@ -199,7 +199,9 @@ dependencies = [
"futures-core",
"http",
"log",
"tokio-rustls 0.22.0",
"tokio-util 0.6.10",
"webpki-roots 0.21.1",
]
[[package]]
@@ -385,6 +387,45 @@ version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
[[package]]
name = "asn1-rs"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30ff05a702273012438132f449575dbc804e27b2f3cbe3069aa237d26c98fa33"
dependencies = [
"asn1-rs-derive",
"asn1-rs-impl",
"displaydoc 0.2.3",
"nom 7.1.1",
"num-traits",
"rusticata-macros",
"thiserror",
"time 0.3.11",
]
[[package]]
name = "asn1-rs-derive"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8b7511298d5b7784b40b092d9e9dcd3a627a5707e4b5e507931ab0d44eeebf"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "asn1-rs-impl"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "async-trait"
version = "0.1.56"
@@ -557,6 +598,12 @@ version = "3.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3"
[[package]]
name = "bytemuck"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5377c8865e74a160d21f29c2d40669f53286db6eab59b88540cbb12ffc8b835"
[[package]]
name = "byteorder"
version = "1.4.3"
@@ -654,6 +701,12 @@ dependencies = [
"os_str_bytes",
]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "combine"
version = "3.8.1"
@@ -913,6 +966,12 @@ dependencies = [
"syn",
]
[[package]]
name = "data-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
[[package]]
name = "der"
version = "0.4.5"
@@ -923,6 +982,20 @@ dependencies = [
"crypto-bigint",
]
[[package]]
name = "der-parser"
version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe398ac75057914d7d07307bf67dc7f3f574a26783b4fc7805a20ffa9f506e82"
dependencies = [
"asn1-rs",
"displaydoc 0.2.3",
"nom 7.1.1",
"num-bigint 0.4.3",
"num-traits",
"rusticata-macros",
]
[[package]]
name = "derive_builder"
version = "0.10.2"
@@ -1030,6 +1103,17 @@ dependencies = [
"syn",
]
[[package]]
name = "displaydoc"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "dotenv"
version = "0.15.0"
@@ -1118,6 +1202,15 @@ dependencies = [
"version_check",
]
[[package]]
name = "figment_file_provider_adapter"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33106424fdbb9b1fd89c18072ba94666496a8a468178911b832a3e406988500"
dependencies = [
"figment",
]
[[package]]
name = "firestorm"
version = "0.5.1"
@@ -1161,21 +1254,6 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.0.1"
@@ -1435,13 +1513,35 @@ dependencies = [
"thiserror",
]
[[package]]
name = "graphql-parser"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ebc8013b4426d5b81a4364c419a95ed0b404af2b82e2457de52d9348f0e474"
dependencies = [
"combine",
"thiserror",
]
[[package]]
name = "graphql_client"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9b58571cfc3cc42c3e8ff44fc6cfbb6c0dea17ed22d20f9d8f1efc4e8209a3f"
dependencies = [
"graphql_query_derive",
"graphql_query_derive 0.10.0",
"serde",
"serde_json",
]
[[package]]
name = "graphql_client"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fc16d75d169fddb720d8f1c7aed6413e329e1584079b9734ff07266a193f5bc"
dependencies = [
"graphql_query_derive 0.11.0",
"reqwest",
"serde",
"serde_json",
]
@@ -1463,13 +1563,41 @@ dependencies = [
"syn",
]
[[package]]
name = "graphql_client_codegen"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f290ecfa3bea3e8a157899dc8a1d96ee7dd6405c18c8ddd213fc58939d18a0e9"
dependencies = [
"graphql-introspection-query",
"graphql-parser 0.4.0",
"heck 0.4.0",
"lazy_static",
"proc-macro2",
"quote",
"serde",
"serde_json",
"syn",
]
[[package]]
name = "graphql_query_derive"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e56b093bfda71de1da99758b036f4cc811fd2511c8a76f75680e9ffbd2bb4251"
dependencies = [
"graphql_client_codegen",
"graphql_client_codegen 0.10.0",
"proc-macro2",
"syn",
]
[[package]]
name = "graphql_query_derive"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a755cc59cda2641ea3037b4f9f7ef40471c329f55c1fa2db6fa0bb7ae6c1f7ce"
dependencies = [
"graphql_client_codegen 0.11.0",
"proc-macro2",
"syn",
]
@@ -1577,17 +1705,6 @@ dependencies = [
"digest",
]
[[package]]
name = "hostname"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
dependencies = [
"libc",
"match_cfg",
"winapi",
]
[[package]]
name = "http"
version = "0.2.8"
@@ -1653,16 +1770,16 @@ dependencies = [
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
name = "hyper-rustls"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac"
dependencies = [
"bytes",
"http",
"hyper",
"native-tls",
"rustls 0.20.6",
"tokio",
"tokio-native-tls",
"tokio-rustls 0.23.4",
]
[[package]]
@@ -1688,6 +1805,20 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
[[package]]
name = "image"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e30ca2ecf7666107ff827a8e481de6a132a9b687ed3bb20bb1c144a36c00964"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"jpeg-decoder",
"num-rational",
"num-traits",
]
[[package]]
name = "indexmap"
version = "1.6.2"
@@ -1750,6 +1881,12 @@ dependencies = [
"libc",
]
[[package]]
name = "jpeg-decoder"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9478aa10f73e7528198d75109c8be5cd7d15fb530238040148d5f9a22d4c5b3b"
[[package]]
name = "js-sys"
version = "0.3.58"
@@ -1761,9 +1898,9 @@ dependencies = [
[[package]]
name = "juniper"
version = "0.15.9"
version = "0.15.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21ac55c9084d08a7e315d78e2b15b7cc220f5eb67413c6ebf6967ee5de3b69fc"
checksum = "4f478f229a8ab52ff242f3250c8b3b8fe0a59b5b934f9706b7bdbc980991a7b6"
dependencies = [
"async-trait",
"bson",
@@ -1866,26 +2003,29 @@ dependencies = [
"lazy_static",
"lber",
"log",
"native-tls",
"nom 2.2.1",
"percent-encoding",
"ring",
"rustls 0.20.6",
"rustls-native-certs",
"thiserror",
"tokio",
"tokio-native-tls",
"tokio-rustls 0.23.4",
"tokio-stream",
"tokio-util 0.7.3",
"url",
"x509-parser",
]
[[package]]
name = "ldap3_server"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7873d5bd5baabdb77aa2d8a762bf8b1136f9f90cc90f44639b4c0d4486281dcb"
name = "ldap3_proto"
version = "0.2.3"
source = "git+https://github.com/nitnelave/ldap3_server/?rev=7b50b2b82c383f5f70e02e11072bb916629ed2bc#7b50b2b82c383f5f70e02e11072bb916629ed2bc"
dependencies = [
"bytes",
"lber",
"tokio-util 0.6.10",
"tokio-util 0.7.3",
"tracing",
]
[[package]]
@@ -1901,18 +2041,19 @@ dependencies = [
"fastrand",
"futures-io",
"futures-util",
"hostname",
"httpdate",
"idna",
"mime",
"native-tls",
"nom 7.1.1",
"once_cell",
"quoted_printable",
"rustls 0.20.6",
"rustls-pemfile",
"serde",
"socket2",
"tokio",
"tokio-native-tls",
"tokio-rustls 0.23.4",
"webpki-roots 0.22.4",
]
[[package]]
@@ -1979,28 +2120,31 @@ dependencies = [
"cron",
"derive_builder",
"figment",
"figment_file_provider_adapter",
"futures",
"futures-util",
"hmac 0.10.1",
"http",
"image",
"itertools",
"juniper",
"juniper_actix",
"jwt",
"ldap3_server",
"ldap3_proto",
"lettre",
"lldap_auth",
"log",
"mockall",
"native-tls",
"opaque-ke",
"openssl-sys",
"orion",
"rand 0.8.5",
"rustls 0.20.6",
"rustls-pemfile",
"sea-query",
"sea-query-binder",
"secstr",
"serde",
"serde_bytes",
"serde_json",
"sha2",
"sqlx",
@@ -2008,9 +2152,9 @@ dependencies = [
"thiserror",
"time 0.2.27",
"tokio",
"tokio-native-tls",
"tokio-rustls 0.23.4",
"tokio-stream",
"tokio-util 0.6.10",
"tokio-util 0.7.3",
"tracing",
"tracing-actix-web",
"tracing-attributes",
@@ -2025,9 +2169,11 @@ name = "lldap_app"
version = "0.4.0"
dependencies = [
"anyhow",
"base64",
"chrono",
"graphql_client",
"graphql_client 0.10.0",
"http",
"image",
"indexmap",
"jwt",
"lldap_auth",
@@ -2099,12 +2245,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "match_cfg"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]]
name = "matchers"
version = "0.1.0"
@@ -2148,7 +2288,8 @@ name = "migration-tool"
version = "0.3.0-alpha.1"
dependencies = [
"anyhow",
"graphql_client",
"base64",
"graphql_client 0.11.0",
"ldap3",
"lldap_auth",
"rand 0.8.5",
@@ -2251,24 +2392,6 @@ dependencies = [
"syn",
]
[[package]]
name = "native-tls"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "nom"
version = "2.2.1"
@@ -2322,6 +2445,17 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
dependencies = [
"autocfg 1.1.0",
"num-integer",
"num-traits",
]
[[package]]
name = "num-bigint-dig"
version = "0.7.0"
@@ -2361,6 +2495,17 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
dependencies = [
"autocfg 1.1.0",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.15"
@@ -2381,6 +2526,15 @@ dependencies = [
"libc",
]
[[package]]
name = "num_threads"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
dependencies = [
"libc",
]
[[package]]
name = "object"
version = "0.28.4"
@@ -2390,6 +2544,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "oid-registry"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38e20717fa0541f39bd146692035c37bedfa532b3e5071b35761082407546b2a"
dependencies = [
"asn1-rs",
]
[[package]]
name = "once_cell"
version = "1.12.0"
@@ -2411,7 +2574,7 @@ dependencies = [
"base64",
"curve25519-dalek",
"digest",
"displaydoc",
"displaydoc 0.1.7",
"generic-array",
"generic-bytes",
"hkdf",
@@ -2423,61 +2586,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "openssl"
version = "0.10.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "111.22.0+1.1.1q"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f31f0d509d1c1ae9cada2f9539ff8f37933831fd5098879e482aa687d659853"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1"
dependencies = [
"autocfg 1.1.0",
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
[[package]]
name = "orion"
version = "0.16.1"
@@ -2880,9 +2994,9 @@ dependencies = [
[[package]]
name = "requestty"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20362058549acfb4b04b014aa182de685d7ded326eaf092db11030431c92693a"
checksum = "8d06fb394ca73d15ad0c7bbc673459506a851a84586cd90d67d42932a280281e"
dependencies = [
"requestty-ui",
"smallvec",
@@ -2891,9 +3005,9 @@ dependencies = [
[[package]]
name = "requestty-ui"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7b232b5ca049619df09d10909dfe32020bc0d402fd98cb5207fc1d2aa53633e"
checksum = "31a4bce6f730d12e36993944036e2f93e88033d8a78734d8734fdb0043662cae"
dependencies = [
"bitflags",
"crossterm",
@@ -2917,28 +3031,45 @@ dependencies = [
"http",
"http-body",
"hyper",
"hyper-tls",
"hyper-rustls",
"ipnet",
"js-sys",
"lazy_static",
"log",
"mime",
"native-tls",
"percent-encoding",
"pin-project-lite",
"rustls 0.20.6",
"rustls-pemfile",
"serde",
"serde_json",
"serde_urlencoded",
"tokio",
"tokio-native-tls",
"tokio-rustls 0.23.4",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots 0.22.4",
"winreg",
]
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin 0.5.2",
"untrusted",
"web-sys",
"winapi",
]
[[package]]
name = "rsa"
version = "0.5.0"
@@ -2995,6 +3126,61 @@ dependencies = [
"semver 1.0.12",
]
[[package]]
name = "rusticata-macros"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
dependencies = [
"nom 7.1.1",
]
[[package]]
name = "rustls"
version = "0.19.1"
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"
dependencies = [
"log",
"ring",
"sct 0.7.0",
"webpki 0.22.0",
]
[[package]]
name = "rustls-native-certs"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9"
dependencies = [
"base64",
]
[[package]]
name = "ryu"
version = "1.0.10"
@@ -3017,6 +3203,26 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "sct"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "sct"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "sea-query"
version = "0.25.2"
@@ -3125,6 +3331,15 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde_bytes"
version = "0.11.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfc50e8183eeeb6178dcb167ae34a8051d63535023ae38b5d8d12beae193d37b"
dependencies = [
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.137"
@@ -3357,12 +3572,13 @@ dependencies = [
"log",
"md-5",
"memchr",
"num-bigint",
"num-bigint 0.3.3",
"once_cell",
"paste",
"percent-encoding",
"rand 0.8.5",
"rsa",
"rustls 0.19.1",
"serde",
"serde_json",
"sha-1",
@@ -3374,6 +3590,8 @@ dependencies = [
"thiserror",
"tokio-stream",
"url",
"webpki 0.21.4",
"webpki-roots 0.21.1",
"whoami",
]
@@ -3403,10 +3621,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4db708cd3e459078f85f39f96a00960bd841f66ee2a669e90bf36907f5a79aae"
dependencies = [
"actix-rt",
"native-tls",
"once_cell",
"tokio",
"tokio-native-tls",
"tokio-rustls 0.22.0",
]
[[package]]
@@ -3608,11 +3825,23 @@ dependencies = [
"libc",
"standback",
"stdweb",
"time-macros",
"time-macros 0.1.1",
"version_check",
"winapi",
]
[[package]]
name = "time"
version = "0.3.11"
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",
]
[[package]]
name = "time-macros"
version = "0.1.1"
@@ -3623,6 +3852,12 @@ dependencies = [
"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"
@@ -3683,13 +3918,25 @@ dependencies = [
]
[[package]]
name = "tokio-native-tls"
version = "0.3.0"
name = "tokio-rustls"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6"
dependencies = [
"native-tls",
"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",
"tokio",
"webpki 0.22.0",
]
[[package]]
@@ -3938,6 +4185,12 @@ dependencies = [
"void",
]
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "url"
version = "2.2.2"
@@ -4132,6 +4385,44 @@ dependencies = [
"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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940"
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",
]
[[package]]
name = "whoami"
version = "1.2.1"
@@ -4225,6 +4516,24 @@ dependencies = [
"winapi",
]
[[package]]
name = "x509-parser"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb9bace5b5589ffead1afb76e43e34cff39cd0f3ce7e170ae0c29e53b88eb1c"
dependencies = [
"asn1-rs",
"base64",
"data-encoding",
"der-parser",
"lazy_static",
"nom 7.1.1",
"oid-registry",
"rusticata-macros",
"thiserror",
"time 0.3.11",
]
[[package]]
name = "yansi"
version = "0.5.1"
@@ -4317,7 +4626,7 @@ dependencies = [
[[package]]
name = "yew_form"
version = "0.1.8"
source = "git+https://github.com/sassman/yew_form/?rev=67050812695b7a8a90b81b0637e347fc6629daed#67050812695b7a8a90b81b0637e347fc6629daed"
source = "git+https://github.com/jfbilodeau/yew_form?rev=67050812695b7a8a90b81b0637e347fc6629daed#67050812695b7a8a90b81b0637e347fc6629daed"
dependencies = [
"validator",
"validator_derive",
@@ -4327,7 +4636,7 @@ dependencies = [
[[package]]
name = "yew_form_derive"
version = "0.1.8"
source = "git+https://github.com/sassman/yew_form/?rev=67050812695b7a8a90b81b0637e347fc6629daed#67050812695b7a8a90b81b0637e347fc6629daed"
source = "git+https://github.com/jfbilodeau/yew_form?rev=67050812695b7a8a90b81b0637e347fc6629daed#67050812695b7a8a90b81b0637e347fc6629daed"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -8,11 +8,7 @@ members = [
default-members = ["server"]
# TODO: remove when there's a new release.
[patch.crates-io.yew_form]
git = 'https://github.com/sassman/yew_form/'
rev = '67050812695b7a8a90b81b0637e347fc6629daed'
[patch.crates-io.yew_form_derive]
git = 'https://github.com/sassman/yew_form/'
rev = '67050812695b7a8a90b81b0637e347fc6629daed'
# 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'

View File

@@ -36,7 +36,7 @@
- [Client configuration](#Client-configuration)
- [Compatible services](#compatible-services)
- [General configuration guide](#general-configuration-guide)
- [Sample cient configurations](#Sample-client-configurations)
- [Sample client configurations](#Sample-client-configurations)
- [Comparisons with other services](#Comparisons-with-other-services)
- [vs OpenLDAP](#vs-openldap)
- [vs FreeIPA](#vs-freeipa)
@@ -90,14 +90,19 @@ Configure the server by copying the `lldap_config.docker_template.toml` to
Environment variables should be prefixed with `LLDAP_` to override the
configuration.
If the `lldap_config.toml` doesn't exist when starting up, LLDAP will use default one. The default admin password is `password`, you can change the password later using the web interface.
Secrets can also be set through a file. The filename should be specified by the
variables `LLDAP_JWT_SECRET_FILE` or `LLDAP_LDAP_USER_PASS_FILE`, and the file
contents are loaded into the respective configuration parameters. Note that
`_FILE` variables take precedence.
Example for docker compose:
Example for docker compose for `:stable` tag:
* When defined with `user: ##:##` , ensure `/data` directory had permission for the defined user, else `1000:1000` used.
```yaml
version: '3'
volumes:
lldap_data:
driver: local
@@ -122,11 +127,56 @@ services:
- LLDAP_LDAP_BASE_DN=dc=example,dc=com
```
Example for docker compose for `:latest` tag:
* `:latest` tag image contain recent pushed codes or feature test, breaks is expected.
* If `UID` and `GID` no defined LLDAP will use default `UID` and `GID` number `1000`
```yaml
version: '3'
volumes:
lldap_data:
driver: local
services:
lldap:
image: nitnelave/lldap:latest
ports:
# For LDAP
- "3890:3890"
# For the web front-end
- "17170:17170"
volumes:
- "lldap_data:/data"
# Alternatively, you can mount a local folder
# - "./lldap_data:/data"
environment:
- UID=####
- GID=####
- LLDAP_JWT_SECRET=REPLACE_WITH_RANDOM
- LLDAP_LDAP_USER_PASS=REPLACE_WITH_PASSWORD
- LLDAP_LDAP_BASE_DN=dc=example,dc=com
```
Then the service will listen on two ports, one for LDAP and one for the web
front-end.
### From source
To compile the project, you'll need:
* npm, curl: `sudo apt install curl npm`
* Rust/Cargo: [rustup.rs](https://rustup.rs/)
Then you can compile the server (and the migration tool if you want):
```shell
cargo build --release -p lldap -p migration-tool
```
The resulting binaries will be in `./target/release/`. Alternatively, you can
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:
@@ -136,10 +186,13 @@ cargo, you'll need:
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).
To bring up the server, just run `cargo run`. The default config is in
`src/infra/configuration.rs`, but you can override it by creating an
`lldap_config.toml`, setting environment variables or passing arguments to
`cargo run`.
The default config is in `src/infra/configuration.rs`, but you can override it
by creating an `lldap_config.toml`, setting environment variables or passing
arguments to `cargo run`. Have a look at the docker template:
`lldap_config.docker_template.toml`.
You can also install it as a systemd service, see
[lldap.service](example_configs/lldap.service).
### Cross-compilation
@@ -198,23 +251,28 @@ 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)
- [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)
- [Jisti Meet](example_configs/jitsi_meet.conf)
- [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)
- [WG Portal](example_configs/wg_portal.env.example)
- [XBackBone](example_configs/xbackbone_config.php)
## Comparisons with other services
@@ -232,7 +290,7 @@ 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.
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 wed UI.
you add PhpLdapAdmin), and comes packed with its own purpose-built web UI.
### vs FreeIPA
@@ -244,7 +302,7 @@ 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.
LLDAP is much lighter to run (<100 MB RAM including the DB), easier to
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.

View File

@@ -1,11 +1,12 @@
[package]
name = "lldap_app"
version = "0.4.0"
version = "0.4.1"
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
edition = "2021"
[dependencies]
anyhow = "1"
base64 = "0.13"
graphql_client = "0.10"
http = "0.2"
jwt = "0.13"
@@ -18,8 +19,6 @@ wasm-bindgen = "0.2"
yew = "0.18"
yewtil = "*"
yew-router = "0.15"
yew_form = "0.1.8"
yew_form_derive = "*"
# Needed because of https://github.com/tkaitchuck/aHash/issues/95
indexmap = "=1.6.2"
@@ -29,6 +28,7 @@ version = "0.3"
features = [
"Document",
"Element",
"FileReader",
"HtmlDocument",
"HtmlInputElement",
"HtmlOptionElement",
@@ -47,5 +47,18 @@ features = [
path = "../auth"
features = [ "opaque_client" ]
[dependencies.image]
features = ["jpeg"]
default-features = false
version = "0.24"
[dependencies.yew_form]
git = "https://github.com/jfbilodeau/yew_form"
rev = "67050812695b7a8a90b81b0637e347fc6629daed"
[dependencies.yew_form_derive]
git = "https://github.com/jfbilodeau/yew_form"
rev = "67050812695b7a8a90b81b0637e347fc6629daed"
[lib]
crate-type = ["cdylib"]

View File

@@ -2,6 +2,8 @@ query GetGroupDetails($id: Int!) {
group(groupId: $id) {
id
displayName
creationDate
uuid
users {
id
displayName

View File

@@ -2,5 +2,6 @@ query GetGroupList {
groups {
id
displayName
creationDate
}
}

View File

@@ -5,7 +5,9 @@ query GetUserDetails($id: String!) {
displayName
firstName
lastName
avatar
creationDate
uuid
groups {
id
displayName

View File

@@ -36,7 +36,7 @@ impl OpaqueData {
}
/// The fields of the form, with the constraints.
#[derive(Model, Validate, PartialEq, Clone, Default)]
#[derive(Model, Validate, PartialEq, Eq, Clone, Default)]
pub struct FormModel {
#[validate(custom(
function = "empty_or_long",
@@ -64,7 +64,7 @@ pub struct ChangePasswordForm {
route_dispatcher: RouteAgentDispatcher,
}
#[derive(Clone, PartialEq, Properties)]
#[derive(Clone, PartialEq, Eq, Properties)]
pub struct Props {
pub username: String,
pub is_admin: bool,
@@ -252,6 +252,7 @@ impl Component for ChangePasswordForm {
<Field
form=&self.form
field_name="password"
input_type="password"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
@@ -271,6 +272,7 @@ impl Component for ChangePasswordForm {
<Field
form=&self.form
field_name="confirm_password"
input_type="password"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"

View File

@@ -28,7 +28,7 @@ pub struct CreateGroupForm {
form: yew_form::Form<CreateGroupModel>,
}
#[derive(Model, Validate, PartialEq, Clone, Default)]
#[derive(Model, Validate, PartialEq, Eq, Clone, Default)]
pub struct CreateGroupModel {
#[validate(length(min = 1, message = "Groupname is required"))]
groupname: String,

View File

@@ -32,7 +32,7 @@ pub struct CreateUserForm {
form: yew_form::Form<CreateUserModel>,
}
#[derive(Model, Validate, PartialEq, Clone, Default)]
#[derive(Model, Validate, PartialEq, Eq, Clone, Default)]
pub struct CreateUserModel {
#[validate(length(min = 1, message = "Username is required"))]
username: String,
@@ -90,6 +90,7 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
displayName: to_option(model.display_name),
firstName: to_option(model.first_name),
lastName: to_option(model.last_name),
avatar: None,
},
};
self.common.call_graphql::<CreateUser, _>(

View File

@@ -40,7 +40,7 @@ pub enum Msg {
OnUserRemovedFromGroup((String, i64)),
}
#[derive(yew::Properties, Clone, PartialEq)]
#[derive(yew::Properties, Clone, PartialEq, Eq)]
pub struct Props {
pub group_id: i64,
}
@@ -68,6 +68,45 @@ impl GroupDetails {
}
}
fn view_details(&self, g: &Group) -> Html {
html! {
<>
<h3>{g.display_name.to_string()}</h3>
<div class="py-3">
<form class="form">
<div class="form-group row mb-3">
<label for="displayName"
class="form-label col-4 col-form-label">
{"Group: "}
</label>
<div class="col-8">
<span id="groupId" class="form-constrol-static">{g.display_name.to_string()}</span>
</div>
</div>
<div class="form-group row mb-3">
<label for="creationDate"
class="form-label col-4 col-form-label">
{"Creation date: "}
</label>
<div class="col-8">
<span id="creationDate" class="form-constrol-static">{g.creation_date.date().naive_local()}</span>
</div>
</div>
<div class="form-group row mb-3">
<label for="uuid"
class="form-label col-4 col-form-label">
{"UUID: "}
</label>
<div class="col-8">
<span id="uuid" class="form-constrol-static">{g.uuid.to_string()}</span>
</div>
</div>
</form>
</div>
</>
}
}
fn view_user_list(&self, g: &Group) -> Html {
let make_user_row = |user: &User| {
let user_id = user.id.clone();
@@ -92,7 +131,6 @@ impl GroupDetails {
};
html! {
<>
<h3>{g.display_name.to_string()}</h3>
<h5 class="fw-bold">{"Members"}</h5>
<div class="table-responsive">
<table class="table table-striped">
@@ -201,6 +239,7 @@ impl Component for GroupDetails {
(Some(u), error) => {
html! {
<div>
{self.view_details(u)}
{self.view_user_list(u)}
{self.view_add_user_button(u)}
{self.view_messages(error)}

View File

@@ -13,7 +13,7 @@ use yew::prelude::*;
#[graphql(
schema_path = "../schema.graphql",
query_path = "queries/get_group_list.graphql",
response_derives = "Debug,Clone,PartialEq",
response_derives = "Debug,Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql"
)]
pub struct GetGroupList;
@@ -97,7 +97,8 @@ impl GroupTable {
<table class="table table-striped">
<thead>
<tr>
<th>{"Groups"}</th>
<th>{"Group name"}</th>
<th>{"Creation date"}</th>
<th>{"Delete"}</th>
</tr>
</thead>
@@ -122,6 +123,9 @@ impl GroupTable {
{&group.display_name}
</Link>
</td>
<td>
{&group.creation_date.date().naive_local()}
</td>
<td>
<DeleteGroup
group=group.clone()

View File

@@ -19,7 +19,7 @@ pub struct LoginForm {
}
/// The fields of the form, with the constraints.
#[derive(Model, Validate, PartialEq, Clone, Default)]
#[derive(Model, Validate, PartialEq, Eq, Clone, Default)]
pub struct FormModel {
#[validate(length(min = 1, message = "Missing username"))]
username: String,

View File

@@ -18,7 +18,7 @@ pub struct ResetPasswordStep1Form {
}
/// The fields of the form, with the constraints.
#[derive(Model, Validate, PartialEq, Clone, Default)]
#[derive(Model, Validate, PartialEq, Eq, Clone, Default)]
pub struct FormModel {
#[validate(length(min = 1, message = "Missing username"))]
username: String,

View File

@@ -20,7 +20,7 @@ use yew_router::{
};
/// The fields of the form, with the constraints.
#[derive(Model, Validate, PartialEq, Clone, Default)]
#[derive(Model, Validate, PartialEq, Eq, Clone, Default)]
pub struct FormModel {
#[validate(length(min = 8, message = "Invalid password. Min length: 8"))]
password: String,
@@ -36,7 +36,7 @@ pub struct ResetPasswordStep2Form {
route_dispatcher: RouteAgentDispatcher,
}
#[derive(Clone, PartialEq, Properties)]
#[derive(Clone, PartialEq, Eq, Properties)]
pub struct Props {
pub token: String,
}

View File

@@ -81,7 +81,7 @@ pub struct SelectOption {
props: SelectOptionProps,
}
#[derive(yew::Properties, Clone, PartialEq, Debug)]
#[derive(yew::Properties, Clone, PartialEq, Eq, Debug)]
pub struct SelectOptionProps {
pub value: String,
pub text: String,

View File

@@ -40,7 +40,7 @@ pub enum Msg {
OnUserRemovedFromGroup((String, i64)),
}
#[derive(yew::Properties, Clone, PartialEq)]
#[derive(yew::Properties, Clone, PartialEq, Eq)]
pub struct Props {
pub username: String,
pub is_admin: bool,
@@ -198,8 +198,7 @@ impl Component for UserDetails {
<>
<h3>{u.id.to_string()}</h3>
<UserDetailsForm
user=u.clone()
on_error=self.common.callback(Msg::OnError)/>
user=u.clone() />
<div class="row justify-content-center">
<NavButton
route=AppRoute::ChangePassword(u.id.clone())

View File

@@ -1,3 +1,5 @@
use std::str::FromStr;
use crate::{
components::user_details::User,
infra::common_component::{CommonComponent, CommonComponentParts},
@@ -5,11 +7,39 @@ use crate::{
use anyhow::{bail, Error, Result};
use graphql_client::GraphQLQuery;
use validator_derive::Validate;
use yew::prelude::*;
use wasm_bindgen::JsCast;
use yew::{prelude::*, services::ConsoleService};
use yew_form_derive::Model;
#[derive(PartialEq, Eq, Clone, Default)]
struct JsFile {
file: Option<web_sys::File>,
contents: Option<Vec<u8>>,
}
impl ToString for JsFile {
fn to_string(&self) -> String {
self.file
.as_ref()
.map(web_sys::File::name)
.unwrap_or_else(String::new)
}
}
impl FromStr for JsFile {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
if s.is_empty() {
Ok(JsFile::default())
} else {
bail!("Building file from non-empty string")
}
}
}
/// The fields of the form, with the editable details and the constraints.
#[derive(Model, Validate, PartialEq, Clone)]
#[derive(Model, Validate, PartialEq, Eq, Clone)]
pub struct UserModel {
#[validate(email)]
email: String,
@@ -25,7 +55,7 @@ pub struct UserModel {
schema_path = "../schema.graphql",
query_path = "queries/update_user.graphql",
response_derives = "Debug",
variables_derives = "Clone,PartialEq",
variables_derives = "Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql"
)]
pub struct UpdateUser;
@@ -34,6 +64,7 @@ pub struct UpdateUser;
pub struct UserDetailsForm {
common: CommonComponentParts<Self>,
form: yew_form::Form<UserModel>,
avatar: JsFile,
/// True if we just successfully updated the user, to display a success message.
just_updated: bool,
}
@@ -43,24 +74,68 @@ pub enum Msg {
Update,
/// The "Submit" button was clicked.
SubmitClicked,
/// A picked file finished loading.
FileLoaded(yew::services::reader::FileData),
/// We got the response from the server about our update message.
UserUpdated(Result<update_user::ResponseData>),
}
#[derive(yew::Properties, Clone, PartialEq)]
#[derive(yew::Properties, Clone, PartialEq, Eq)]
pub struct Props {
/// The current user details.
pub user: User,
/// Callback to report errors (e.g. server error).
pub on_error: Callback<Error>,
}
impl CommonComponent<UserDetailsForm> for UserDetailsForm {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
match msg {
Msg::Update => Ok(true),
Msg::Update => {
let window = web_sys::window().expect("no global `window` exists");
let document = window.document().expect("should have a document on window");
let input = document
.get_element_by_id("avatarInput")
.expect("Form field avatarInput should be present")
.dyn_into::<web_sys::HtmlInputElement>()
.expect("Should be an HtmlInputElement");
ConsoleService::log("Form update");
if let Some(files) = input.files() {
ConsoleService::log("Got file list");
if files.length() > 0 {
ConsoleService::log("Got a file");
let new_avatar = JsFile {
file: files.item(0),
contents: None,
};
if self.avatar.file.as_ref().map(|f| f.name())
!= new_avatar.file.as_ref().map(|f| f.name())
{
if let Some(ref file) = new_avatar.file {
self.mut_common().read_file(file.clone(), Msg::FileLoaded)?;
}
self.avatar = new_avatar;
}
}
}
Ok(true)
}
Msg::SubmitClicked => self.submit_user_update_form(),
Msg::UserUpdated(response) => self.user_update_finished(response),
Msg::FileLoaded(data) => {
self.common.cancel_task();
if let Some(file) = &self.avatar.file {
if file.name() == data.name {
if !is_valid_jpeg(data.content.as_slice()) {
// Clear the selection.
self.avatar = JsFile::default();
bail!("Chosen image is not a valid JPEG");
} else {
self.avatar.contents = Some(data.content);
return Ok(true);
}
}
}
Ok(false)
}
}
}
@@ -83,17 +158,14 @@ impl Component for UserDetailsForm {
Self {
common: CommonComponentParts::<Self>::create(props, link),
form: yew_form::Form::new(model),
avatar: JsFile::default(),
just_updated: false,
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
self.just_updated = false;
CommonComponentParts::<Self>::update_and_report_error(
self,
msg,
self.common.on_error.clone(),
)
CommonComponentParts::<Self>::update(self, msg)
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
@@ -102,6 +174,9 @@ impl Component for UserDetailsForm {
fn view(&self) -> Html {
type Field = yew_form::Field<UserModel>;
let avatar_base64 = maybe_to_base64(&self.avatar).unwrap_or_default();
let avatar_string = avatar_base64.as_ref().unwrap_or(&self.common.user.avatar);
html! {
<div class="py-3">
<form class="form">
@@ -111,7 +186,24 @@ impl Component for UserDetailsForm {
{"User ID: "}
</label>
<div class="col-8">
<span id="userId" class="form-constrol-static">{&self.common.user.id}</span>
<span id="userId" class="form-constrol-static"><b>{&self.common.user.id}</b></span>
</div>
</div>
<div class="form-group row mb-3">
<div class="col-4 col-form-label">
<img
id="avatarDisplay"
src={format!("data:image/jpeg;base64, {}", avatar_string)}
style="max-height:128px;max-width:128px;height:auto;width:auto;"
alt="Avatar" />
</div>
<div class="col-8">
<input
class="form-control"
id="avatarInput"
type="file"
accept="image/jpeg"
oninput=self.common.callback(|_| Msg::Update) />
</div>
</div>
<div class="form-group row mb-3">
@@ -195,6 +287,15 @@ impl Component for UserDetailsForm {
<span id="creationDate" class="form-constrol-static">{&self.common.user.creation_date.date().naive_local()}</span>
</div>
</div>
<div class="form-group row mb-3">
<label for="uuid"
class="form-label col-4 col-form-label">
{"UUID: "}
</label>
<div class="col-8">
<span id="creationDate" class="form-constrol-static">{&self.common.user.uuid}</span>
</div>
</div>
<div class="form-group row justify-content-center">
<button
type="submit"
@@ -205,6 +306,14 @@ impl Component for UserDetailsForm {
</button>
</div>
</form>
{ if let Some(e) = &self.common.error {
html! {
<div class="alert alert-danger">
{e.to_string() }
</div>
}
} else { html! {} }
}
<div hidden=!self.just_updated>
<span>{"User successfully updated!"}</span>
</div>
@@ -215,9 +324,19 @@ impl Component for UserDetailsForm {
impl UserDetailsForm {
fn submit_user_update_form(&mut self) -> Result<bool> {
ConsoleService::log("Submit");
if !self.form.validate() {
bail!("Invalid inputs");
}
ConsoleService::log("Valid inputs");
if let JsFile {
file: Some(_),
contents: None,
} = &self.avatar
{
bail!("Image file hasn't finished loading, try again");
}
ConsoleService::log("File is correctly loaded");
let base_user = &self.common.user;
let mut user_input = update_user::UpdateUserInput {
id: self.common.user.id.clone(),
@@ -225,6 +344,7 @@ impl UserDetailsForm {
displayName: None,
firstName: None,
lastName: None,
avatar: None,
};
let default_user_input = user_input.clone();
let model = self.form.model();
@@ -241,11 +361,14 @@ impl UserDetailsForm {
if base_user.last_name != model.last_name {
user_input.lastName = Some(model.last_name);
}
user_input.avatar = maybe_to_base64(&self.avatar)?;
// Nothing changed.
if user_input == default_user_input {
ConsoleService::log("No changes");
return Ok(false);
}
let req = update_user::Variables { user: user_input };
ConsoleService::log("Querying");
self.common.call_graphql::<UpdateUser, _>(
req,
Msg::UserUpdated,
@@ -260,18 +383,44 @@ impl UserDetailsForm {
Err(e) => return Err(e),
Ok(_) => {
let model = self.form.model();
self.common.user = User {
id: self.common.user.id.clone(),
email: model.email,
display_name: model.display_name,
first_name: model.first_name,
last_name: model.last_name,
creation_date: self.common.user.creation_date,
groups: self.common.user.groups.clone(),
};
self.common.user.email = model.email;
self.common.user.display_name = model.display_name;
self.common.user.first_name = model.first_name;
self.common.user.last_name = model.last_name;
if let Some(avatar) = maybe_to_base64(&self.avatar)? {
self.common.user.avatar = avatar;
}
self.just_updated = true;
}
};
Ok(true)
}
}
fn is_valid_jpeg(bytes: &[u8]) -> bool {
image::io::Reader::with_format(std::io::Cursor::new(bytes), image::ImageFormat::Jpeg)
.decode()
.is_ok()
}
fn maybe_to_base64(file: &JsFile) -> Result<Option<String>> {
match file {
JsFile {
file: None,
contents: _,
} => Ok(None),
JsFile {
file: Some(_),
contents: None,
} => bail!("Image file hasn't finished loading, try again"),
JsFile {
file: Some(_),
contents: Some(data),
} => {
if !is_valid_jpeg(data.as_slice()) {
bail!("Chosen image is not a valid JPEG");
}
Ok(Some(base64::encode(data)))
}
}
}

View File

@@ -26,7 +26,11 @@ use anyhow::{Error, Result};
use graphql_client::GraphQLQuery;
use yew::{
prelude::*,
services::{fetch::FetchTask, ConsoleService},
services::{
fetch::FetchTask,
reader::{FileData, ReaderService, ReaderTask},
ConsoleService,
},
};
use yewtil::NeqAssign;
@@ -40,13 +44,34 @@ pub trait CommonComponent<C: Component + CommonComponent<C>>: Component {
fn mut_common(&mut self) -> &mut CommonComponentParts<C>;
}
enum AnyTask {
None,
FetchTask(FetchTask),
ReaderTask(ReaderTask),
}
impl AnyTask {
fn is_some(&self) -> bool {
!matches!(self, AnyTask::None)
}
}
impl From<Option<FetchTask>> for AnyTask {
fn from(task: Option<FetchTask>) -> Self {
match task {
Some(t) => AnyTask::FetchTask(t),
None => AnyTask::None,
}
}
}
/// Structure that contains the common parts needed by most components.
/// The fields of [`props`] are directly accessible through a `Deref` implementation.
pub struct CommonComponentParts<C: CommonComponent<C>> {
link: ComponentLink<C>,
pub props: <C as Component>::Properties,
pub error: Option<Error>,
task: Option<FetchTask>,
task: AnyTask,
}
impl<C: CommonComponent<C>> CommonComponentParts<C> {
@@ -57,7 +82,7 @@ impl<C: CommonComponent<C>> CommonComponentParts<C> {
/// Cancel any background task.
pub fn cancel_task(&mut self) {
self.task = None;
self.task = AnyTask::None;
}
pub fn create(props: <C as Component>::Properties, link: ComponentLink<C>) -> Self {
@@ -65,7 +90,7 @@ impl<C: CommonComponent<C>> CommonComponentParts<C> {
link,
props,
error: None,
task: None,
task: AnyTask::None,
}
}
@@ -131,7 +156,7 @@ impl<C: CommonComponent<C>> CommonComponentParts<C> {
M: Fn(Req, Callback<Resp>) -> Result<FetchTask>,
Cb: FnOnce(Resp) -> <C as Component>::Message + 'static,
{
self.task = Some(method(req, self.link.callback_once(callback))?);
self.task = AnyTask::FetchTask(method(req, self.link.callback_once(callback))?);
Ok(())
}
@@ -156,7 +181,19 @@ impl<C: CommonComponent<C>> CommonComponentParts<C> {
ConsoleService::log(&e.to_string());
self.error = Some(e);
})
.ok();
.ok()
.into();
}
pub(crate) fn read_file<Cb>(&mut self, file: web_sys::File, callback: Cb) -> Result<()>
where
Cb: FnOnce(FileData) -> <C as Component>::Message + 'static,
{
self.task = AnyTask::ReaderTask(ReaderService::read_file(
file,
self.link.callback_once(callback),
)?);
Ok(())
}
}

View File

@@ -1,20 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
for SECRET in LLDAP_JWT_SECRET LLDAP_LDAP_USER_PASS; do
FILE_VAR="${SECRET}_FILE"
SECRET_FILE="${!FILE_VAR:-}"
if [[ -n "$SECRET_FILE" ]]; then
if [[ -f "$SECRET_FILE" ]]; then
declare "$SECRET=$(cat $SECRET_FILE)"
export "$SECRET"
echo "[entrypoint] Set $SECRET from $SECRET_FILE"
else
echo "[entrypoint] Could not read contents of $SECRET_FILE (specified in $FILE_VAR)" >&2
fi
fi
done
CONFIG_FILE=/data/lldap_config.toml
if [[ ( ! -w "/data" ) ]] || [[ ( ! -d "/data" ) ]]; then
@@ -35,4 +21,13 @@ if [[ ! -r "$CONFIG_FILE" ]]; then
exit 1;
fi
exec /app/lldap "$@"
echo "> Setup permissions.."
find /app \! -user "$UID" -exec chown "$UID:$GID" '{}' +
find /data \! -user "$UID" -exec chown "$UID:$GID" '{}' +
echo "> Starting lldap.."
echo ""
exec gosu "$UID:$GID" /app/lldap "$@"
exec "$@"

View File

@@ -0,0 +1,26 @@
# Configuration for Airsonic Advanced
Replace `dc=example,dc=com` with your LLDAP configured domain.
### LDAP URL
```
ldap://lldap:3890/ou=people,dc=example,dc=com
```
### LDAP search filter
```
(&(uid={0})(memberof=cn=airsonic,ou=groups,dc=example,dc=com))
```
### LDAP manager DN
```
cn=admin,ou=people,dc=example,dc=com
```
### Password
```
admin-password
```
Make sure the box `Automatically create users in Airsonic` is checked.
Restart airsonic-advanced

View File

@@ -0,0 +1,25 @@
# Configuration for dokuwiki
LDAP configuration is in ```/dokuwiki/conf/local.protected.php```:
```
<?php
$conf['useacl'] = 1; //enable ACL
$conf['authtype'] = 'authldap'; //enable this Auth plugin
$conf['plugin']['authldap']['server'] = 'ldap://lldap_server:3890'; #IP of your lldap
$conf['plugin']['authldap']['usertree'] = 'ou=people,dc=example,dc=com';
$conf['plugin']['authldap']['grouptree'] = 'ou=groups, dc=example, dc=com';
$conf['plugin']['authldap']['userfilter'] = '(&(uid=%{user})(objectClass=person))';
$conf['plugin']['authldap']['groupfilter'] = '(&(objectClass=group)(memberUID=member))';
$conf['plugin']['authldap']['attributes'] = array('cn', 'displayname', 'mail', 'givenname', 'objectclass', 'sn', 'uid', 'memberof');
$conf['plugin']['authldap']['version'] = 3;
$conf['plugin']['authldap']['binddn'] = 'cn=admin,ou=people,dc=example,dc=com';
$conf['plugin']['authldap']['bindpw'] = 'ENTER_YOUR_LLDAP_PASSWORD';
```
DokuWiki by default, ships with an LDAP Authentication Plugin called ```authLDAP``` that allows authentication against an LDAP directory.
All you need to do is to activate the plugin. This can be done on the DokuWiki Extensions Manager.
Once the LDAP settings are defined, proceed to define the default authentication method.
Navigate to Table of Contents > DokuWiki > Authentication.
On the Authentication backend, select ```authldap``` and save the changes.

View File

@@ -20,7 +20,7 @@ ssl_skip_verify = false
# client_key = "/path/to/client.key"
# Search user bind dn
bind_dn = "cn=<your grafana user>,ou=people,dc=example,dc=org"
bind_dn = "uid=<your grafana user>,ou=people,dc=example,dc=org"
# Search user bind password
# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
bind_password = "<grafana user password>"
@@ -44,6 +44,6 @@ username = "uid"
# If you want to map your ldap groups to grafana's groups, see: https://grafana.com/docs/grafana/latest/auth/ldap/#group-mappings
# As a quick example, here is how you would map lldap's admin group to grafana's admin
# [[servers.group_mappings]]
# group_dn = "cn=lldap_admin,ou=groups,c=example,dc=org"
# group_dn = "uid=lldap_admin,ou=groups,dc=example,dc=org"
# org_role = "Admin"
# grafana_admin = true

View File

@@ -0,0 +1,16 @@
# Configuration for hedgedoc
[Hedgedoc](https://hedgedoc.org/) is a platform to write and share markdown.
### Using docker variables
Any member of the group ```hedgedoc``` can log into hedgedoc.
```
- CMD_LDAP_URL=ldap://lldap:3890
- CMD_LDAP_BINDDN=uid=admin,ou=people,dc=example,dc=com
- CMD_LDAP_BINDCREDENTIALS=insert_your_password
- CMD_LDAP_SEARCHBASE=ou=people,dc=example,dc=com
- CMD_LDAP_SEARCHFILTER=(&(memberOf=cn=hedgedoc,ou=groups,dc=example,dc=com)(uid={{username}}))
- CMD_LDAP_USERIDFIELD=uid
```
Replace `dc=example,dc=com` with your LLDAP configured domain for all occurances

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

@@ -0,0 +1,22 @@
[Unit]
Description=Nitnelave LLDAP
Documentation=https://github.com/nitnelave/lldap
# Only sqlite
After=network.target
[Service]
# The user/group LLDAP is run under. The working directory (see below) should allow write and read access to this user/group.
User=root
Group=root
# The location of the compiled binary
ExecStart=/opt/nitnelave/lldap \
run
# Only allow writes to the following directory and set it to the working directory (user and password data are stored here).
WorkingDirectory=/opt/nitnelave/
ReadWriteDirectories=/opt/nitnelave/
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,111 @@
# Nextcloud LLDAP example config
## lldap users & groups
This example is using following users & groups in lldap :
* A technical user (ex: `ro_admin`), member of `lldap_strict_readonly` or `lldap_password_manager`
* Several accounts, members of `users` group will be authorized to log in Nextcloud (eg neither `admin` nor `ro_admin`)
* Some "application" groups, let's say `friends` and `family`: users in Nextcloud will be able to share files and view people in dynamic lists only to members of their own group(s)
## Nextcloud config : the cli way
TL;DR let's script it. The "user_ldap" application is shipped with default Nextcloud installation (at least using Docker official stable images), you just have to install & enable it :
```sh
occ app:install user_ldap
occ app:enable user_ldap
occ ldap:create-empty-config
# EDIT: domain
occ ldap:set-config s01 ldapHost "ldap://lldap.example.net."
occ ldap:set-config s01 ldapPort 3890
# EDIT: admin user
occ ldap:set-config s01 ldapAgentName "uid=ro_admin,ou=people,dc=example,dc=com"
# EDIT: password
occ ldap:set-config s01 ldapAgentPassword "password"
# EDIT: Base DN
occ ldap:set-config s01 ldapBase "dc=example,dc=com"
occ ldap:set-config s01 ldapBaseUsers "dc=example,dc=com"
occ ldap:set-config s01 ldapBaseGroups "dc=example,dc=com"
occ ldap:set-config s01 ldapConfigurationActive 1
occ ldap:set-config s01 ldapLoginFilter "(&(objectclass=person)(uid=%uid))"
# EDIT: users group, contains the users who can login to Nextcloud
occ ldap:set-config s01 ldapUserFilter "(&(objectclass=person)(memberOf=cn=users,ou=groups,dc=example,dc=com))"
occ ldap:set-config s01 ldapUserFilterMode 0
occ ldap:set-config s01 ldapUserFilterObjectclass person
occ ldap:set-config s01 turnOnPasswordChange 0
occ ldap:set-config s01 ldapCacheTTL 600
occ ldap:set-config s01 ldapExperiencedAdmin 0
occ ldap:set-config s01 ldapGidNumber gidNumber
# EDIT: list of application groups
occ ldap:set-config s01 ldapGroupFilter "(&(objectclass=groupOfUniqueNames)(|(cn=friends)(cn=family)))"
# EDIT: list of application groups
occ ldap:set-config s01 ldapGroupFilterGroups "friends;family"
occ ldap:set-config s01 ldapGroupFilterMode 0
occ ldap:set-config s01 ldapGroupDisplayName cn
occ ldap:set-config s01 ldapGroupFilterObjectclass groupOfUniqueNames
occ ldap:set-config s01 ldapGroupMemberAssocAttr uniqueMember
occ ldap:set-config s01 ldapLoginFilterEmail 0
occ ldap:set-config s01 ldapLoginFilterUsername 1
occ ldap:set-config s01 ldapMatchingRuleInChainState unknown
occ ldap:set-config s01 ldapNestedGroups 0
occ ldap:set-config s01 ldapPagingSize 500
occ ldap:set-config s01 ldapTLS 0
occ ldap:set-config s01 ldapUserAvatarRule default
occ ldap:set-config s01 ldapUserDisplayName displayname
occ ldap:set-config s01 ldapUserFilterMode 1
occ ldap:set-config s01 ldapUuidGroupAttribute auto
occ ldap:set-config s01 ldapUuidUserAttribute auto
```
With small amount of luck, you should be able to log in your nextcloud instance with LLDAP accounts in the `users` group.
## Nextcloud config : the GUI way
1. enable LDAP application (installed but not enabled by default)
2. setup your ldap server in Settings > Administration > LDAP / AD integration
3. setup Group limitations
### LDAP server config
Fill the LLDAP domain and port, DN + password of your technical account and base DN (as usual : change `example.com` by your own domain) :
![ldap configuration page](images/nextcloud_ldap_srv.png)
### Users tab
Select `person` as object class and then choose `Edit LDAP Query` : the `only from these groups` option is not functional.
We want only users from the `users` group to be allowed to log in Nextcloud :
```
(&(objectclass=person)(memberOf=cn=users,ou=groups,dc=example,dc=com))
```
![login configuration page](images/nextcloud_loginfilter.png)
You can check with `Verify settings and count users` that your filter is working properly (here your accounts `admin` and `ro_admin` will not be counted as users).
### Login attributes
Select `Edit LDAP Query` and enter :
```
(&(objectclass=person)(uid=%uid))
```
![login attributes page](images/nextcloud_login_attributes.png)
Enter a valid username in lldap and check if your filter is working.
### Groups
You can use the menus for this part : select `groupOfUniqueNames` in the first menu and check every group you want members to be allowed to view their group member / share files with.
![groups configuration page](images/nextcloud_groups.png)
The resulting LDAP filter could be simplified removing the first 'OR' condition (I think).
## Sharing restrictions
Go to Settings > Administration > Sharing and check following boxes :
* "Allow username autocompletion to users within the same groups"
* "Restrict users to only share with users in their groups"
![sharing options](images/nextcloud_sharing_options.png)

View File

@@ -0,0 +1,21 @@
<?php
return array (
'ldap' =>
array (
'enabled' => true,
'schema' => 'ldap',
// If using same docker network, use 'lldap', otherwise put ip/hostname
'host' => 'lldap',
// Normal ldap port is 389, standard in LLDAP is 3890
'port' => 3890,
'base_domain' => 'ou=people,dc=example,dc=com',
// ???? is replaced with user-provided username, authenticates users in an lldap group called "xbackbone"
// Remove the "(memberof=...)" if you want to allow all users.
'search_filter' => '(&(uid=????)(objectClass=person)(memberof=cn=xbackbone,ou=groups,dc=example,dc=com))',
// the attribute to use as username
'rdn_attribute' => 'uid',
// LDAP admin/service account info below
'service_account_dn' => 'cn=admin,ou=people,dc=example,dc=com',
'service_account_password' => 'REPLACE_ME',
),
);

View File

@@ -45,6 +45,11 @@
## For the administration interface, this is the username.
#ldap_user_dn = "admin"
## Admin email.
## Email for the admin account. It is only used when initially creating
## the admin user, and can safely be omitted.
#ldap_user_email = "admin@example.com"
## Admin password.
## Password for the admin account, both for the LDAP bind and for the
## administration interface. It is only used when initially creating
@@ -96,8 +101,8 @@ key_file = "/data/private_key"
#server="smtp.gmail.com"
## The SMTP port.
#port=587
## Whether to connect with TLS.
#tls_required=true
## How the connection is encrypted, either "TLS" or "STARTTLS".
#smtp_encryption = "TLS"
## The SMTP user, usually your email address.
#user="sender@gmail.com"
## The SMTP password.

View File

@@ -1,23 +1,33 @@
[package]
name = "migration-tool"
version = "0.3.0-alpha.1"
version = "0.4.1"
edition = "2021"
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
[dependencies]
anyhow = "*"
graphql_client = "0.10"
ldap3 = "*"
base64 = "0.13"
rand = "0.8"
requestty = "*"
requestty = "0.4.1"
serde = "1"
serde_json = "1"
smallvec = "*"
[dependencies.lldap_auth]
path = "../auth"
features = [ "opaque_client" ]
features = ["opaque_client"]
[dependencies.graphql_client]
features = ["graphql_query_derive", "reqwest-rustls"]
default-features = false
version = "0.11"
[dependencies.reqwest]
version = "*"
features = [ "json", "blocking" ]
default-features = false
features = ["json", "blocking", "rustls-tls"]
[dependencies.ldap3]
version = "*"
default-features = false
features = ["sync", "tls-rustls"]

View File

@@ -131,7 +131,7 @@ fn bind_ldap(
};
if let Err(e) = ldap_connection
.simple_bind(&binddn, &password)
.and_then(|r| r.success())
.and_then(ldap3::LdapResult::success)
{
println!("Error connecting as '{}': {}", binddn, e);
bind_ldap(ldap_connection, Some(binddn))
@@ -150,12 +150,11 @@ impl TryFrom<ResultEntry> for User {
.attrs
.get(attr)
.ok_or_else(|| anyhow!("Missing {} for user", attr))
.and_then(|u| {
if u.len() > 1 {
Err(anyhow!("Too many {}s", attr))
} else {
Ok(u.first().unwrap().to_owned())
}
.and_then(|u| -> Result<String> {
u.iter()
.next()
.map(String::to_owned)
.ok_or_else(|| anyhow!("Too many {}s", attr))
})
};
let id = get_required_attribute("uid")
@@ -170,13 +169,8 @@ impl TryFrom<ResultEntry> for User {
.attrs
.get(attr)
.and_then(|v| v.first().map(|s| s.as_str()))
.and_then(|s| {
if s.is_empty() {
None
} else {
Some(s.to_owned())
}
})
.filter(|s| !s.is_empty())
.map(str::to_owned)
};
let last_name = get_optional_attribute("sn").or_else(|| get_optional_attribute("surname"));
let display_name = get_optional_attribute("cn")
@@ -184,14 +178,23 @@ impl TryFrom<ResultEntry> for User {
.or_else(|| get_optional_attribute("name"))
.or_else(|| get_optional_attribute("displayName"));
let first_name = get_optional_attribute("givenName");
let avatar = entry
.attrs
.get("jpegPhoto")
.map(|v| v.iter().map(|s| s.as_bytes().to_vec()).collect::<Vec<_>>())
.or_else(|| entry.bin_attrs.get("jpegPhoto").map(Clone::clone))
.and_then(|v| v.into_iter().next().filter(|s| !s.is_empty()));
let password =
get_optional_attribute("userPassword").or_else(|| get_optional_attribute("password"));
Ok(User::new(
id,
email,
display_name,
first_name,
last_name,
crate::lldap::CreateUserInput {
id,
email,
display_name,
first_name,
last_name,
avatar: avatar.map(base64::encode),
},
password,
entry.dn,
))

View File

@@ -30,7 +30,7 @@ impl GraphQLClient {
where
QueryType: GraphQLQuery + 'static,
{
let unwrap_graphql_response = |graphql_client::Response { data, errors }| {
let unwrap_graphql_response = |graphql_client::Response { data, errors, .. }| {
data.ok_or_else(|| {
anyhow!(
"Errors: [{}]",
@@ -69,24 +69,13 @@ pub struct User {
impl User {
// https://github.com/graphql-rust/graphql-client/issues/386
#[allow(non_snake_case)]
pub fn new(
id: String,
email: String,
displayName: Option<String>,
firstName: Option<String>,
lastName: Option<String>,
user_input: create_user::CreateUserInput,
password: Option<String>,
dn: String,
) -> User {
User {
user_input: create_user::CreateUserInput {
id,
email,
displayName,
firstName,
lastName,
},
user_input,
password,
dn,
}
@@ -103,6 +92,8 @@ impl User {
)]
struct CreateUser;
pub type CreateUserInput = create_user::CreateUserInput;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
@@ -175,7 +166,9 @@ fn try_login(
response.status().as_str()
);
}
Ok(response.text()?)
let json = serde_json::from_str::<lldap_auth::login::ServerLoginResponse>(&response.text()?)
.context("Could not parse response")?;
Ok(json.token)
}
pub fn get_lldap_user_and_password(

View File

@@ -17,6 +17,8 @@ type Mutation {
type Group {
id: Int!
displayName: String!
creationDate: DateTimeUtc!
uuid: String!
"The groups to which this user belongs."
users: [User!]!
}
@@ -58,6 +60,7 @@ input CreateUserInput {
displayName: String
firstName: String
lastName: String
avatar: String
}
type User {
@@ -66,7 +69,9 @@ type User {
displayName: String!
firstName: String!
lastName: String!
avatar: String!
creationDate: DateTimeUtc!
uuid: String!
"The groups to which this user belongs."
groups: [Group!]!
}
@@ -82,6 +87,7 @@ input UpdateUserInput {
displayName: String
firstName: String
lastName: String
avatar: String
}
schema {

View File

@@ -2,7 +2,7 @@
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
edition = "2021"
name = "lldap"
version = "0.4.0"
version = "0.4.1"
[dependencies]
actix = "0.12"
@@ -11,7 +11,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-tls = "=3.0.0-beta.5"
actix-web = "=4.0.0-beta.8"
actix-web-httpauth = "0.6.0-beta.2"
anyhow = "*"
@@ -20,31 +19,34 @@ base64 = "0.13"
bincode = "1.3"
cron = "*"
derive_builder = "0.10.2"
figment_file_provider_adapter = "0.1"
futures = "*"
futures-util = "*"
hmac = "0.10"
http = "*"
itertools = "0.10.1"
juniper = "0.15.6"
juniper = "0.15.10"
juniper_actix = "0.4.0"
jwt = "0.13"
ldap3_server = "=0.1.11"
ldap3_proto = "*"
log = "*"
native-tls = "0.2.10"
orion = "0.16"
rustls = "0.20"
serde = "*"
serde_json = "1"
sha2 = "0.9"
sqlx-core = "0.5.11"
thiserror = "*"
time = "0.2"
tokio-native-tls = "0.3"
tokio-rustls = "0.23"
tokio-stream = "*"
tokio-util = "0.6.3"
tokio-util = "0.7.3"
tracing = "*"
tracing-actix-web = "0.4.0-beta.7"
tracing-attributes = "^0.1.21"
tracing-log = "*"
rustls-pemfile = "1.0.0"
serde_bytes = "0.11.7"
[dependencies.chrono]
features = ["serde"]
@@ -63,7 +65,8 @@ version = "0.3"
features = ["env-filter", "tracing-log"]
[dependencies.lettre]
features = ["builder", "serde", "smtp-transport", "tokio1-native-tls", "tokio1"]
features = ["builder", "serde", "smtp-transport", "tokio1-rustls-tls"]
default-features = false
version = "0.10.0-rc.3"
[dependencies.sqlx]
@@ -74,7 +77,7 @@ features = [
"macros",
"mysql",
"postgres",
"runtime-actix-native-tls",
"runtime-actix-rustls",
"sqlite",
]
@@ -92,10 +95,6 @@ features = ["with-chrono", "sqlx-sqlite", "sqlx-any"]
[dependencies.opaque-ke]
version = "0.6"
[dependencies.openssl-sys]
features = ["vendored"]
version = "*"
[dependencies.rand]
features = ["small_rng", "getrandom"]
version = "0.8"
@@ -106,7 +105,7 @@ version = "*"
[dependencies.tokio]
features = ["full"]
version = "1.13.1"
version = "1.17"
[dependencies.uuid]
features = ["v3"]
@@ -116,5 +115,14 @@ version = "*"
features = ["smallvec", "chrono", "tokio"]
version = "^0.1.4"
[dependencies.actix-tls]
features = ["default", "rustls"]
version = "=3.0.0-beta.5"
[dependencies.image]
features = ["jpeg"]
default-features = false
version = "0.24"
[dev-dependencies]
mockall = "0.9.1"

View File

@@ -47,7 +47,7 @@ impl std::string::ToString for Uuid {
#[macro_export]
macro_rules! uuid {
($s:literal) => {
crate::domain::handler::Uuid::try_from($s).unwrap()
$crate::domain::handler::Uuid::try_from($s).unwrap()
};
}
@@ -82,6 +82,74 @@ impl From<String> for UserId {
}
}
#[derive(PartialEq, Eq, Clone, Debug, Default, Serialize, Deserialize, sqlx::Type)]
#[sqlx(transparent)]
pub struct JpegPhoto(#[serde(with = "serde_bytes")] Vec<u8>);
impl From<JpegPhoto> for sea_query::Value {
fn from(photo: JpegPhoto) -> Self {
photo.0.into()
}
}
impl From<&JpegPhoto> for sea_query::Value {
fn from(photo: &JpegPhoto) -> Self {
photo.0.as_slice().into()
}
}
impl TryFrom<Vec<u8>> for JpegPhoto {
type Error = anyhow::Error;
fn try_from(bytes: Vec<u8>) -> anyhow::Result<Self> {
// Confirm that it's a valid Jpeg, then store only the bytes.
image::io::Reader::with_format(
std::io::Cursor::new(bytes.as_slice()),
image::ImageFormat::Jpeg,
)
.decode()?;
Ok(JpegPhoto(bytes))
}
}
impl TryFrom<String> for JpegPhoto {
type Error = anyhow::Error;
fn try_from(string: String) -> anyhow::Result<Self> {
// The String format is in base64.
Self::try_from(base64::decode(string.as_str())?)
}
}
impl From<&JpegPhoto> for String {
fn from(val: &JpegPhoto) -> Self {
base64::encode(&val.0)
}
}
impl JpegPhoto {
pub fn into_bytes(self) -> Vec<u8> {
self.0
}
#[cfg(test)]
pub fn for_tests() -> Self {
use image::{ImageOutputFormat, Rgb, RgbImage};
let img = RgbImage::from_fn(32, 32, |x, y| {
if (x + y) % 2 == 0 {
Rgb([0, 0, 0])
} else {
Rgb([255, 255, 255])
}
});
let mut bytes: Vec<u8> = Vec::new();
img.write_to(
&mut std::io::Cursor::new(&mut bytes),
ImageOutputFormat::Jpeg(0),
)
.unwrap();
Self(bytes)
}
}
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct User {
pub user_id: UserId,
@@ -89,7 +157,7 @@ pub struct User {
pub display_name: String,
pub first_name: String,
pub last_name: String,
// pub avatar: ?,
pub avatar: JpegPhoto,
pub creation_date: chrono::DateTime<chrono::Utc>,
pub uuid: Uuid,
}
@@ -105,6 +173,7 @@ impl Default for User {
display_name: String::new(),
first_name: String::new(),
last_name: String::new(),
avatar: JpegPhoto::default(),
creation_date: epoch,
uuid: Uuid::from_name_and_date("", &epoch),
}
@@ -159,6 +228,7 @@ pub struct CreateUserRequest {
pub display_name: Option<String>,
pub first_name: Option<String>,
pub last_name: Option<String>,
pub avatar: Option<JpegPhoto>,
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)]
@@ -169,6 +239,7 @@ pub struct UpdateUserRequest {
pub display_name: Option<String>,
pub first_name: Option<String>,
pub last_name: Option<String>,
pub avatar: Option<JpegPhoto>,
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
@@ -194,7 +265,7 @@ pub struct GroupDetails {
pub uuid: Uuid,
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UserAndGroups {
pub user: User,
pub groups: Option<Vec<GroupDetails>>,
@@ -263,4 +334,11 @@ mod tests {
Uuid::from_name_and_date(user_id, &date2)
);
}
#[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();
JpegPhoto::try_from(base64_jpeg).unwrap();
}
}

View File

@@ -2,7 +2,7 @@ use super::{error::*, handler::*, sql_tables::*};
use crate::infra::configuration::Configuration;
use async_trait::async_trait;
use futures_util::StreamExt;
use sea_query::{Alias, Cond, Expr, Iden, Order, Query, SimpleExpr};
use sea_query::{Alias, Cond, Expr, Iden, Order, Query};
use sea_query_binder::SqlxBinder;
use sqlx::{query_as_with, query_with, FromRow, Row};
use std::collections::HashSet;
@@ -23,90 +23,89 @@ impl SqlBackendHandler {
struct RequiresGroup(bool);
// Returns the condition for the SQL query, and whether it requires joining with the groups table.
fn get_user_filter_expr(filter: UserRequestFilter) -> (RequiresGroup, SimpleExpr) {
fn get_user_filter_expr(filter: UserRequestFilter) -> (RequiresGroup, Cond) {
use sea_query::IntoCondition;
use UserRequestFilter::*;
fn get_repeated_filter(
fs: Vec<UserRequestFilter>,
field: &dyn Fn(SimpleExpr, SimpleExpr) -> SimpleExpr,
) -> (RequiresGroup, SimpleExpr) {
fn get_repeated_filter(fs: Vec<UserRequestFilter>, condition: Cond) -> (RequiresGroup, Cond) {
let mut requires_group = false;
let mut it = fs.into_iter();
let first_expr = match it.next() {
None => return (RequiresGroup(false), Expr::value(true)),
Some(f) => {
let (group, filter) = get_user_filter_expr(f);
requires_group |= group.0;
filter
}
};
let filter = it.fold(first_expr, |e, f| {
let filter = fs.into_iter().fold(condition, |c, f| {
let (group, filters) = get_user_filter_expr(f);
requires_group |= group.0;
field(e, filters)
c.add(filters)
});
(RequiresGroup(requires_group), filter)
}
match filter {
And(fs) => get_repeated_filter(fs, &SimpleExpr::and),
Or(fs) => get_repeated_filter(fs, &SimpleExpr::or),
And(fs) => get_repeated_filter(fs, Cond::all()),
Or(fs) => get_repeated_filter(fs, Cond::any()),
Not(f) => {
let (requires_group, filters) = get_user_filter_expr(*f);
(requires_group, Expr::not(Expr::expr(filters)))
(requires_group, filters.not())
}
UserId(user_id) => (
RequiresGroup(false),
Expr::col((Users::Table, Users::UserId)).eq(user_id),
Expr::col((Users::Table, Users::UserId))
.eq(user_id)
.into_condition(),
),
Equality(s1, s2) => (
RequiresGroup(false),
if s1 == Users::DisplayName.to_string() {
Expr::col((Users::Table, Users::DisplayName)).eq(s2)
Expr::col((Users::Table, Users::DisplayName))
.eq(s2)
.into_condition()
} else if s1 == Users::UserId.to_string() {
panic!("User id should be wrapped")
} else {
Expr::expr(Expr::cust(&s1)).eq(s2)
Expr::expr(Expr::cust(&s1)).eq(s2).into_condition()
},
),
MemberOf(group) => (
RequiresGroup(true),
Expr::col((Groups::Table, Groups::DisplayName)).eq(group),
Expr::col((Groups::Table, Groups::DisplayName))
.eq(group)
.into_condition(),
),
MemberOfId(group_id) => (
RequiresGroup(true),
Expr::col((Groups::Table, Groups::GroupId)).eq(group_id),
Expr::col((Groups::Table, Groups::GroupId))
.eq(group_id)
.into_condition(),
),
}
}
// Returns the condition for the SQL query, and whether it requires joining with the groups table.
fn get_group_filter_expr(filter: GroupRequestFilter) -> SimpleExpr {
fn get_group_filter_expr(filter: GroupRequestFilter) -> Cond {
use sea_query::IntoCondition;
use GroupRequestFilter::*;
fn get_repeated_filter(
fs: Vec<GroupRequestFilter>,
field: &dyn Fn(SimpleExpr, SimpleExpr) -> SimpleExpr,
) -> SimpleExpr {
let mut it = fs.into_iter();
let first_expr = match it.next() {
None => return Expr::value(true),
Some(f) => get_group_filter_expr(f),
};
it.fold(first_expr, |e, f| field(e, get_group_filter_expr(f)))
}
match filter {
And(fs) => get_repeated_filter(fs, &SimpleExpr::and),
Or(fs) => get_repeated_filter(fs, &SimpleExpr::or),
Not(f) => Expr::not(Expr::expr(get_group_filter_expr(*f))),
DisplayName(name) => Expr::col((Groups::Table, Groups::DisplayName)).eq(name),
GroupId(id) => Expr::col((Groups::Table, Groups::GroupId)).eq(id.0),
Uuid(uuid) => Expr::col((Groups::Table, Groups::Uuid)).eq(uuid.to_string()),
And(fs) => fs
.into_iter()
.fold(Cond::all(), |c, f| c.add(get_group_filter_expr(f))),
Or(fs) => fs
.into_iter()
.fold(Cond::any(), |c, f| c.add(get_group_filter_expr(f))),
Not(f) => get_group_filter_expr(*f).not(),
DisplayName(name) => Expr::col((Groups::Table, Groups::DisplayName))
.eq(name)
.into_condition(),
GroupId(id) => Expr::col((Groups::Table, Groups::GroupId))
.eq(id.0)
.into_condition(),
Uuid(uuid) => Expr::col((Groups::Table, Groups::Uuid))
.eq(uuid.to_string())
.into_condition(),
// WHERE (group_id in (SELECT group_id FROM memberships WHERE user_id = user))
Member(user) => Expr::col((Memberships::Table, Memberships::GroupId)).in_subquery(
Query::select()
.column(Memberships::GroupId)
.from(Memberships::Table)
.cond_where(Expr::col(Memberships::UserId).eq(user))
.take(),
),
Member(user) => Expr::col((Memberships::Table, Memberships::GroupId))
.in_subquery(
Query::select()
.column(Memberships::GroupId)
.from(Memberships::Table)
.cond_where(Expr::col(Memberships::UserId).eq(user))
.take(),
)
.into_condition(),
}
}
@@ -287,7 +286,7 @@ impl BackendHandler for SqlBackendHandler {
Ok(groups)
}
#[instrument(skip_all, level = "debug", ret, err)]
#[instrument(skip_all, level = "debug", ret)]
async fn get_user_details(&self, user_id: &UserId) -> Result<User> {
debug!(?user_id);
let (query, values) = Query::select()
@@ -367,6 +366,7 @@ impl BackendHandler for SqlBackendHandler {
Users::DisplayName,
Users::FirstName,
Users::LastName,
Users::Avatar,
Users::CreationDate,
Users::Uuid,
];
@@ -378,6 +378,7 @@ impl BackendHandler for SqlBackendHandler {
request.display_name.unwrap_or_default().into(),
request.first_name.unwrap_or_default().into(),
request.last_name.unwrap_or_default().into(),
request.avatar.unwrap_or_default().into(),
now.naive_utc().into(),
uuid.into(),
];
@@ -409,6 +410,9 @@ impl BackendHandler for SqlBackendHandler {
if let Some(last_name) = request.last_name {
values.push((Users::LastName, last_name.into()));
}
if let Some(avatar) = request.avatar {
values.push((Users::Avatar, avatar.into()));
}
if values.is_empty() {
return Ok(());
}
@@ -532,10 +536,7 @@ mod tests {
use lldap_auth::{opaque, registration};
fn get_default_config() -> Configuration {
ConfigurationBuilder::default()
.verbose(true)
.build()
.unwrap()
ConfigurationBuilder::for_tests()
}
async fn get_in_memory_db() -> Pool {
@@ -695,6 +696,21 @@ mod tests {
.await;
assert_eq!(users, vec!["bob", "john"]);
}
{
let users = get_user_names(
&handler,
Some(UserRequestFilter::And(vec![
UserRequestFilter::Or(vec![]),
UserRequestFilter::Or(vec![
UserRequestFilter::UserId(UserId::new("bob")),
UserRequestFilter::UserId(UserId::new("John")),
UserRequestFilter::UserId(UserId::new("random")),
]),
])),
)
.await;
assert_eq!(users, vec!["bob", "john"]);
}
{
let users = get_user_names(
&handler,
@@ -879,14 +895,14 @@ mod tests {
insert_user(&handler, "Jennz", "boupBoup").await;
// Remove a user
let _request_result = handler.delete_user(&UserId::new("Jennz")).await.unwrap();
handler.delete_user(&UserId::new("Jennz")).await.unwrap();
assert_eq!(get_user_names(&handler, None).await, vec!["hector", "val"]);
// Insert new user and remove two
insert_user(&handler, "NewBoi", "Joni").await;
let _request_result = handler.delete_user(&UserId::new("Hector")).await.unwrap();
let _request_result = handler.delete_user(&UserId::new("NewBoi")).await.unwrap();
handler.delete_user(&UserId::new("Hector")).await.unwrap();
handler.delete_user(&UserId::new("NewBoi")).await.unwrap();
assert_eq!(get_user_names(&handler, None).await, vec!["val"]);
}

View File

@@ -77,11 +77,10 @@ async fn column_exists(pool: &Pool, table_name: &str, column_name: &str) -> sqlx
"SELECT COUNT(*) AS col_count FROM pragma_table_info('{}') WHERE name = '{}'",
table_name, column_name
);
Ok(sqlx::query(&query)
.fetch_one(pool)
.await?
.get::<i32, _>("col_count")
> 0)
match sqlx::query(&query).fetch_one(pool).await {
Err(_) => Ok(false),
Ok(row) => Ok(row.get::<i32, _>("col_count") > 0),
}
}
pub async fn create_group(group_name: &str, pool: &Pool) -> sqlx::Result<()> {

View File

@@ -562,7 +562,7 @@ where
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Permission {
Admin,
PasswordManager,
@@ -570,7 +570,7 @@ pub enum Permission {
Regular,
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationResults {
pub user: UserId,
pub permission: Permission,

View File

@@ -1,5 +1,6 @@
use clap::Parser;
use lettre::message::Mailbox;
use serde::{Deserialize, Serialize};
/// lldap is a lightweight LDAP server
#[derive(Debug, Parser, Clone)]
@@ -102,6 +103,14 @@ pub struct LdapsOpts {
pub ldaps_key_file: Option<String>,
}
clap::arg_enum! {
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum SmtpEncryption {
TLS,
STARTTLS,
}
}
#[derive(Debug, Parser, Clone)]
#[clap(next_help_heading = Some("SMTP"), setting = clap::AppSettings::DeriveDisplayOrder)]
pub struct SmtpOpts {
@@ -130,8 +139,11 @@ pub struct SmtpOpts {
pub smtp_password: Option<String>,
/// Whether TLS should be used to connect to SMTP.
#[clap(long, env = "LLDAP_SMTP_OPTIONS__TLS_REQUIRED")]
#[clap(long, env = "LLDAP_SMTP_OPTIONS__TLS_REQUIRED", setting=clap::ArgSettings::Hidden)]
pub smtp_tls_required: Option<bool>,
#[clap(long, env = "LLDAP_SMTP_OPTIONS__ENCRYPTION", possible_values = SmtpEncryption::variants(), case_insensitive = true)]
pub smtp_encryption: Option<SmtpEncryption>,
}
#[derive(Debug, Parser, Clone)]

View File

@@ -1,6 +1,6 @@
use crate::{
domain::handler::UserId,
infra::cli::{GeneralConfigOpts, LdapsOpts, RunOpts, SmtpOpts, TestEmailOpts},
infra::cli::{GeneralConfigOpts, LdapsOpts, RunOpts, SmtpEncryption, SmtpOpts, TestEmailOpts},
};
use anyhow::{Context, Result};
use figment::{
@@ -29,8 +29,11 @@ pub struct MailOptions {
pub user: String,
#[builder(default = r#"SecUtf8::from("")"#)]
pub password: SecUtf8,
#[builder(default = "true")]
pub tls_required: bool,
#[builder(default = "SmtpEncryption::TLS")]
pub smtp_encryption: SmtpEncryption,
/// Deprecated.
#[builder(default = "None")]
pub tls_required: Option<bool>,
}
impl std::default::Default for MailOptions {
@@ -71,6 +74,8 @@ pub struct Configuration {
pub ldap_base_dn: String,
#[builder(default = r#"UserId::new("admin")"#)]
pub ldap_user_dn: UserId,
#[builder(default = r#"String::default()"#)]
pub ldap_user_email: String,
#[builder(default = r#"SecUtf8::from("password")"#)]
pub ldap_user_pass: SecUtf8,
#[builder(default = r#"String::from("sqlite://users.db?mode=rwc")"#)]
@@ -105,6 +110,15 @@ impl ConfigurationBuilder {
let server_setup = get_server_setup(self.key_file.as_deref().unwrap_or("server_key"))?;
Ok(self.server_setup(Some(server_setup)).private_build()?)
}
#[cfg(test)]
pub fn for_tests() -> Configuration {
ConfigurationBuilder::default()
.verbose(true)
.server_setup(Some(generate_random_private_key()))
.private_build()
.unwrap()
}
}
impl Configuration {
@@ -117,17 +131,34 @@ impl Configuration {
}
}
fn generate_random_private_key() -> ServerSetup {
let mut rng = rand::rngs::OsRng;
ServerSetup::new(&mut rng)
}
fn write_to_readonly_file(path: &std::path::Path, buffer: &[u8]) -> Result<()> {
use std::{fs::File, io::Write};
assert!(!path.exists());
let mut file = File::create(path)?;
let mut permissions = file.metadata()?.permissions();
permissions.set_readonly(true);
if cfg!(unix) {
use std::os::unix::fs::PermissionsExt;
permissions.set_mode(0o400);
}
file.set_permissions(permissions)?;
Ok(file.write_all(buffer)?)
}
fn get_server_setup(file_path: &str) -> Result<ServerSetup> {
use std::path::Path;
let path = Path::new(file_path);
use std::fs::read;
let path = std::path::Path::new(file_path);
if path.exists() {
let bytes =
std::fs::read(file_path).context(format!("Could not read key file `{}`", file_path))?;
let bytes = read(file_path).context(format!("Could not read key file `{}`", file_path))?;
Ok(ServerSetup::deserialize(&bytes)?)
} else {
let mut rng = rand::rngs::OsRng;
let server_setup = ServerSetup::new(&mut rng);
std::fs::write(path, server_setup.serialize()).context(format!(
let server_setup = generate_random_private_key();
write_to_readonly_file(path, &server_setup.serialize()).context(format!(
"Could not write the generated server setup to file `{}`",
file_path,
))?;
@@ -232,7 +263,7 @@ impl ConfigOverrider for SmtpOpts {
config.smtp_options.password = SecUtf8::from(password.clone());
}
if let Some(tls_required) = self.smtp_tls_required {
config.smtp_options.tls_required = tls_required;
config.smtp_options.tls_required = Some(tls_required);
}
}
}
@@ -248,11 +279,13 @@ where
overrides.general_config().config_file
);
use figment_file_provider_adapter::FileAdapter;
let ignore_keys = ["key_file", "cert_file"];
let mut config: Configuration = Figment::from(Serialized::defaults(
ConfigurationBuilder::default().private_build().unwrap(),
))
.merge(Toml::file(config_file))
.merge(Env::prefixed("LLDAP_").split("__"))
.merge(FileAdapter::wrap(Toml::file(config_file)).ignore(&ignore_keys))
.merge(FileAdapter::wrap(Env::prefixed("LLDAP_").split("__")).ignore(&ignore_keys))
.extract()?;
overrides.override_config(&mut config);
@@ -266,5 +299,8 @@ where
if config.ldap_user_pass == SecUtf8::from("password") {
println!("WARNING: Unsecure default admin password is used.");
}
if config.smtp_options.tls_required.is_some() {
println!("DEPRECATED: smtp_options.tls_required field is deprecated, it never did anything. You can replace it with smtp_options.smtp_encryption.");
}
Ok(config)
}

View File

@@ -1,6 +1,8 @@
use crate::domain::handler::{
BackendHandler, CreateUserRequest, GroupId, UpdateGroupRequest, UpdateUserRequest, UserId,
BackendHandler, CreateUserRequest, GroupId, JpegPhoto, UpdateGroupRequest, UpdateUserRequest,
UserId,
};
use anyhow::Context as AnyhowContext;
use juniper::{graphql_object, FieldResult, GraphQLInputObject, GraphQLObject};
use tracing::{debug, debug_span, Instrument};
@@ -28,6 +30,8 @@ pub struct CreateUserInput {
display_name: Option<String>,
first_name: Option<String>,
last_name: Option<String>,
// Base64 encoded JpegPhoto.
avatar: Option<String>,
}
#[derive(PartialEq, Eq, Debug, GraphQLInputObject)]
@@ -38,6 +42,8 @@ pub struct UpdateUserInput {
display_name: Option<String>,
first_name: Option<String>,
last_name: Option<String>,
// Base64 encoded JpegPhoto.
avatar: Option<String>,
}
#[derive(PartialEq, Eq, Debug, GraphQLInputObject)]
@@ -73,6 +79,14 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
return Err("Unauthorized user creation".into());
}
let user_id = UserId::new(&user.id);
let avatar = user
.avatar
.map(base64::decode)
.transpose()
.context("Invalid base64 image")?
.map(JpegPhoto::try_from)
.transpose()
.context("Provided image is not a valid JPEG")?;
context
.handler
.create_user(CreateUserRequest {
@@ -81,6 +95,7 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
display_name: user.display_name,
first_name: user.first_name,
last_name: user.last_name,
avatar,
})
.instrument(span.clone())
.await?;
@@ -126,6 +141,14 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
span.in_scope(|| debug!("Unauthorized"));
return Err("Unauthorized user update".into());
}
let avatar = user
.avatar
.map(base64::decode)
.transpose()
.context("Invalid base64 image")?
.map(JpegPhoto::try_from)
.transpose()
.context("Provided image is not a valid JPEG")?;
context
.handler
.update_user(UpdateUserRequest {
@@ -134,6 +157,7 @@ impl<Handler: BackendHandler + Sync> Mutation<Handler> {
display_name: user.display_name,
first_name: user.first_name,
last_name: user.last_name,
avatar,
})
.instrument(span)
.await?;

View File

@@ -217,10 +217,18 @@ impl<Handler: BackendHandler + Sync> User<Handler> {
&self.user.last_name
}
fn avatar(&self) -> String {
(&self.user.avatar).into()
}
fn creation_date(&self) -> chrono::DateTime<chrono::Utc> {
self.user.creation_date
}
fn uuid(&self) -> &str {
self.user.uuid.as_str()
}
/// The groups to which this user belongs.
async fn groups(&self, context: &Context<Handler>) -> FieldResult<Vec<Group<Handler>>> {
let span = debug_span!("[GraphQL query] user::groups");
@@ -260,6 +268,7 @@ pub struct Group<Handler: BackendHandler> {
group_id: i32,
display_name: String,
creation_date: chrono::DateTime<chrono::Utc>,
uuid: String,
members: Option<Vec<String>>,
_phantom: std::marker::PhantomData<Box<Handler>>,
}
@@ -272,6 +281,12 @@ impl<Handler: BackendHandler + Sync> Group<Handler> {
fn display_name(&self) -> String {
self.display_name.clone()
}
fn creation_date(&self) -> chrono::DateTime<chrono::Utc> {
self.creation_date
}
fn uuid(&self) -> String {
self.uuid.clone()
}
/// The groups to which this user belongs.
async fn users(&self, context: &Context<Handler>) -> FieldResult<Vec<User<Handler>>> {
let span = debug_span!("[GraphQL query] group::users");
@@ -300,6 +315,7 @@ impl<Handler: BackendHandler> From<GroupDetails> for Group<Handler> {
group_id: group_details.group_id.0,
display_name: group_details.display_name,
creation_date: group_details.creation_date,
uuid: group_details.uuid.into_string(),
members: None,
_phantom: std::marker::PhantomData,
}
@@ -312,6 +328,7 @@ impl<Handler: BackendHandler> From<DomainGroup> for Group<Handler> {
group_id: group.id.0,
display_name: group.display_name,
creation_date: group.creation_date,
uuid: group.uuid.into_string(),
members: Some(group.users.into_iter().map(UserId::into_string).collect()),
_phantom: std::marker::PhantomData,
}
@@ -350,8 +367,12 @@ mod tests {
user(userId: "bob") {
id
email
creationDate
uuid
groups {
id
creationDate
uuid
}
}
}"#;
@@ -363,6 +384,8 @@ mod tests {
Ok(DomainUser {
user_id: UserId::new("bob"),
email: "bob@bobbers.on".to_string(),
creation_date: chrono::Utc.timestamp_millis(42),
uuid: crate::uuid!("b1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
..Default::default()
})
});
@@ -391,7 +414,13 @@ mod tests {
"user": {
"id": "bob",
"email": "bob@bobbers.on",
"groups": [{"id": 3}]
"creationDate": "1970-01-01T00:00:00.042+00:00",
"uuid": "b1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8",
"groups": [{
"id": 3,
"creationDate": "1970-01-01T00:00:00.000000042+00:00",
"uuid": "a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8"
}]
}
}),
vec![]

View File

@@ -10,12 +10,12 @@ use crate::{
};
use anyhow::{bail, Context, Result};
use itertools::Itertools;
use ldap3_server::proto::{
use ldap3_proto::proto::{
LdapBindCred, LdapBindRequest, LdapBindResponse, LdapExtendedRequest, LdapExtendedResponse,
LdapFilter, LdapOp, LdapPartialAttribute, LdapPasswordModifyRequest, LdapResult,
LdapResultCode, LdapSearchRequest, LdapSearchResultEntry, LdapSearchScope,
};
use tracing::{debug, instrument, warn};
use tracing::{debug, info, instrument, warn};
#[derive(Debug, PartialEq, Eq, Clone)]
struct LdapDn(String);
@@ -151,25 +151,26 @@ fn get_user_id_from_distinguished_name(
fn get_user_attribute(
user: &User,
attribute: &str,
dn: &str,
base_dn_str: &str,
groups: Option<&[GroupDetails]>,
ignored_user_attributes: &[String],
) -> Result<Option<Vec<String>>> {
) -> Result<Option<Vec<Vec<u8>>>> {
let attribute = attribute.to_ascii_lowercase();
Ok(Some(match attribute.as_str() {
let attribute_values = match attribute.as_str() {
"objectclass" => vec![
"inetOrgPerson".to_string(),
"posixAccount".to_string(),
"mailAccount".to_string(),
"person".to_string(),
b"inetOrgPerson".to_vec(),
b"posixAccount".to_vec(),
b"mailAccount".to_vec(),
b"person".to_vec(),
],
"dn" | "distinguishedname" => vec![dn.to_string()],
"uid" => vec![user.user_id.to_string()],
"entryuuid" => vec![user.uuid.to_string()],
"mail" => vec![user.email.clone()],
"givenname" => vec![user.first_name.clone()],
"sn" => vec![user.last_name.clone()],
// dn is always returned as part of the base response.
"dn" | "distinguishedname" => return Ok(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()],
"memberof" => groups
.into_iter()
.flatten()
@@ -178,10 +179,11 @@ fn get_user_attribute(
"uid={},ou=groups,{}",
&id_and_name.display_name, base_dn_str
)
.into_bytes()
})
.collect(),
"cn" | "displayname" => vec![user.display_name.clone()],
"createtimestamp" | "modifytimestamp" => vec![user.creation_date.to_rfc3339()],
"cn" | "displayname" => vec![user.display_name.clone().into_bytes()],
"createtimestamp" | "modifytimestamp" => vec![user.creation_date.to_rfc3339().into_bytes()],
"1.1" => return Ok(None),
// We ignore the operational attribute wildcard.
"+" => return Ok(None),
@@ -201,7 +203,12 @@ fn get_user_attribute(
}
return Ok(None);
}
}))
};
if attribute_values.len() == 1 && attribute_values[0].is_empty() {
Ok(None)
} else {
Ok(Some(attribute_values))
}
}
#[instrument(skip_all, level = "debug")]
@@ -232,12 +239,12 @@ fn expand_attribute_wildcards<'a>(
const ALL_USER_ATTRIBUTE_KEYS: &[&str] = &[
"objectclass",
"dn",
"uid",
"mail",
"givenname",
"sn",
"cn",
"jpegPhoto",
"createtimestamp",
];
@@ -251,14 +258,13 @@ fn make_ldap_search_user_result_entry(
let dn = format!("uid={},ou=people,{}", user.user_id.as_str(), base_dn_str);
Ok(LdapSearchResultEntry {
dn: dn.clone(),
dn,
attributes: attributes
.iter()
.filter_map(|a| {
let values = match get_user_attribute(
&user,
a,
&dn,
base_dn_str,
groups,
ignored_user_attributes,
@@ -281,21 +287,19 @@ fn get_group_attribute(
attribute: &str,
user_filter: &Option<&UserId>,
ignored_group_attributes: &[String],
) -> Result<Option<Vec<String>>> {
) -> Result<Option<Vec<Vec<u8>>>> {
let attribute = attribute.to_ascii_lowercase();
Ok(Some(match attribute.as_str() {
"objectclass" => vec!["groupOfUniqueNames".to_string()],
"dn" | "distinguishedname" => vec![format!(
"cn={},ou=groups,{}",
group.display_name, base_dn_str
)],
"cn" | "uid" => vec![group.display_name.clone()],
"entryuuid" => vec![group.uuid.to_string()],
let attribute_values = match attribute.as_str() {
"objectclass" => vec![b"groupOfUniqueNames".to_vec()],
// Always returned as part of the base response.
"dn" | "distinguishedname" => return Ok(None),
"cn" | "uid" => vec![group.display_name.clone().into_bytes()],
"entryuuid" => vec![group.uuid.to_string().into_bytes()],
"member" | "uniquemember" => group
.users
.iter()
.filter(|u| user_filter.map(|f| *u == f).unwrap_or(true))
.map(|u| format!("uid={},ou=people,{}", u, base_dn_str))
.map(|u| format!("uid={},ou=people,{}", u, base_dn_str).into_bytes())
.collect(),
"1.1" => return Ok(None),
// We ignore the operational attribute wildcard
@@ -316,11 +320,15 @@ fn get_group_attribute(
}
return Ok(None);
}
}))
};
if attribute_values.len() == 1 && attribute_values[0].is_empty() {
Ok(None)
} else {
Ok(Some(attribute_values))
}
}
const ALL_GROUP_ATTRIBUTE_KEYS: &[&str] =
&["objectclass", "dn", "uid", "cn", "member", "uniquemember"];
const ALL_GROUP_ATTRIBUTE_KEYS: &[&str] = &["objectclass", "uid", "cn", "member", "uniquemember"];
fn make_ldap_search_group_result_entry(
group: Group,
@@ -423,27 +431,27 @@ fn root_dse_response(base_dn: &str) -> LdapOp {
attributes: vec![
LdapPartialAttribute {
atype: "objectClass".to_string(),
vals: vec!["top".to_string()],
vals: vec![b"top".to_vec()],
},
LdapPartialAttribute {
atype: "vendorName".to_string(),
vals: vec!["LLDAP".to_string()],
vals: vec![b"LLDAP".to_vec()],
},
LdapPartialAttribute {
atype: "vendorVersion".to_string(),
vals: vec!["lldap_0.2.0".to_string()],
vals: vec![b"lldap_0.2.0".to_vec()],
},
LdapPartialAttribute {
atype: "supportedLDAPVersion".to_string(),
vals: vec!["3".to_string()],
vals: vec![b"3".to_vec()],
},
LdapPartialAttribute {
atype: "supportedExtension".to_string(),
vals: vec!["1.3.6.1.4.1.4203.1.11.1".to_string()],
vals: vec![b"1.3.6.1.4.1.4203.1.11.1".to_vec()],
},
LdapPartialAttribute {
atype: "defaultnamingcontext".to_string(),
vals: vec![base_dn.to_string()],
vals: vec![base_dn.to_string().into_bytes()],
},
],
})
@@ -738,6 +746,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
let parsed_filters = match user_filter {
None => filters,
Some(u) => {
info!("Unpriviledged search, limiting results");
UserRequestFilter::And(vec![filters, UserRequestFilter::UserId((*u).clone())])
}
};
@@ -802,6 +811,7 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
let parsed_filters = match user_filter {
None => filter,
Some(u) => {
info!("Unpriviledged search, limiting results");
GroupRequestFilter::And(vec![filter, GroupRequestFilter::Member((*u).clone())])
}
};
@@ -928,7 +938,11 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
self.convert_group_filter(filter)?,
))),
LdapFilter::Present(field) => {
if ALL_GROUP_ATTRIBUTE_KEYS.contains(&field.to_ascii_lowercase().as_str()) {
let field = &field.to_ascii_lowercase();
if field == "dn"
|| field == "distinguishedname"
|| ALL_GROUP_ATTRIBUTE_KEYS.contains(&field.as_str())
{
Ok(GroupRequestFilter::And(vec![]))
} else {
Ok(GroupRequestFilter::Not(Box::new(GroupRequestFilter::And(
@@ -1005,7 +1019,11 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
LdapFilter::Present(field) => {
let field = &field.to_ascii_lowercase();
// Check that it's a field we support.
if field == "objectclass" || map_field(field).is_ok() {
if field == "objectclass"
|| field == "dn"
|| field == "distinguishedname"
|| map_field(field).is_ok()
{
Ok(UserRequestFilter::And(vec![]))
} else {
Ok(UserRequestFilter::Not(Box::new(UserRequestFilter::And(
@@ -1027,7 +1045,7 @@ mod tests {
};
use async_trait::async_trait;
use chrono::TimeZone;
use ldap3_server::proto::{LdapDerefAliases, LdapSearchScope};
use ldap3_proto::proto::{LdapDerefAliases, LdapSearchScope};
use mockall::predicate::eq;
use std::collections::HashSet;
use tokio;
@@ -1309,7 +1327,7 @@ mod tests {
dn: "uid=bob,ou=people,dc=example,dc=com".to_string(),
attributes: vec![LdapPartialAttribute {
atype: "memberOf".to_string(),
vals: vec!["uid=rockstars,ou=groups,dc=example,dc=com".to_string()]
vals: vec![b"uid=rockstars,ou=groups,dc=example,dc=com".to_vec()]
}],
}),
make_search_success(),
@@ -1454,6 +1472,7 @@ mod tests {
display_name: "Jimminy Cricket".to_string(),
first_name: "Jim".to_string(),
last_name: "Cricket".to_string(),
avatar: JpegPhoto::for_tests(),
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
creation_date: Utc.ymd(2014, 7, 8).and_hms(9, 10, 11),
},
@@ -1474,6 +1493,7 @@ mod tests {
"cn",
"createTimestamp",
"entryUuid",
"jpegPhoto",
],
);
assert_eq!(
@@ -1485,43 +1505,39 @@ mod tests {
LdapPartialAttribute {
atype: "objectClass".to_string(),
vals: vec![
"inetOrgPerson".to_string(),
"posixAccount".to_string(),
"mailAccount".to_string(),
"person".to_string()
b"inetOrgPerson".to_vec(),
b"posixAccount".to_vec(),
b"mailAccount".to_vec(),
b"person".to_vec()
]
},
LdapPartialAttribute {
atype: "dn".to_string(),
vals: vec!["uid=bob_1,ou=people,dc=example,dc=com".to_string()]
},
LdapPartialAttribute {
atype: "uid".to_string(),
vals: vec!["bob_1".to_string()]
vals: vec![b"bob_1".to_vec()]
},
LdapPartialAttribute {
atype: "mail".to_string(),
vals: vec!["bob@bobmail.bob".to_string()]
vals: vec![b"bob@bobmail.bob".to_vec()]
},
LdapPartialAttribute {
atype: "givenName".to_string(),
vals: vec!["Bôb".to_string()]
vals: vec!["Bôb".to_string().into_bytes()]
},
LdapPartialAttribute {
atype: "sn".to_string(),
vals: vec!["Böbberson".to_string()]
vals: vec!["Böbberson".to_string().into_bytes()]
},
LdapPartialAttribute {
atype: "cn".to_string(),
vals: vec!["Bôb Böbberson".to_string()]
vals: vec!["Bôb Böbberson".to_string().into_bytes()]
},
LdapPartialAttribute {
atype: "createTimestamp".to_string(),
vals: vec!["1970-01-01T00:00:00+00:00".to_string()]
vals: vec![b"1970-01-01T00:00:00+00:00".to_vec()]
},
LdapPartialAttribute {
atype: "entryUuid".to_string(),
vals: vec!["698e1d5f-7a40-3151-8745-b9b8a37839da".to_string()]
vals: vec![b"698e1d5f-7a40-3151-8745-b9b8a37839da".to_vec()]
},
],
}),
@@ -1531,43 +1547,43 @@ mod tests {
LdapPartialAttribute {
atype: "objectClass".to_string(),
vals: vec![
"inetOrgPerson".to_string(),
"posixAccount".to_string(),
"mailAccount".to_string(),
"person".to_string()
b"inetOrgPerson".to_vec(),
b"posixAccount".to_vec(),
b"mailAccount".to_vec(),
b"person".to_vec()
]
},
LdapPartialAttribute {
atype: "dn".to_string(),
vals: vec!["uid=jim,ou=people,dc=example,dc=com".to_string()]
},
LdapPartialAttribute {
atype: "uid".to_string(),
vals: vec!["jim".to_string()]
vals: vec![b"jim".to_vec()]
},
LdapPartialAttribute {
atype: "mail".to_string(),
vals: vec!["jim@cricket.jim".to_string()]
vals: vec![b"jim@cricket.jim".to_vec()]
},
LdapPartialAttribute {
atype: "givenName".to_string(),
vals: vec!["Jim".to_string()]
vals: vec![b"Jim".to_vec()]
},
LdapPartialAttribute {
atype: "sn".to_string(),
vals: vec!["Cricket".to_string()]
vals: vec![b"Cricket".to_vec()]
},
LdapPartialAttribute {
atype: "cn".to_string(),
vals: vec!["Jimminy Cricket".to_string()]
vals: vec![b"Jimminy Cricket".to_vec()]
},
LdapPartialAttribute {
atype: "createTimestamp".to_string(),
vals: vec!["2014-07-08T09:10:11+00:00".to_string()]
vals: vec![b"2014-07-08T09:10:11+00:00".to_vec()]
},
LdapPartialAttribute {
atype: "entryUuid".to_string(),
vals: vec!["04ac75e0-2900-3e21-926c-2f732c26b3fc".to_string()]
vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()]
},
LdapPartialAttribute {
atype: "jpegPhoto".to_string(),
vals: vec![JpegPhoto::for_tests().into_bytes()]
},
],
}),
@@ -1614,26 +1630,22 @@ mod tests {
attributes: vec![
LdapPartialAttribute {
atype: "objectClass".to_string(),
vals: vec!["groupOfUniqueNames".to_string(),]
},
LdapPartialAttribute {
atype: "dn".to_string(),
vals: vec!["cn=group_1,ou=groups,dc=example,dc=com".to_string()]
vals: vec![b"groupOfUniqueNames".to_vec(),]
},
LdapPartialAttribute {
atype: "cn".to_string(),
vals: vec!["group_1".to_string()]
vals: vec![b"group_1".to_vec()]
},
LdapPartialAttribute {
atype: "uniqueMember".to_string(),
vals: vec![
"uid=bob,ou=people,dc=example,dc=com".to_string(),
"uid=john,ou=people,dc=example,dc=com".to_string(),
b"uid=bob,ou=people,dc=example,dc=com".to_vec(),
b"uid=john,ou=people,dc=example,dc=com".to_vec(),
]
},
LdapPartialAttribute {
atype: "entryUuid".to_string(),
vals: vec!["04ac75e0-2900-3e21-926c-2f732c26b3fc".to_string()],
vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()],
},
],
}),
@@ -1642,23 +1654,19 @@ mod tests {
attributes: vec![
LdapPartialAttribute {
atype: "objectClass".to_string(),
vals: vec!["groupOfUniqueNames".to_string(),]
},
LdapPartialAttribute {
atype: "dn".to_string(),
vals: vec!["cn=BestGroup,ou=groups,dc=example,dc=com".to_string()]
vals: vec![b"groupOfUniqueNames".to_vec(),]
},
LdapPartialAttribute {
atype: "cn".to_string(),
vals: vec!["BestGroup".to_string()]
vals: vec![b"BestGroup".to_vec()]
},
LdapPartialAttribute {
atype: "uniqueMember".to_string(),
vals: vec!["uid=john,ou=people,dc=example,dc=com".to_string()]
vals: vec![b"uid=john,ou=people,dc=example,dc=com".to_vec()]
},
LdapPartialAttribute {
atype: "entryUuid".to_string(),
vals: vec!["04ac75e0-2900-3e21-926c-2f732c26b3fc".to_string()],
vals: vec![b"04ac75e0-2900-3e21-926c-2f732c26b3fc".to_vec()],
},
],
}),
@@ -1760,7 +1768,7 @@ mod tests {
dn: "cn=group_1,ou=groups,dc=example,dc=com".to_string(),
attributes: vec![LdapPartialAttribute {
atype: "cn".to_string(),
vals: vec!["group_1".to_string()]
vals: vec![b"group_1".to_vec()]
},],
}),
make_search_success(),
@@ -1836,7 +1844,7 @@ mod tests {
"ou=groups,dc=example,dc=com",
LdapFilter::And(vec![LdapFilter::Substring(
"whatever".to_string(),
ldap3_server::proto::LdapSubstringFilter::default(),
ldap3_proto::proto::LdapSubstringFilter::default(),
)]),
vec!["cn"],
);
@@ -1980,10 +1988,10 @@ mod tests {
attributes: vec![LdapPartialAttribute {
atype: "objectclass".to_string(),
vals: vec![
"inetOrgPerson".to_string(),
"posixAccount".to_string(),
"mailAccount".to_string(),
"person".to_string()
b"inetOrgPerson".to_vec(),
b"posixAccount".to_vec(),
b"mailAccount".to_vec(),
b"person".to_vec()
]
},]
}),
@@ -2035,19 +2043,15 @@ mod tests {
LdapPartialAttribute {
atype: "objectClass".to_string(),
vals: vec![
"inetOrgPerson".to_string(),
"posixAccount".to_string(),
"mailAccount".to_string(),
"person".to_string()
b"inetOrgPerson".to_vec(),
b"posixAccount".to_vec(),
b"mailAccount".to_vec(),
b"person".to_vec()
]
},
LdapPartialAttribute {
atype: "dn".to_string(),
vals: vec!["uid=bob_1,ou=people,dc=example,dc=com".to_string()]
},
LdapPartialAttribute {
atype: "cn".to_string(),
vals: vec!["Bôb Böbberson".to_string()]
vals: vec!["Bôb Böbberson".to_string().into_bytes()]
},
],
}),
@@ -2056,15 +2060,11 @@ mod tests {
attributes: vec![
LdapPartialAttribute {
atype: "objectClass".to_string(),
vals: vec!["groupOfUniqueNames".to_string(),]
},
LdapPartialAttribute {
atype: "dn".to_string(),
vals: vec!["cn=group_1,ou=groups,dc=example,dc=com".to_string()]
vals: vec![b"groupOfUniqueNames".to_vec(),]
},
LdapPartialAttribute {
atype: "cn".to_string(),
vals: vec!["group_1".to_string()]
vals: vec![b"group_1".to_vec()]
},
],
}),
@@ -2082,8 +2082,8 @@ mod tests {
user_id: UserId::new("bob_1"),
email: "bob@bobmail.bob".to_string(),
display_name: "Bôb Böbberson".to_string(),
first_name: "Bôb".to_string(),
last_name: "Böbberson".to_string(),
avatar: JpegPhoto::for_tests(),
..Default::default()
},
groups: None,
@@ -2116,39 +2116,35 @@ mod tests {
LdapPartialAttribute {
atype: "objectclass".to_string(),
vals: vec![
"inetOrgPerson".to_string(),
"posixAccount".to_string(),
"mailAccount".to_string(),
"person".to_string(),
b"inetOrgPerson".to_vec(),
b"posixAccount".to_vec(),
b"mailAccount".to_vec(),
b"person".to_vec(),
],
},
LdapPartialAttribute {
atype: "dn".to_string(),
vals: vec!["uid=bob_1,ou=people,dc=example,dc=com".to_string()],
},
LdapPartialAttribute {
atype: "uid".to_string(),
vals: vec!["bob_1".to_string()],
vals: vec![b"bob_1".to_vec()],
},
LdapPartialAttribute {
atype: "mail".to_string(),
vals: vec!["bob@bobmail.bob".to_string()],
},
LdapPartialAttribute {
atype: "givenname".to_string(),
vals: vec!["Bôb".to_string()],
vals: vec![b"bob@bobmail.bob".to_vec()],
},
LdapPartialAttribute {
atype: "sn".to_string(),
vals: vec!["Böbberson".to_string()],
vals: vec!["Böbberson".to_string().into_bytes()],
},
LdapPartialAttribute {
atype: "cn".to_string(),
vals: vec!["Bôb Böbberson".to_string()],
vals: vec!["Bôb Böbberson".to_string().into_bytes()],
},
LdapPartialAttribute {
atype: "jpegPhoto".to_string(),
vals: vec![JpegPhoto::for_tests().into_bytes()],
},
LdapPartialAttribute {
atype: "createtimestamp".to_string(),
vals: vec![chrono::Utc.timestamp(0, 0).to_rfc3339()],
vals: vec![chrono::Utc.timestamp(0, 0).to_rfc3339().into_bytes()],
},
],
}),
@@ -2158,34 +2154,30 @@ mod tests {
attributes: vec![
LdapPartialAttribute {
atype: "objectclass".to_string(),
vals: vec!["groupOfUniqueNames".to_string()],
},
LdapPartialAttribute {
atype: "dn".to_string(),
vals: vec!["cn=group_1,ou=groups,dc=example,dc=com".to_string()],
vals: vec![b"groupOfUniqueNames".to_vec()],
},
// UID
LdapPartialAttribute {
atype: "uid".to_string(),
vals: vec!["group_1".to_string()],
vals: vec![b"group_1".to_vec()],
},
LdapPartialAttribute {
atype: "cn".to_string(),
vals: vec!["group_1".to_string()],
vals: vec![b"group_1".to_vec()],
},
//member / uniquemember : "uid={},ou=people,{}"
LdapPartialAttribute {
atype: "member".to_string(),
vals: vec![
"uid=bob,ou=people,dc=example,dc=com".to_string(),
"uid=john,ou=people,dc=example,dc=com".to_string(),
b"uid=bob,ou=people,dc=example,dc=com".to_vec(),
b"uid=john,ou=people,dc=example,dc=com".to_vec(),
],
},
LdapPartialAttribute {
atype: "uniquemember".to_string(),
vals: vec![
"uid=bob,ou=people,dc=example,dc=com".to_string(),
"uid=john,ou=people,dc=example,dc=com".to_string(),
b"uid=bob,ou=people,dc=example,dc=com".to_vec(),
b"uid=john,ou=people,dc=example,dc=com".to_vec(),
],
},
],
@@ -2260,7 +2252,7 @@ mod tests {
let request = make_user_search_request(
LdapFilter::Substring(
"uid".to_string(),
ldap3_server::proto::LdapSubstringFilter::default(),
ldap3_proto::proto::LdapSubstringFilter::default(),
),
vec!["objectClass"],
);

View File

@@ -8,10 +8,10 @@ use crate::{
use actix_rt::net::TcpStream;
use actix_server::ServerBuilder;
use actix_service::{fn_service, ServiceFactoryExt};
use anyhow::{Context, Result};
use ldap3_server::{proto::LdapMsg, LdapCodec};
use native_tls::{Identity, TlsAcceptor};
use tokio_native_tls::TlsAcceptor as NativeTlsAcceptor;
use anyhow::{anyhow, Context, Result};
use ldap3_proto::{proto::LdapMsg, LdapCodec};
use rustls::PrivateKey;
use tokio_rustls::TlsAcceptor as RustlsTlsAcceptor;
use tokio_util::codec::{FramedRead, FramedWrite};
use tracing::{debug, error, info, instrument};
@@ -54,19 +54,6 @@ where
Ok(true)
}
fn get_file_as_byte_vec(filename: &str) -> Result<Vec<u8>> {
(|| -> Result<Vec<u8>> {
use std::fs::{metadata, File};
use std::io::Read;
let mut f = File::open(&filename).context("file not found")?;
let metadata = metadata(&filename).context("unable to read metadata")?;
let mut buffer = vec![0; metadata.len() as usize];
f.read(&mut buffer).context("buffer overflow")?;
Ok(buffer)
})()
.context(format!("while reading file {}", filename))
}
#[instrument(skip_all, level = "info", name = "LDAP session")]
async fn handle_ldap_stream<Stream, Backend>(
stream: Stream,
@@ -103,12 +90,53 @@ where
Ok(requests.into_inner().unsplit(resp.into_inner()))
}
fn get_tls_acceptor(config: &Configuration) -> Result<NativeTlsAcceptor> {
fn read_private_key(key_file: &str) -> Result<PrivateKey> {
use rustls_pemfile::{pkcs8_private_keys, rsa_private_keys};
use std::{fs::File, io::BufReader};
pkcs8_private_keys(&mut BufReader::new(File::open(key_file)?))
.map_err(anyhow::Error::from)
.and_then(|keys| {
keys.into_iter()
.next()
.ok_or_else(|| anyhow!("No PKCS8 key"))
})
.or_else(|_| {
rsa_private_keys(&mut BufReader::new(File::open(key_file)?))
.map_err(anyhow::Error::from)
.and_then(|keys| {
keys.into_iter()
.next()
.ok_or_else(|| anyhow!("No PKCS1 key"))
})
})
.with_context(|| {
format!(
"Cannot read either PKCS1 or PKCS8 private key from {}",
key_file
)
})
.map(rustls::PrivateKey)
}
fn get_tls_acceptor(config: &Configuration) -> Result<RustlsTlsAcceptor> {
use rustls::{Certificate, ServerConfig};
use rustls_pemfile::certs;
use std::{fs::File, io::BufReader};
// Load TLS key and cert files
let cert_file = get_file_as_byte_vec(&config.ldaps_options.cert_file)?;
let key_file = get_file_as_byte_vec(&config.ldaps_options.key_file)?;
let identity = Identity::from_pkcs8(&cert_file, &key_file)?;
Ok(TlsAcceptor::new(identity)?.into())
let certs = certs(&mut BufReader::new(File::open(
&config.ldaps_options.cert_file,
)?))?
.into_iter()
.map(Certificate)
.collect::<Vec<_>>();
let private_key = read_private_key(&config.ldaps_options.key_file)?;
let server_config = std::sync::Arc::new(
ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(certs, private_key)?,
);
Ok(server_config.into())
}
pub fn build_ldap_server<Backend>(

View File

@@ -1,4 +1,4 @@
use crate::infra::configuration::MailOptions;
use crate::infra::{cli::SmtpEncryption, configuration::MailOptions};
use anyhow::Result;
use lettre::{
message::Mailbox, transport::smtp::authentication::Credentials, Message, SmtpTransport,
@@ -26,9 +26,11 @@ fn send_email(to: Mailbox, subject: &str, body: String, options: &MailOptions) -
options.user.clone(),
options.password.unsecure().to_string(),
);
let mailer = SmtpTransport::relay(&options.server)?
.credentials(creds)
.build();
let relay_factory = match options.smtp_encryption {
SmtpEncryption::TLS => SmtpTransport::relay,
SmtpEncryption::STARTTLS => SmtpTransport::starttls_relay,
};
let mailer = relay_factory(&options.server)?.credentials(creds).build();
mailer.send(&email)?;
Ok(())
}

View File

@@ -30,6 +30,7 @@ async fn create_admin_user(handler: &SqlBackendHandler, config: &Configuration)
handler
.create_user(CreateUserRequest {
user_id: config.ldap_user_dn.clone(),
email: config.ldap_user_email.clone(),
display_name: Some("Administrator".to_string()),
..Default::default()
})