mirror of
https://github.com/lldap/lldap.git
synced 2026-07-25 17:58:44 +00:00
Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e248ab3bb | |||
| 5df92fbb16 | |||
| e92088a7aa | |||
| 5e83ed8eb0 | |||
| c69957690e | |||
| 7ef2af8beb | |||
| 5c9897b156 | |||
| 0b720aa082 | |||
| 3e7277e77d | |||
| 5241626a3a | |||
| 363ef106e2 | |||
| 3c7e4c3dec | |||
| fa196a9fd9 | |||
| f02b365478 | |||
| 0b0e6ae2cd | |||
| da525fc99b | |||
| 78337bce72 | |||
| 87e9311a44 | |||
| 53e62ecf5a | |||
| 10d33a7537 | |||
| ada438398e | |||
| 8c65d8958a | |||
| f8cd7ad023 | |||
| 823adcefd0 | |||
| 5b120a5958 | |||
| c658666b3f | |||
| 7a5a88384d | |||
| 4eb4fae49c | |||
| 58b028ad5f | |||
| 612bce48ad | |||
| 1b5f6bfa66 | |||
| 5913d81a44 | |||
| cb9fd38271 | |||
| 97bcfd1a99 | |||
| 7330496a77 | |||
| 0baee7a120 | |||
| 0a5b2d4c46 | |||
| 9978111bec | |||
| 8e25e9b2a4 | |||
| 4d6402c838 | |||
| b4f636ded9 | |||
| 4018a6933c | |||
| bd29c7282d | |||
| 1f89059c84 | |||
| 74dbba0bdc | |||
| 3556e41612 | |||
| d38a2cd08b | |||
| db77a0f023 | |||
| 3d61c209d2 | |||
| 55de3ac329 | |||
| ee21d83056 | |||
| a49ddeaa02 | |||
| dbba4c4e26 | |||
| 0eef966c3e | |||
| cdf43f2a69 | |||
| 7450ff1028 | |||
| c3ae149ae3 | |||
| 0a05a091d8 | |||
| 6a2a5fe7f5 | |||
| 52f22c00c3 | |||
| 37a85b4c2e | |||
| 63f8b51c88 | |||
| c4aca0dad7 | |||
| b8f114bd43 | |||
| 31364da6d4 | |||
| 853c561314 | |||
| 0aa31a282a | |||
| 41e38234ed | |||
| ba9bcb3894 | |||
| e18f2af54f | |||
| 5afcdbda65 | |||
| ba93533790 | |||
| e4044b7415 | |||
| 26b25e7776 | |||
| 20ade89633 | |||
| 928559890a | |||
| 049e882c35 | |||
| f5f3091313 | |||
| 0a0f915ce6 | |||
| 5f42d423e3 | |||
| 2a226963ee | |||
| ca1c6ff645 | |||
| e22d17dca6 | |||
| f34fa1d701 | |||
| d854ace89f | |||
| 3c0359eb8a | |||
| b591539c8a | |||
| 5d2f168554 | |||
| cf0e9a01f1 | |||
| 86d15e831e | |||
| 8285e21ebb | |||
| 4c6cfeee9e | |||
| 37a683dcb2 | |||
| b5e87c7226 | |||
| dd0ba5975e | |||
| 1b26859141 | |||
| 417abc54e4 | |||
| 5cc489aafe | |||
| c01c7744c7 | |||
| 1b58ac61f4 | |||
| f46e5375df | |||
| 722464daf4 | |||
| 0799b6bc26 | |||
| f5fbb31e6e | |||
| 31a0cf5a4f | |||
| 33fb59f2f7 | |||
| fb43af1299 | |||
| f417427635 | |||
| 1f26262e13 | |||
| 42fccf4713 | |||
| 928faa4bcc | |||
| 3895a5050d | |||
| f92035b6fd | |||
| 37a10c871f | |||
| 8397d536d9 |
@@ -1,4 +1,4 @@
|
|||||||
FROM rust:1.74
|
FROM rust:1.85
|
||||||
|
|
||||||
ARG USERNAME=lldapdev
|
ARG USERNAME=lldapdev
|
||||||
# We need to keep the user as 1001 to match the GitHub runner's UID.
|
# We need to keep the user as 1001 to match the GitHub runner's UID.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
FROM localhost:5000/lldap/lldap:alpine-base
|
FROM localhost:5000/lldap/lldap:alpine-base
|
||||||
# Taken directly from https://github.com/tianon/gosu/blob/master/INSTALL.md
|
# Taken directly from https://github.com/tianon/gosu/blob/master/INSTALL.md
|
||||||
ENV GOSU_VERSION 1.17
|
ENV GOSU_VERSION=1.17
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
\
|
\
|
||||||
apk add --no-cache --virtual .gosu-deps \
|
apk add --no-cache --virtual .gosu-deps \
|
||||||
@@ -15,7 +15,18 @@ RUN set -eux; \
|
|||||||
\
|
\
|
||||||
# verify the signature
|
# verify the signature
|
||||||
export GNUPGHOME="$(mktemp -d)"; \
|
export GNUPGHOME="$(mktemp -d)"; \
|
||||||
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
|
for server in \
|
||||||
|
hkps://keys.openpgp.org \
|
||||||
|
ha.pool.sks-keyservers.net \
|
||||||
|
hkp://p80.pool.sks-keyservers.net:80 \
|
||||||
|
keyserver.ubuntu.com \
|
||||||
|
hkp://keyserver.ubuntu.com:80 \
|
||||||
|
pgp.mit.edu \
|
||||||
|
; do \
|
||||||
|
if gpg --batch --keyserver "$server" --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; then \
|
||||||
|
break; \
|
||||||
|
fi; \
|
||||||
|
done; \
|
||||||
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
|
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
|
||||||
gpgconf --kill all; \
|
gpgconf --kill all; \
|
||||||
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
|
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
FROM localhost:5000/lldap/lldap:debian-base
|
FROM localhost:5000/lldap/lldap:debian-base
|
||||||
# Taken directly from https://github.com/tianon/gosu/blob/master/INSTALL.md
|
# Taken directly from https://github.com/tianon/gosu/blob/master/INSTALL.md
|
||||||
ENV GOSU_VERSION 1.17
|
ENV GOSU_VERSION=1.17
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
# save list of currently installed packages for later so we can clean up
|
# save list of currently installed packages for later so we can clean up
|
||||||
savedAptMark="$(apt-mark showmanual)"; \
|
savedAptMark="$(apt-mark showmanual)"; \
|
||||||
apt-get update; \
|
for i in 1 2 3; do \
|
||||||
apt-get install -y --no-install-recommends ca-certificates gnupg wget; \
|
apt-get update && \
|
||||||
rm -rf /var/lib/apt/lists/*; \
|
apt-get install -y --no-install-recommends wget ca-certificates gnupg && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && break || sleep 5; \
|
||||||
|
done; \
|
||||||
\
|
\
|
||||||
dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
|
dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
|
||||||
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
|
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
|
||||||
@@ -14,7 +17,18 @@ RUN set -eux; \
|
|||||||
\
|
\
|
||||||
# verify the signature
|
# verify the signature
|
||||||
export GNUPGHOME="$(mktemp -d)"; \
|
export GNUPGHOME="$(mktemp -d)"; \
|
||||||
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
|
for server in \
|
||||||
|
hkps://keys.openpgp.org \
|
||||||
|
ha.pool.sks-keyservers.net \
|
||||||
|
hkp://p80.pool.sks-keyservers.net:80 \
|
||||||
|
keyserver.ubuntu.com \
|
||||||
|
hkp://keyserver.ubuntu.com:80 \
|
||||||
|
pgp.mit.edu \
|
||||||
|
; do \
|
||||||
|
if gpg --batch --keyserver "$server" --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; then \
|
||||||
|
break; \
|
||||||
|
fi; \
|
||||||
|
done; \
|
||||||
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
|
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
|
||||||
gpgconf --kill all; \
|
gpgconf --kill all; \
|
||||||
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
|
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Keep tracking base image
|
# Keep tracking base image
|
||||||
FROM rust:1.81-slim-bookworm
|
FROM rust:1.85-slim-bookworm
|
||||||
|
|
||||||
# Set needed env path
|
# Set needed env path
|
||||||
ENV PATH="/opt/armv7l-linux-musleabihf-cross/:/opt/armv7l-linux-musleabihf-cross/bin/:/opt/aarch64-linux-musl-cross/:/opt/aarch64-linux-musl-cross/bin/:/opt/x86_64-linux-musl-cross/:/opt/x86_64-linux-musl-cross/bin/:$PATH"
|
ENV PATH="/opt/armv7l-linux-musleabihf-cross/:/opt/armv7l-linux-musleabihf-cross/bin/:/opt/aarch64-linux-musl-cross/:/opt/aarch64-linux-musl-cross/bin/:/opt/x86_64-linux-musl-cross/:/opt/x86_64-linux-musl-cross/bin/:$PATH"
|
||||||
|
|||||||
@@ -84,10 +84,10 @@ jobs:
|
|||||||
needs: pre_job
|
needs: pre_job
|
||||||
if: ${{ needs.pre_job.outputs.should_skip != 'true' || github.event_name == 'release' }}
|
if: ${{ needs.pre_job.outputs.should_skip != 'true' || github.event_name == 'release' }}
|
||||||
container:
|
container:
|
||||||
image: lldap/rust-dev:v81
|
image: lldap/rust-dev:latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@@ -125,14 +125,14 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
target: [armv7-unknown-linux-musleabihf, aarch64-unknown-linux-musl, x86_64-unknown-linux-musl]
|
target: [armv7-unknown-linux-musleabihf, aarch64-unknown-linux-musl, x86_64-unknown-linux-musl]
|
||||||
container:
|
container:
|
||||||
image: lldap/rust-dev:v81
|
image: lldap/rust-dev:latest
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
RUSTFLAGS: -Ctarget-feature=+crt-static
|
RUSTFLAGS: -Ctarget-feature=+crt-static
|
||||||
CARGO_HOME: ${GITHUB_WORKSPACE}/.cargo
|
CARGO_HOME: ${GITHUB_WORKSPACE}/.cargo
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@@ -216,6 +216,8 @@ jobs:
|
|||||||
LLDAP_database_url: postgres://lldapuser:lldappass@localhost/lldap
|
LLDAP_database_url: postgres://lldapuser:lldappass@localhost/lldap
|
||||||
LLDAP_ldap_port: 3890
|
LLDAP_ldap_port: 3890
|
||||||
LLDAP_http_port: 17170
|
LLDAP_http_port: 17170
|
||||||
|
LLDAP_JWT_SECRET: verysecret
|
||||||
|
LLDAP_LDAP_USER_PASS: password
|
||||||
|
|
||||||
|
|
||||||
- name: Run lldap with mariadb DB (MySQL Compatible) and healthcheck
|
- name: Run lldap with mariadb DB (MySQL Compatible) and healthcheck
|
||||||
@@ -227,6 +229,8 @@ jobs:
|
|||||||
LLDAP_database_url: mysql://lldapuser:lldappass@localhost/lldap
|
LLDAP_database_url: mysql://lldapuser:lldappass@localhost/lldap
|
||||||
LLDAP_ldap_port: 3891
|
LLDAP_ldap_port: 3891
|
||||||
LLDAP_http_port: 17171
|
LLDAP_http_port: 17171
|
||||||
|
LLDAP_JWT_SECRET: verysecret
|
||||||
|
LLDAP_LDAP_USER_PASS: password
|
||||||
|
|
||||||
|
|
||||||
- name: Run lldap with sqlite DB and healthcheck
|
- name: Run lldap with sqlite DB and healthcheck
|
||||||
@@ -238,6 +242,8 @@ jobs:
|
|||||||
LLDAP_database_url: sqlite://users.db?mode=rwc
|
LLDAP_database_url: sqlite://users.db?mode=rwc
|
||||||
LLDAP_ldap_port: 3892
|
LLDAP_ldap_port: 3892
|
||||||
LLDAP_http_port: 17172
|
LLDAP_http_port: 17172
|
||||||
|
LLDAP_JWT_SECRET: verysecret
|
||||||
|
LLDAP_LDAP_USER_PASS: password
|
||||||
|
|
||||||
- name: Check DB container logs
|
- name: Check DB container logs
|
||||||
run: |
|
run: |
|
||||||
@@ -294,7 +300,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout scripts
|
- name: Checkout scripts
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
with:
|
with:
|
||||||
sparse-checkout: 'scripts'
|
sparse-checkout: 'scripts'
|
||||||
|
|
||||||
@@ -324,9 +330,9 @@ jobs:
|
|||||||
sleep 10s
|
sleep 10s
|
||||||
bin/lldap healthcheck
|
bin/lldap healthcheck
|
||||||
env:
|
env:
|
||||||
LLDAP_database_url: sqlite://users.db?mode=rwc
|
LLDAP_DATABASE_URL: sqlite://users.db?mode=rwc
|
||||||
LLDAP_ldap_port: 3890
|
LLDAP_LDAP_PORT: 3890
|
||||||
LLDAP_http_port: 17170
|
LLDAP_HTTP_PORT: 17170
|
||||||
LLDAP_LDAP_USER_PASS: ldappass
|
LLDAP_LDAP_USER_PASS: ldappass
|
||||||
LLDAP_JWT_SECRET: somejwtsecret
|
LLDAP_JWT_SECRET: somejwtsecret
|
||||||
|
|
||||||
@@ -350,8 +356,11 @@ jobs:
|
|||||||
sed -i -r -e "s/X'([[:xdigit:]]+'[^'])/'\\\x\\1/g" -e ":a; s/(INSERT INTO (user_attribute_schema|jwt_storage)\(.*\) VALUES\(.*),1([^']*\);)$/\1,true\3/; s/(INSERT INTO (user_attribute_schema|jwt_storage)\(.*\) VALUES\(.*),0([^']*\);)$/\1,false\3/; ta" -e '1s/^/BEGIN;\n/' -e '$aCOMMIT;' ./dump.sql
|
sed -i -r -e "s/X'([[:xdigit:]]+'[^'])/'\\\x\\1/g" -e ":a; s/(INSERT INTO (user_attribute_schema|jwt_storage)\(.*\) VALUES\(.*),1([^']*\);)$/\1,true\3/; s/(INSERT INTO (user_attribute_schema|jwt_storage)\(.*\) VALUES\(.*),0([^']*\);)$/\1,false\3/; ta" -e '1s/^/BEGIN;\n/' -e '$aCOMMIT;' ./dump.sql
|
||||||
|
|
||||||
- name: Create schema on postgres
|
- name: Create schema on postgres
|
||||||
|
env:
|
||||||
|
LLDAP_DATABASE_URL: postgres://lldapuser:lldappass@localhost:5432/lldap
|
||||||
|
LLDAP_JWT_SECRET: somejwtsecret
|
||||||
run: |
|
run: |
|
||||||
bin/lldap create_schema -d postgres://lldapuser:lldappass@localhost:5432/lldap
|
bin/lldap create_schema
|
||||||
|
|
||||||
- name: Copy converted db to postgress and import
|
- name: Copy converted db to postgress and import
|
||||||
run: |
|
run: |
|
||||||
@@ -368,7 +377,10 @@ jobs:
|
|||||||
sed -i '1 i\SET FOREIGN_KEY_CHECKS = 0;' ./dump.sql
|
sed -i '1 i\SET FOREIGN_KEY_CHECKS = 0;' ./dump.sql
|
||||||
|
|
||||||
- name: Create schema on mariadb
|
- name: Create schema on mariadb
|
||||||
run: bin/lldap create_schema -d mysql://lldapuser:lldappass@localhost:3306/lldap
|
env:
|
||||||
|
LLDAP_DATABASE_URL: mysql://lldapuser:lldappass@localhost:3306/lldap
|
||||||
|
LLDAP_JWT_SECRET: somejwtsecret
|
||||||
|
run: bin/lldap create_schema
|
||||||
|
|
||||||
- name: Copy converted db to mariadb and import
|
- name: Copy converted db to mariadb and import
|
||||||
run: |
|
run: |
|
||||||
@@ -384,7 +396,10 @@ jobs:
|
|||||||
sed -i '1 i\SET FOREIGN_KEY_CHECKS = 0;' ./dump.sql
|
sed -i '1 i\SET FOREIGN_KEY_CHECKS = 0;' ./dump.sql
|
||||||
|
|
||||||
- name: Create schema on mysql
|
- name: Create schema on mysql
|
||||||
run: bin/lldap create_schema -d mysql://lldapuser:lldappass@localhost:3307/lldap
|
env:
|
||||||
|
LLDAP_DATABASE_URL: mysql://lldapuser:lldappass@localhost:3307/lldap
|
||||||
|
LLDAP_JWT_SECRET: somejwtsecret
|
||||||
|
run: bin/lldap create_schema
|
||||||
|
|
||||||
- name: Copy converted db to mysql and import
|
- name: Copy converted db to mysql and import
|
||||||
run: |
|
run: |
|
||||||
@@ -399,10 +414,9 @@ jobs:
|
|||||||
sleep 10s
|
sleep 10s
|
||||||
bin/lldap healthcheck
|
bin/lldap healthcheck
|
||||||
env:
|
env:
|
||||||
LLDAP_database_url: postgres://lldapuser:lldappass@localhost:5432/lldap
|
LLDAP_DATABASE_URL: postgres://lldapuser:lldappass@localhost:5432/lldap
|
||||||
LLDAP_ldap_port: 3891
|
LLDAP_LDAP_PORT: 3891
|
||||||
LLDAP_http_port: 17171
|
LLDAP_HTTP_PORT: 17171
|
||||||
LLDAP_LDAP_USER_PASS: ldappass
|
|
||||||
LLDAP_JWT_SECRET: somejwtsecret
|
LLDAP_JWT_SECRET: somejwtsecret
|
||||||
|
|
||||||
- name: Run lldap with mariaDB and healthcheck again
|
- name: Run lldap with mariaDB and healthcheck again
|
||||||
@@ -411,9 +425,9 @@ jobs:
|
|||||||
sleep 10s
|
sleep 10s
|
||||||
bin/lldap healthcheck
|
bin/lldap healthcheck
|
||||||
env:
|
env:
|
||||||
LLDAP_database_url: mysql://lldapuser:lldappass@localhost:3306/lldap
|
LLDAP_DATABASE_URL: mysql://lldapuser:lldappass@localhost:3306/lldap
|
||||||
LLDAP_ldap_port: 3892
|
LLDAP_LDAP_PORT: 3892
|
||||||
LLDAP_http_port: 17172
|
LLDAP_HTTP_PORT: 17172
|
||||||
LLDAP_JWT_SECRET: somejwtsecret
|
LLDAP_JWT_SECRET: somejwtsecret
|
||||||
|
|
||||||
- name: Run lldap with mysql and healthcheck again
|
- name: Run lldap with mysql and healthcheck again
|
||||||
@@ -422,9 +436,9 @@ jobs:
|
|||||||
sleep 10s
|
sleep 10s
|
||||||
bin/lldap healthcheck
|
bin/lldap healthcheck
|
||||||
env:
|
env:
|
||||||
LLDAP_database_url: mysql://lldapuser:lldappass@localhost:3307/lldap
|
LLDAP_DATABASE_URL: mysql://lldapuser:lldappass@localhost:3307/lldap
|
||||||
LLDAP_ldap_port: 3893
|
LLDAP_LDAP_PORT: 3893
|
||||||
LLDAP_http_port: 17173
|
LLDAP_HTTP_PORT: 17173
|
||||||
LLDAP_JWT_SECRET: somejwtsecret
|
LLDAP_JWT_SECRET: somejwtsecret
|
||||||
|
|
||||||
- name: Test Dummy User Postgres
|
- name: Test Dummy User Postgres
|
||||||
@@ -482,7 +496,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
@@ -738,5 +752,9 @@ jobs:
|
|||||||
artifacts: aarch64-lldap.tar.gz,
|
artifacts: aarch64-lldap.tar.gz,
|
||||||
amd64-lldap.tar.gz,
|
amd64-lldap.tar.gz,
|
||||||
armhf-lldap.tar.gz
|
armhf-lldap.tar.gz
|
||||||
|
draft: true
|
||||||
|
omitBodyDuringUpdate: true
|
||||||
|
omitDraftDuringUpdate: true
|
||||||
|
omitNameDuringUpdate: true
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --verbose --workspace
|
run: cargo build --verbose --workspace
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
run: rustup toolchain install nightly --component llvm-tools-preview && rustup component add llvm-tools-preview --toolchain stable-x86_64-unknown-linux-gnu
|
run: rustup toolchain install nightly --component llvm-tools-preview && rustup component add llvm-tools-preview --toolchain stable-x86_64-unknown-linux-gnu
|
||||||
|
|||||||
@@ -5,6 +5,61 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.6.2] 2025-07-21
|
||||||
|
|
||||||
|
Small release, focused on LDAP improvements and ongoing maintenance.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- LDAP
|
||||||
|
- Support for searching groups by their `groupid`
|
||||||
|
- Support for `whoamiOID`
|
||||||
|
- Support for creating groups
|
||||||
|
- Support for subschema entry
|
||||||
|
- Custom assets path.
|
||||||
|
- New endpoint for requesting client settings
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- A missing JWT secret now prevents startup.
|
||||||
|
- Attributes with invalid characters (such as underscores) cannot be created anymore.
|
||||||
|
- Searching custom (string) attributes is now case insensitive.
|
||||||
|
- Using the top-level `firstName`, `lastName` and `avatar` GraphQL fields for users is now deprecated. Use the `attributes` field instead.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `lldap_set_password` now uses the system's SSL certificates.
|
||||||
|
|
||||||
|
### Cleanups
|
||||||
|
|
||||||
|
- Split the main `lldap` crate into many sub-crates
|
||||||
|
- Various dependency version bumps
|
||||||
|
- Upgraded to 2024 Rust edition
|
||||||
|
- Docs/FAQ improvements
|
||||||
|
|
||||||
|
### Bootstrap script
|
||||||
|
|
||||||
|
- Custom attributes support
|
||||||
|
- Read the paswsord from a file
|
||||||
|
- Resilient to no user or group files
|
||||||
|
|
||||||
|
### New services
|
||||||
|
|
||||||
|
- Discord integration (Discord role to LLDAP user)
|
||||||
|
- HashiCorp
|
||||||
|
- Jellyfin 2FA with Duo
|
||||||
|
- Kimai
|
||||||
|
- Mailcow
|
||||||
|
- Peertube
|
||||||
|
- Penpot
|
||||||
|
- PgAdmin
|
||||||
|
- Project Quay
|
||||||
|
- Quadlet
|
||||||
|
- Snipe-IT
|
||||||
|
- SSSD
|
||||||
|
- Stalwart
|
||||||
|
- UnifiOS
|
||||||
|
|
||||||
## [0.6.1] 2024-11-22
|
## [0.6.1] 2024-11-22
|
||||||
|
|
||||||
Small release, mainly to fix a migration issue with Sqlite and Postgresql.
|
Small release, mainly to fix a migration issue with Sqlite and Postgresql.
|
||||||
|
|||||||
Generated
+458
-188
File diff suppressed because it is too large
Load Diff
+20
-7
@@ -1,16 +1,22 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"server",
|
"server",
|
||||||
"auth",
|
"app",
|
||||||
"app",
|
"migration-tool",
|
||||||
"migration-tool",
|
"set-password",
|
||||||
"set-password",
|
"crates/*",
|
||||||
]
|
]
|
||||||
|
|
||||||
default-members = ["server"]
|
default-members = ["server"]
|
||||||
|
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
|
||||||
|
documentation = "https://github.com/lldap/lldap"
|
||||||
|
edition = "2024"
|
||||||
|
homepage = "https://github.com/lldap/lldap"
|
||||||
|
license = "GPL-3.0-only"
|
||||||
|
repository = "https://github.com/lldap/lldap"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
|
||||||
@@ -19,3 +25,10 @@ opt-level = 's'
|
|||||||
|
|
||||||
[patch.crates-io.lber]
|
[patch.crates-io.lber]
|
||||||
git = 'https://github.com/inejge/ldap3/'
|
git = 'https://github.com/inejge/ldap3/'
|
||||||
|
|
||||||
|
[workspace.dependencies.sea-orm]
|
||||||
|
version = "1.1.8"
|
||||||
|
default-features = false
|
||||||
|
|
||||||
|
[workspace.dependencies.serde]
|
||||||
|
version = "1"
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
# Build image
|
# Build image
|
||||||
FROM rust:alpine3.16 AS chef
|
FROM rust:alpine3.21 AS chef
|
||||||
|
|
||||||
RUN set -x \
|
RUN set -x \
|
||||||
# Add user
|
# Add user
|
||||||
@@ -41,9 +41,9 @@ RUN cargo build --release -p lldap -p lldap_migration_tool -p lldap_set_password
|
|||||||
&& ./app/build.sh
|
&& ./app/build.sh
|
||||||
|
|
||||||
# Final image
|
# Final image
|
||||||
FROM alpine:3.19
|
FROM alpine:3.21
|
||||||
|
|
||||||
ENV GOSU_VERSION 1.14
|
ENV GOSU_VERSION=1.14
|
||||||
# Fetch gosu from git
|
# Fetch gosu from git
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
\
|
\
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
build-dev-container:
|
||||||
|
docker buildx build --tag lldap/rust-dev --file .github/workflows/Dockerfile.dev --push .github/workflows
|
||||||
|
|
||||||
|
prepare-release:
|
||||||
|
./prepare-release.sh
|
||||||
@@ -34,29 +34,14 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
- [About](#about)
|
- [About](#about)
|
||||||
- [Installation](#installation)
|
- [Installation](docs/install.md)
|
||||||
- [With Docker](#with-docker)
|
|
||||||
- [With Kubernetes](#with-kubernetes)
|
|
||||||
- [From a package repository](#from-a-package-repository)
|
|
||||||
- [With FreeBSD](#with-freebsd)
|
|
||||||
- [From source](#from-source)
|
|
||||||
- [Backend](#backend)
|
|
||||||
- [Frontend](#frontend)
|
|
||||||
- [Cross-compilation](#cross-compilation)
|
|
||||||
- [Usage](#usage)
|
- [Usage](#usage)
|
||||||
- [Recommended architecture](#recommended-architecture)
|
- [Recommended architecture](#recommended-architecture)
|
||||||
- [Client configuration](#client-configuration)
|
- [Client configuration](#client-configuration)
|
||||||
- [Compatible services](#compatible-services)
|
- [Known compatible services](#known-compatible-services)
|
||||||
- [General configuration guide](#general-configuration-guide)
|
- [General configuration guide](#general-configuration-guide)
|
||||||
- [Integration with OS's](#integration-with-oss)
|
|
||||||
- [Sample client configurations](#sample-client-configurations)
|
|
||||||
- [Incompatible services](#incompatible-services)
|
- [Incompatible services](#incompatible-services)
|
||||||
- [Migrating from SQLite](#migrating-from-sqlite)
|
- [Frequently Asked Questions](#frequently-asked-questions)
|
||||||
- [Comparisons with other services](#comparisons-with-other-services)
|
|
||||||
- [vs OpenLDAP](#vs-openldap)
|
|
||||||
- [vs FreeIPA](#vs-freeipa)
|
|
||||||
- [vs Kanidm](#vs-kanidm)
|
|
||||||
- [I can't log in!](#i-cant-log-in)
|
|
||||||
- [Contributions](#contributions)
|
- [Contributions](#contributions)
|
||||||
|
|
||||||
## About
|
## About
|
||||||
@@ -98,383 +83,9 @@ MySQL/MariaDB or PostgreSQL.
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### With Docker
|
It's possible to install lldap from OCI images ([docker](docs/install.md#with-docker)/[podman](docs/install.md#with-podman)), from [Kubernetes](docs/install.md#with-kubernetes), or from [a regular distribution package manager](docs/install.md/#from-a-package-repository) (Archlinux, Debian, CentOS, Fedora, OpenSuse, Ubuntu, FreeBSD).
|
||||||
|
|
||||||
The image is available at `lldap/lldap`. You should persist the `/data`
|
Building [from source](docs/install.md#from-source) and [cross-compiling](docs/install.md#cross-compilation) to a different hardware architecture is also supported.
|
||||||
folder, which contains your configuration and the SQLite database (you can
|
|
||||||
remove this step if you use a different DB and configure with environment
|
|
||||||
variables only).
|
|
||||||
|
|
||||||
Configure the server by copying the `lldap_config.docker_template.toml` to
|
|
||||||
`/data/lldap_config.toml` and updating the configuration values (especially the
|
|
||||||
`jwt_secret` and `ldap_user_pass`, unless you override them with env variables).
|
|
||||||
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_KEY_SEED_FILE`, and the file
|
|
||||||
contents are loaded into the respective configuration parameters. Note that
|
|
||||||
`_FILE` variables take precedence.
|
|
||||||
|
|
||||||
Example for docker compose:
|
|
||||||
|
|
||||||
- You can use either the `:latest` tag image or `:stable` as used in this example.
|
|
||||||
- `:latest` tag image contains recently pushed code or feature tests, in which some instability can be expected.
|
|
||||||
- If `UID` and `GID` no defined LLDAP will use default `UID` and `GID` number `1000`.
|
|
||||||
- If no `TZ` is set, default `UTC` timezone will be used.
|
|
||||||
- You can generate the secrets by running `./generate_secrets.sh`
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: "3"
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
lldap_data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
services:
|
|
||||||
lldap:
|
|
||||||
image: lldap/lldap:stable
|
|
||||||
ports:
|
|
||||||
# For LDAP, not recommended to expose, see Usage section.
|
|
||||||
#- "3890:3890"
|
|
||||||
# For LDAPS (LDAP Over SSL), enable port if LLDAP_LDAPS_OPTIONS__ENABLED set true, look env below
|
|
||||||
#- "6360:6360"
|
|
||||||
# For the web front-end
|
|
||||||
- "17170:17170"
|
|
||||||
volumes:
|
|
||||||
- "lldap_data:/data"
|
|
||||||
# Alternatively, you can mount a local folder
|
|
||||||
# - "./lldap_data:/data"
|
|
||||||
environment:
|
|
||||||
- UID=####
|
|
||||||
- GID=####
|
|
||||||
- TZ=####/####
|
|
||||||
- LLDAP_JWT_SECRET=REPLACE_WITH_RANDOM
|
|
||||||
- LLDAP_KEY_SEED=REPLACE_WITH_RANDOM
|
|
||||||
- LLDAP_LDAP_BASE_DN=dc=example,dc=com
|
|
||||||
# If using LDAPS, set enabled true and configure cert and key path
|
|
||||||
# - LLDAP_LDAPS_OPTIONS__ENABLED=true
|
|
||||||
# - LLDAP_LDAPS_OPTIONS__CERT_FILE=/path/to/certfile.crt
|
|
||||||
# - LLDAP_LDAPS_OPTIONS__KEY_FILE=/path/to/keyfile.key
|
|
||||||
# You can also set a different database:
|
|
||||||
# - LLDAP_DATABASE_URL=mysql://mysql-user:password@mysql-server/my-database
|
|
||||||
# - LLDAP_DATABASE_URL=postgres://postgres-user:password@postgres-server/my-database
|
|
||||||
# If using SMTP, set the following variables
|
|
||||||
# - LLDAP_SMTP_OPTIONS__ENABLE_PASSWORD_RESET=true
|
|
||||||
# - LLDAP_SMTP_OPTIONS__SERVER=smtp.example.com
|
|
||||||
# - LLDAP_SMTP_OPTIONS__PORT=465 # Check your smtp providor's documentation for this setting
|
|
||||||
# - LLDAP_SMTP_OPTIONS__SMTP_ENCRYPTION=TLS # How the connection is encrypted, either "NONE" (no encryption, port 25), "TLS" (sometimes called SSL, port 465) or "STARTTLS" (sometimes called TLS, port 587).
|
|
||||||
# - LLDAP_SMTP_OPTIONS__USER=no-reply@example.com # The SMTP user, usually your email address
|
|
||||||
# - LLDAP_SMTP_OPTIONS__PASSWORD=PasswordGoesHere # The SMTP password
|
|
||||||
# - LLDAP_SMTP_OPTIONS__FROM=no-reply <no-reply@example.com> # The header field, optional: how the sender appears in the email. The first is a free-form name, followed by an email between <>.
|
|
||||||
# - LLDAP_SMTP_OPTIONS__TO=admin <admin@example.com> # Same for reply-to, optional.
|
|
||||||
```
|
|
||||||
|
|
||||||
Then the service will listen on two ports, one for LDAP and one for the web
|
|
||||||
front-end.
|
|
||||||
|
|
||||||
### With Kubernetes
|
|
||||||
|
|
||||||
See https://github.com/Evantage-WS/lldap-kubernetes for a LLDAP deployment for Kubernetes
|
|
||||||
|
|
||||||
You can bootstrap your lldap instance (users, groups)
|
|
||||||
using [bootstrap.sh](example_configs/bootstrap/bootstrap.md#kubernetes-job).
|
|
||||||
It can be run by Argo CD for managing users in git-opt way, or as a one-shot job.
|
|
||||||
|
|
||||||
### From a package repository
|
|
||||||
|
|
||||||
**Do not open issues in this repository for problems with third-party
|
|
||||||
pre-built packages. Report issues downstream.**
|
|
||||||
|
|
||||||
Depending on the distribution you use, it might be possible to install lldap
|
|
||||||
from a package repository, officially supported by the distribution or
|
|
||||||
community contributed.
|
|
||||||
|
|
||||||
Each package offers a [systemd service](https://wiki.archlinux.org/title/systemd#Using_units) `lldap.service` to (auto-)start and stop lldap.<br>
|
|
||||||
When using the distributed packages, the default login is `admin/password`. You can change that from the web UI after starting the service.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><b>Arch</b></summary>
|
|
||||||
<br>
|
|
||||||
Arch Linux offers unofficial support through the <a href="https://wiki.archlinux.org/title/Arch_User_Repository">Arch User Repository (AUR)</a>.<br>
|
|
||||||
The package descriptions can be used <a href="https://wiki.archlinux.org/title/Arch_User_Repository#Getting_started">to create and install packages</a>.<br><br>
|
|
||||||
Maintainer: <a href="https://github.com/Zepmann">@Zepmann</a><br>
|
|
||||||
Support: <a href="https://github.com/lldap/lldap/discussions">Discussions</a><br>
|
|
||||||
Package repository: <a href="https://aur.archlinux.org/packages">Arch user repository</a><br>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>Available packages:</td>
|
|
||||||
<td><a href="https://aur.archlinux.org/packages/lldap">lldap</a></td>
|
|
||||||
<td>Builds the latest stable version.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td><a href="https://aur.archlinux.org/packages/lldap-bin">lldap-bin</a></td>
|
|
||||||
<td>Uses the latest pre-compiled binaries from the <a href="https://aur.archlinux.org/packages/lldap-bin">releases in this repository</a>.<br>
|
|
||||||
This package is recommended if you want to run lldap on a system with limited resources.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td><a href="https://aur.archlinux.org/packages/lldap-git">lldap-git</a></td>
|
|
||||||
<td>Builds the latest main branch code.</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
LLDPA configuration file: /etc/lldap.toml<br>
|
|
||||||
</details>
|
|
||||||
<details>
|
|
||||||
<summary><b>Debian</b></summary>
|
|
||||||
<br>
|
|
||||||
Unofficial Debian support is offered through the <a href="https://build.opensuse.org/">openSUSE Build Service</a>.<br><br>
|
|
||||||
Maintainer: <a href="https://github.com/Masgalor">@Masgalor</a><br>
|
|
||||||
Support: <a href="https://codeberg.org/Masgalor/LLDAP-Packaging/issues">Codeberg</a>, <a href="https://github.com/lldap/lldap/discussions">Discussions</a><br>
|
|
||||||
Package repository: <a href="https://software.opensuse.org//download.html?project=home%3AMasgalor%3ALLDAP&package=lldap">SUSE openBuildService</a><br>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>Available packages:</td>
|
|
||||||
<td>lldap</td>
|
|
||||||
<td>Light LDAP server for authentication.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>lldap-extras</td>
|
|
||||||
<td>Meta-Package for LLDAP and its tools and extensions.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>lldap-migration-tool</td>
|
|
||||||
<td>CLI migration tool to go from OpenLDAP to LLDAP.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>lldap-set-password</td>
|
|
||||||
<td>CLI tool to set a user password in LLDAP.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>lldap-cli</td>
|
|
||||||
<td>LLDAP-CLI is an unofficial command line interface for LLDAP.</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
LLDPA configuration file: /etc/lldap/lldap_config.toml<br>
|
|
||||||
</details>
|
|
||||||
<details>
|
|
||||||
<summary><b>CentOS</b></summary>
|
|
||||||
<br>
|
|
||||||
Unofficial CentOS support is offered through the <a href="https://build.opensuse.org/">openSUSE Build Service</a>.<br><br>
|
|
||||||
Maintainer: <a href="https://github.com/Masgalor">@Masgalor</a><br>
|
|
||||||
Support: <a href="https://codeberg.org/Masgalor/LLDAP-Packaging/issues">Codeberg</a>, <a href="https://github.com/lldap/lldap/discussions">Discussions</a><br>
|
|
||||||
Package repository: <a href="https://software.opensuse.org//download.html?project=home%3AMasgalor%3ALLDAP&package=lldap">SUSE openBuildService</a><br>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>Available packages:</td>
|
|
||||||
<td>lldap</td>
|
|
||||||
<td>Light LDAP server for authentication.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>lldap-extras</td>
|
|
||||||
<td>Meta-Package for LLDAP and its tools and extensions.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>lldap-migration-tool</td>
|
|
||||||
<td>CLI migration tool to go from OpenLDAP to LLDAP.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>lldap-set-password</td>
|
|
||||||
<td>CLI tool to set a user password in LLDAP.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>lldap-cli</td>
|
|
||||||
<td>LLDAP-CLI is an unofficial command line interface for LLDAP.</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
LLDPA configuration file: /etc/lldap/lldap_config.toml<br>
|
|
||||||
</details>
|
|
||||||
<details>
|
|
||||||
<summary><b>Fedora</b></summary>
|
|
||||||
<br>
|
|
||||||
Unofficial Fedora support is offered through the <a href="https://build.opensuse.org/">openSUSE Build Service</a>.<br><br>
|
|
||||||
Maintainer: <a href="https://github.com/Masgalor">@Masgalor</a><br>
|
|
||||||
Support: <a href="https://codeberg.org/Masgalor/LLDAP-Packaging/issues">Codeberg</a>, <a href="https://github.com/lldap/lldap/discussions">Discussions</a><br>
|
|
||||||
Package repository: <a href="https://software.opensuse.org//download.html?project=home%3AMasgalor%3ALLDAP&package=lldap">SUSE openBuildService</a><br>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>Available packages:</td>
|
|
||||||
<td>lldap</td>
|
|
||||||
<td>Light LDAP server for authentication.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>lldap-extras</td>
|
|
||||||
<td>Meta-Package for LLDAP and its tools and extensions.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>lldap-migration-tool</td>
|
|
||||||
<td>CLI migration tool to go from OpenLDAP to LLDAP.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>lldap-set-password</td>
|
|
||||||
<td>CLI tool to set a user password in LLDAP.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>lldap-cli</td>
|
|
||||||
<td>LLDAP-CLI is an unofficial command line interface for LLDAP.</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
LLDPA configuration file: /etc/lldap/lldap_config.toml<br>
|
|
||||||
</details>
|
|
||||||
<details>
|
|
||||||
<summary><b>OpenSUSE</b></summary>
|
|
||||||
<br>
|
|
||||||
Unofficial OpenSUSE support is offered through the <a href="https://build.opensuse.org/">openSUSE Build Service</a>.<br><br>
|
|
||||||
Maintainer: <a href="https://github.com/Masgalor">@Masgalor</a><br>
|
|
||||||
Support: <a href="https://codeberg.org/Masgalor/LLDAP-Packaging/issues">Codeberg</a>, <a href="https://github.com/lldap/lldap/discussions">Discussions</a><br>
|
|
||||||
Package repository: <a href="https://software.opensuse.org//download.html?project=home%3AMasgalor%3ALLDAP&package=lldap">SUSE openBuildService</a><br>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>Available packages:</td>
|
|
||||||
<td>lldap</td>
|
|
||||||
<td>Light LDAP server for authentication.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>lldap-extras</td>
|
|
||||||
<td>Meta-Package for LLDAP and its tools and extensions.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>lldap-migration-tool</td>
|
|
||||||
<td>CLI migration tool to go from OpenLDAP to LLDAP.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>lldap-set-password</td>
|
|
||||||
<td>CLI tool to set a user password in LLDAP.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>lldap-cli</td>
|
|
||||||
<td>LLDAP-CLI is an unofficial command line interface for LLDAP.</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
LLDPA configuration file: /etc/lldap/lldap_config.toml<br>
|
|
||||||
</details>
|
|
||||||
<details>
|
|
||||||
<summary><b>Ubuntu</b></summary>
|
|
||||||
<br>
|
|
||||||
Unofficial Ubuntu support is offered through the <a href="https://build.opensuse.org/">openSUSE Build Service</a>.<br><br>
|
|
||||||
Maintainer: <a href="https://github.com/Masgalor">@Masgalor</a><br>
|
|
||||||
Support: <a href="https://codeberg.org/Masgalor/LLDAP-Packaging/issues">Codeberg</a>, <a href="https://github.com/lldap/lldap/discussions">Discussions</a><br>
|
|
||||||
Package repository: <a href="https://software.opensuse.org//download.html?project=home%3AMasgalor%3ALLDAP&package=lldap">SUSE openBuildService</a><br>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>Available packages:</td>
|
|
||||||
<td>lldap</td>
|
|
||||||
<td>Light LDAP server for authentication.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>lldap-extras</td>
|
|
||||||
<td>Meta-Package for LLDAP and its tools and extensions.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>lldap-migration-tool</td>
|
|
||||||
<td>CLI migration tool to go from OpenLDAP to LLDAP.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>lldap-set-password</td>
|
|
||||||
<td>CLI tool to set a user password in LLDAP.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>lldap-cli</td>
|
|
||||||
<td>LLDAP-CLI is an unofficial command line interface for LLDAP.</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
LLDPA configuration file: /etc/lldap/lldap_config.toml<br>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### With FreeBSD
|
|
||||||
|
|
||||||
You can also install it as a rc.d service in FreeBSD, see
|
|
||||||
[FreeBSD-install.md](example_configs/freebsd/freebsd-install.md).
|
|
||||||
|
|
||||||
The rc.d script file
|
|
||||||
[rc.d_lldap](example_configs/freebsd/rc.d_lldap).
|
|
||||||
|
|
||||||
### From source
|
|
||||||
|
|
||||||
#### Backend
|
|
||||||
|
|
||||||
To compile the project, you'll need:
|
|
||||||
|
|
||||||
- curl and gzip: `sudo apt install curl gzip`
|
|
||||||
- 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 lldap_migration_tool
|
|
||||||
```
|
|
||||||
|
|
||||||
The resulting binaries will be in `./target/release/`. Alternatively, you can
|
|
||||||
just run `cargo run -- run` to run the server.
|
|
||||||
|
|
||||||
#### Frontend
|
|
||||||
|
|
||||||
To bring up the server, you'll need to compile the frontend. In addition to
|
|
||||||
`cargo`, you'll need WASM-pack, which can be installed by running `cargo install wasm-pack`.
|
|
||||||
|
|
||||||
Then you can build the frontend files with
|
|
||||||
|
|
||||||
```shell
|
|
||||||
./app/build.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
(you'll need to run this after every front-end change to update the WASM
|
|
||||||
package served).
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Docker images are provided for AMD64, ARM64 and ARM/V7.
|
|
||||||
|
|
||||||
If you want to cross-compile yourself, you can do so by installing
|
|
||||||
[`cross`](https://github.com/rust-embedded/cross):
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo install cross
|
|
||||||
cross build --target=armv7-unknown-linux-musleabihf -p lldap --release
|
|
||||||
./app/build.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
(Replace `armv7-unknown-linux-musleabihf` with the correct Rust target for your
|
|
||||||
device.)
|
|
||||||
|
|
||||||
You can then get the compiled server binary in
|
|
||||||
`target/armv7-unknown-linux-musleabihf/release/lldap` and the various needed files
|
|
||||||
(`index.html`, `main.js`, `pkg` folder) in the `app` folder. Copy them to the
|
|
||||||
Raspberry Pi (or other target), with the folder structure maintained (`app`
|
|
||||||
files in an `app` folder next to the binary).
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -525,7 +136,7 @@ If you are using containers, a sample architecture could look like this:
|
|||||||
|
|
||||||
## Client configuration
|
## Client configuration
|
||||||
|
|
||||||
### Compatible services
|
### Known compatible services
|
||||||
|
|
||||||
Most services that can use LDAP as an authentication provider should work out
|
Most services that can use LDAP as an authentication provider should work out
|
||||||
of the box. For new services, it's possible that they require a bit of tweaking
|
of the box. For new services, it's possible that they require a bit of tweaking
|
||||||
@@ -533,6 +144,13 @@ on LLDAP's side to make things work. In that case, just create an issue with
|
|||||||
the relevant details (logs of the service, LLDAP logs with `verbose=true` in
|
the relevant details (logs of the service, LLDAP logs with `verbose=true` in
|
||||||
the config).
|
the config).
|
||||||
|
|
||||||
|
Some specific clients have been tested to work and come with sample
|
||||||
|
configuration files, or guides. See the [`example_configs`](example_configs)
|
||||||
|
folder for example configs for integration with specific services.
|
||||||
|
|
||||||
|
Integration with Linux accounts is possible, through PAM and nslcd. See [PAM
|
||||||
|
configuration guide](example_configs/pam/README.md). Integration with Windows (e.g. Samba) is WIP.
|
||||||
|
|
||||||
### General configuration guide
|
### General configuration guide
|
||||||
|
|
||||||
To configure the services that will talk to LLDAP, here are the values:
|
To configure the services that will talk to LLDAP, here are the values:
|
||||||
@@ -552,83 +170,9 @@ filter like: `(memberOf=cn=admins,ou=groups,dc=example,dc=com)`.
|
|||||||
The administrator group for LLDAP is `lldap_admin`: anyone in this group has
|
The administrator group for LLDAP is `lldap_admin`: anyone in this group has
|
||||||
admin rights in the Web UI. Most LDAP integrations should instead use a user in
|
admin rights in the Web UI. Most LDAP integrations should instead use a user in
|
||||||
the `lldap_strict_readonly` or `lldap_password_manager` group, to avoid granting full
|
the `lldap_strict_readonly` or `lldap_password_manager` group, to avoid granting full
|
||||||
administration access to many services.
|
administration access to many services. To prevent privilege escalation users in the
|
||||||
|
`lldap_password_manager` group are not allowed to change passwords of admins in the
|
||||||
### Integration with OS's
|
`lldap_admin` group.
|
||||||
|
|
||||||
Integration with Linux accounts is possible, through PAM and nslcd. See [PAM
|
|
||||||
configuration guide](example_configs/pam/README.md).
|
|
||||||
|
|
||||||
Integration with Windows (e.g. Samba) is WIP.
|
|
||||||
|
|
||||||
### Sample client configurations
|
|
||||||
|
|
||||||
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)
|
|
||||||
- [Apereo CAS Server](example_configs/apereo_cas_server.md)
|
|
||||||
- [Authelia](example_configs/authelia_config.yml)
|
|
||||||
- [Authentik](example_configs/authentik.md)
|
|
||||||
- [Bookstack](example_configs/bookstack.env.example)
|
|
||||||
- [Calibre-Web](example_configs/calibre_web.md)
|
|
||||||
- [Carpal](example_configs/carpal.md)
|
|
||||||
- [Dell iDRAC](example_configs/dell_idrac.md)
|
|
||||||
- [Dex](example_configs/dex_config.yml)
|
|
||||||
- [Dokuwiki](example_configs/dokuwiki.md)
|
|
||||||
- [Dolibarr](example_configs/dolibarr.md)
|
|
||||||
- [Ejabberd](example_configs/ejabberd.md)
|
|
||||||
- [Emby](example_configs/emby.md)
|
|
||||||
- [Ergo IRCd](example_configs/ergo.md)
|
|
||||||
- [Gitea](example_configs/gitea.md)
|
|
||||||
- [GitLab](example_configs/gitlab.md)
|
|
||||||
- [Grafana](example_configs/grafana_ldap_config.toml)
|
|
||||||
- [Grocy](example_configs/grocy.md)
|
|
||||||
- [Harbor](example_configs/harbor.md)
|
|
||||||
- [Hedgedoc](example_configs/hedgedoc.md)
|
|
||||||
- [Home Assistant](example_configs/home-assistant.md)
|
|
||||||
- [Jellyfin](example_configs/jellyfin.md)
|
|
||||||
- [Jenkins](example_configs/jenkins.md)
|
|
||||||
- [Jitsi Meet](example_configs/jitsi_meet.conf)
|
|
||||||
- [Kasm](example_configs/kasm.md)
|
|
||||||
- [KeyCloak](example_configs/keycloak.md)
|
|
||||||
- [LibreNMS](example_configs/librenms.md)
|
|
||||||
- [Maddy](example_configs/maddy.md)
|
|
||||||
- [Mastodon](example_configs/mastodon.env.example)
|
|
||||||
- [Matrix](example_configs/matrix_synapse.yml)
|
|
||||||
- [Mealie](example_configs/mealie.md)
|
|
||||||
- [Metabase](example_configs/metabase.md)
|
|
||||||
- [MegaRAC-BMC](example_configs/MegaRAC-SP-X-BMC.md)
|
|
||||||
- [MinIO](example_configs/minio.md)
|
|
||||||
- [Netbox](example_configs/netbox.md)
|
|
||||||
- [Nextcloud](example_configs/nextcloud.md)
|
|
||||||
- [Nexus](example_configs/nexus.md)
|
|
||||||
- [OCIS (OwnCloud Infinite Scale)](example_configs/ocis.md)
|
|
||||||
- [OneDev](example_configs/onedev.md)
|
|
||||||
- [Organizr](example_configs/Organizr.md)
|
|
||||||
- [Portainer](example_configs/portainer.md)
|
|
||||||
- [PowerDNS Admin](example_configs/powerdns_admin.md)
|
|
||||||
- [Prosody](example_configs/prosody.md)
|
|
||||||
- [Proxmox VE](example_configs/proxmox.md)
|
|
||||||
- [Radicale](example_configs/radicale.md)
|
|
||||||
- [Rancher](example_configs/rancher.md)
|
|
||||||
- [Seafile](example_configs/seafile.md)
|
|
||||||
- [Shaarli](example_configs/shaarli.md)
|
|
||||||
- [SonarQube](example_configs/sonarqube.md)
|
|
||||||
- [Squid](example_configs/squid.md)
|
|
||||||
- [Syncthing](example_configs/syncthing.md)
|
|
||||||
- [TheLounge](example_configs/thelounge.md)
|
|
||||||
- [Traccar](example_configs/traccar.xml)
|
|
||||||
- [Vaultwarden](example_configs/vaultwarden.md)
|
|
||||||
- [WeKan](example_configs/wekan.md)
|
|
||||||
- [WG Portal](example_configs/wg_portal.env.example)
|
|
||||||
- [WikiJS](example_configs/wikijs.md)
|
|
||||||
- [XBackBone](example_configs/xbackbone_config.php)
|
|
||||||
- [Zendto](example_configs/zendto.md)
|
|
||||||
- [Zitadel](example_configs/zitadel.md)
|
|
||||||
- [Zulip](example_configs/zulip.md)
|
|
||||||
|
|
||||||
### Incompatible services
|
### Incompatible services
|
||||||
|
|
||||||
@@ -651,76 +195,16 @@ it duplicates the places from which a password hash could leak.
|
|||||||
In that category, the most prominent is Synology. It is, to date, the only
|
In that category, the most prominent is Synology. It is, to date, the only
|
||||||
service that seems definitely incompatible with LLDAP.
|
service that seems definitely incompatible with LLDAP.
|
||||||
|
|
||||||
## Migrating from SQLite
|
## Frequently Asked Questions
|
||||||
|
|
||||||
If you started with an SQLite database and would like to migrate to
|
- [I can't login](docs/faq.md#i-cant-log-in)
|
||||||
MySQL/MariaDB or PostgreSQL, check out the [DB
|
- [Discord Integration](docs/faq.md#discord-integration)
|
||||||
migration docs](/docs/database_migration.md).
|
- [Migrating from SQLite](docs/faq.md#migrating-from-sqlite)
|
||||||
|
- How does lldap compare [with OpenLDAP](docs/faq.md#how-does-lldap-compare-with-openldap)? [With FreeIPA](docs/faq.md#how-does-lldap-compare-with-freeipa)? [With Kanidm](docs/faq.md#how-does-lldap-compare-with-kanidm)?
|
||||||
## Comparisons with other services
|
- [Does lldap support vhosts?](docs/faq.md#does-lldap-support-vhosts)
|
||||||
|
- [Does lldap provide commercial support contracts?](docs/faq.md#does-lldap-provide-commercial-support-contracts)
|
||||||
### vs OpenLDAP
|
- [Can I make a donation to fund development?](docs/faq.md#can-i-make-a-donation-to-fund-development)
|
||||||
|
- [Is lldap sustainable? Can we depend on it for our infrastructure?](docs/faq.md#is-lldap-sustainable-can-we-depend-on-it-for-our-infrastructure)
|
||||||
[OpenLDAP](https://www.openldap.org) is a monster of a service that implements
|
|
||||||
all of LDAP and all of its extensions, plus some of its own. That said, if you
|
|
||||||
need all that flexibility, it might be what you need! Note that installation
|
|
||||||
can be a bit painful (figuring out how to use `slapd`) and people have mixed
|
|
||||||
experiences following tutorials online. If you don't configure it properly, you
|
|
||||||
might end up storing passwords in clear, so a breach of your server would
|
|
||||||
reveal all the stored passwords!
|
|
||||||
|
|
||||||
OpenLDAP doesn't come with a UI: if you want a web interface, you'll have to
|
|
||||||
install one (not that many look nice) and configure it.
|
|
||||||
|
|
||||||
LLDAP is much simpler to setup, has a much smaller image (10x smaller, 20x if
|
|
||||||
you add PhpLdapAdmin), and comes packed with its own purpose-built web UI.
|
|
||||||
However, it's not as flexible as OpenLDAP.
|
|
||||||
|
|
||||||
### vs FreeIPA
|
|
||||||
|
|
||||||
[FreeIPA](http://www.freeipa.org) is the one-stop shop for identity management:
|
|
||||||
LDAP, Kerberos, NTP, DNS, Samba, you name it, it has it. In addition to user
|
|
||||||
management, it also does security policies, single sign-on, certificate
|
|
||||||
management, linux account management and so on.
|
|
||||||
|
|
||||||
If you need all of that, go for it! Keep in mind that a more complex system is
|
|
||||||
more complex to maintain, though.
|
|
||||||
|
|
||||||
LLDAP is much lighter to run (<10 MB RAM including the DB), easier to
|
|
||||||
configure (no messing around with DNS or security policies) and simpler to
|
|
||||||
use. It also comes conveniently packed in a docker container.
|
|
||||||
|
|
||||||
### vs Kanidm
|
|
||||||
|
|
||||||
[Kanidm](https://kanidm.com) is an up-and-coming Rust identity management
|
|
||||||
platform, covering all your bases: OAuth, Linux accounts, SSH keys, Radius,
|
|
||||||
WebAuthn. It comes with a (read-only) LDAPS server.
|
|
||||||
|
|
||||||
It's fairly easy to install and does much more; but their LDAP server is
|
|
||||||
read-only, and by having more moving parts it is inherently more complex. If
|
|
||||||
you don't need to modify the users through LDAP and you're planning on
|
|
||||||
installing something like [KeyCloak](https://www.keycloak.org) to provide
|
|
||||||
modern identity protocols, check out Kanidm.
|
|
||||||
|
|
||||||
## I can't log in!
|
|
||||||
|
|
||||||
If you just set up the server, can get to the login page but the password you
|
|
||||||
set isn't working, try the following:
|
|
||||||
|
|
||||||
- (For docker): Make sure that the `/data` folder is persistent, either to a
|
|
||||||
docker volume or mounted from the host filesystem.
|
|
||||||
- Check if there is a `lldap_config.toml` file (either in `/data` for docker
|
|
||||||
or in the current directory). If there isn't, copy
|
|
||||||
`lldap_config.docker_template.toml` there, and fill in the various values
|
|
||||||
(passwords, secrets, ...).
|
|
||||||
- Check if there is a `users.db` file (either in `/data` for docker or where
|
|
||||||
you specified the DB URL, which defaults to the current directory). If
|
|
||||||
there isn't, check that the user running the command (user with ID 10001
|
|
||||||
for docker) has the rights to write to the `/data` folder. If in doubt, you
|
|
||||||
can `chmod 777 /data` (or whatever the folder) to make it world-writeable.
|
|
||||||
- Make sure you restart the server.
|
|
||||||
- If it's still not working, join the
|
|
||||||
[Discord server](https://discord.gg/h5PEdRMNyP) to ask for help.
|
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
|
|||||||
+32
-10
@@ -1,13 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
|
|
||||||
description = "Frontend for LLDAP"
|
|
||||||
edition = "2021"
|
|
||||||
homepage = "https://github.com/lldap/lldap"
|
|
||||||
license = "GPL-3.0-only"
|
|
||||||
name = "lldap_app"
|
name = "lldap_app"
|
||||||
repository = "https://github.com/lldap/lldap"
|
version = "0.6.2"
|
||||||
version = "0.6.1"
|
description = "Frontend for LLDAP"
|
||||||
|
edition.workspace = true
|
||||||
include = ["src/**/*", "queries/**/*", "Cargo.toml", "../schema.graphql"]
|
include = ["src/**/*", "queries/**/*", "Cargo.toml", "../schema.graphql"]
|
||||||
|
authors.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
@@ -19,12 +19,11 @@ graphql_client = "0.10"
|
|||||||
http = "0.2"
|
http = "0.2"
|
||||||
jwt = "0.13"
|
jwt = "0.13"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
serde = "1"
|
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
url-escape = "0.1.1"
|
url-escape = "0.1.1"
|
||||||
validator = "0.14"
|
validator = "0.14"
|
||||||
validator_derive = "0.14"
|
validator_derive = "0.14"
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2.100"
|
||||||
wasm-bindgen-futures = "*"
|
wasm-bindgen-futures = "*"
|
||||||
yew = "0.19.3"
|
yew = "0.19.3"
|
||||||
yew-router = "0.16"
|
yew-router = "0.16"
|
||||||
@@ -56,15 +55,33 @@ features = [
|
|||||||
"wasmbind"
|
"wasmbind"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[dependencies.derive_more]
|
||||||
|
features = ["debug", "display", "from", "from_str"]
|
||||||
|
default-features = false
|
||||||
|
version = "1"
|
||||||
|
|
||||||
[dependencies.lldap_auth]
|
[dependencies.lldap_auth]
|
||||||
path = "../auth"
|
path = "../crates/auth"
|
||||||
features = [ "opaque_client" ]
|
features = [ "opaque_client" ]
|
||||||
|
|
||||||
|
[dependencies.lldap_frontend_options]
|
||||||
|
path = "../crates/frontend-options"
|
||||||
|
|
||||||
|
[dependencies.lldap_validation]
|
||||||
|
path = "../crates/validation"
|
||||||
|
|
||||||
[dependencies.image]
|
[dependencies.image]
|
||||||
features = ["jpeg"]
|
features = ["jpeg"]
|
||||||
default-features = false
|
default-features = false
|
||||||
version = "0.24"
|
version = "0.24"
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies.strum]
|
||||||
|
features = ["derive"]
|
||||||
|
version = "0.25"
|
||||||
|
|
||||||
[dependencies.yew_form]
|
[dependencies.yew_form]
|
||||||
git = "https://github.com/jfbilodeau/yew_form"
|
git = "https://github.com/jfbilodeau/yew_form"
|
||||||
rev = "4b9fabffb63393ec7626a4477fd36de12a07fac9"
|
rev = "4b9fabffb63393ec7626a4477fd36de12a07fac9"
|
||||||
@@ -76,6 +93,11 @@ rev = "4b9fabffb63393ec7626a4477fd36de12a07fac9"
|
|||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||||
|
'cfg(wasm_bindgen_unstable_test_coverage)',
|
||||||
|
] }
|
||||||
|
|
||||||
[package.metadata.wasm-pack.profile.dev]
|
[package.metadata.wasm-pack.profile.dev]
|
||||||
wasm-opt = ['--enable-bulk-memory']
|
wasm-opt = ['--enable-bulk-memory']
|
||||||
[package.metadata.wasm-pack.profile.profiling]
|
[package.metadata.wasm-pack.profile.profiling]
|
||||||
|
|||||||
+36
-26
@@ -21,16 +21,16 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use gloo_console::error;
|
use gloo_console::error;
|
||||||
|
use lldap_frontend_options::Options;
|
||||||
use yew::{
|
use yew::{
|
||||||
function_component,
|
Context, function_component,
|
||||||
html::Scope,
|
html::Scope,
|
||||||
prelude::{html, Component, Html},
|
prelude::{Component, Html, html},
|
||||||
Context,
|
|
||||||
};
|
};
|
||||||
use yew_router::{
|
use yew_router::{
|
||||||
|
BrowserRouter, Switch,
|
||||||
prelude::{History, Location},
|
prelude::{History, Location},
|
||||||
scope_ext::RouterScopeExt,
|
scope_ext::RouterScopeExt,
|
||||||
BrowserRouter, Switch,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[function_component(AppContainer)]
|
#[function_component(AppContainer)]
|
||||||
@@ -51,7 +51,7 @@ pub struct App {
|
|||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
Login((String, bool)),
|
Login((String, bool)),
|
||||||
Logout,
|
Logout,
|
||||||
PasswordResetProbeFinished(anyhow::Result<bool>),
|
SettingsReceived(anyhow::Result<Options>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for App {
|
impl Component for App {
|
||||||
@@ -76,9 +76,8 @@ impl Component for App {
|
|||||||
redirect_to: Self::get_redirect_route(ctx),
|
redirect_to: Self::get_redirect_route(ctx),
|
||||||
password_reset_enabled: None,
|
password_reset_enabled: None,
|
||||||
};
|
};
|
||||||
ctx.link().send_future(async move {
|
ctx.link()
|
||||||
Msg::PasswordResetProbeFinished(HostService::probe_password_reset().await)
|
.send_future(async move { Msg::SettingsReceived(HostService::get_settings().await) });
|
||||||
});
|
|
||||||
app.apply_initial_redirections(ctx);
|
app.apply_initial_redirections(ctx);
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
@@ -103,14 +102,11 @@ impl Component for App {
|
|||||||
self.redirect_to = None;
|
self.redirect_to = None;
|
||||||
history.push(AppRoute::Login);
|
history.push(AppRoute::Login);
|
||||||
}
|
}
|
||||||
Msg::PasswordResetProbeFinished(Ok(enabled)) => {
|
Msg::SettingsReceived(Ok(settings)) => {
|
||||||
self.password_reset_enabled = Some(enabled);
|
self.password_reset_enabled = Some(settings.password_reset_enabled);
|
||||||
}
|
}
|
||||||
Msg::PasswordResetProbeFinished(Err(err)) => {
|
Msg::SettingsReceived(Err(err)) => {
|
||||||
self.password_reset_enabled = Some(false);
|
error!(err.to_string());
|
||||||
error!(&format!(
|
|
||||||
"Could not probe for password reset support: {err:#}"
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@@ -126,7 +122,7 @@ impl Component for App {
|
|||||||
<Banner is_admin={is_admin} username={username} on_logged_out={link.callback(|_| Msg::Logout)} />
|
<Banner is_admin={is_admin} username={username} on_logged_out={link.callback(|_| Msg::Logout)} />
|
||||||
<div class="container py-3 bg-kug">
|
<div class="container py-3 bg-kug">
|
||||||
<div class="row justify-content-center" style="padding-bottom: 80px;">
|
<div class="row justify-content-center" style="padding-bottom: 80px;">
|
||||||
<main class="py-3" style="max-width: 1000px">
|
<main class="py-3">
|
||||||
<Switch<AppRoute>
|
<Switch<AppRoute>
|
||||||
render={Switch::render(move |routes| Self::dispatch_route(routes, &link, is_admin, password_reset_enabled))}
|
render={Switch::render(move |routes| Self::dispatch_route(routes, &link, is_admin, password_reset_enabled))}
|
||||||
/>
|
/>
|
||||||
@@ -200,15 +196,21 @@ impl App {
|
|||||||
AppRoute::CreateUser => html! {
|
AppRoute::CreateUser => html! {
|
||||||
<CreateUserForm/>
|
<CreateUserForm/>
|
||||||
},
|
},
|
||||||
AppRoute::Index | AppRoute::ListUsers => html! {
|
AppRoute::Index | AppRoute::ListUsers => {
|
||||||
<div>
|
let user_button = html! {
|
||||||
<UserTable />
|
|
||||||
<Link classes="btn btn-primary" to={AppRoute::CreateUser}>
|
<Link classes="btn btn-primary" to={AppRoute::CreateUser}>
|
||||||
<i class="bi-person-plus me-2"></i>
|
<i class="bi-person-plus me-2"></i>
|
||||||
{"Create a user"}
|
{"Create a user"}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
};
|
||||||
},
|
html! {
|
||||||
|
<div>
|
||||||
|
{ user_button.clone() }
|
||||||
|
<UserTable />
|
||||||
|
{ user_button }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
AppRoute::CreateGroup => html! {
|
AppRoute::CreateGroup => html! {
|
||||||
<CreateGroupForm/>
|
<CreateGroupForm/>
|
||||||
},
|
},
|
||||||
@@ -218,15 +220,23 @@ impl App {
|
|||||||
AppRoute::CreateGroupAttribute => html! {
|
AppRoute::CreateGroupAttribute => html! {
|
||||||
<CreateGroupAttributeForm/>
|
<CreateGroupAttributeForm/>
|
||||||
},
|
},
|
||||||
AppRoute::ListGroups => html! {
|
AppRoute::ListGroups => {
|
||||||
<div>
|
let group_button = html! {
|
||||||
<GroupTable />
|
|
||||||
<Link classes="btn btn-primary" to={AppRoute::CreateGroup}>
|
<Link classes="btn btn-primary" to={AppRoute::CreateGroup}>
|
||||||
<i class="bi-plus-circle me-2"></i>
|
<i class="bi-plus-circle me-2"></i>
|
||||||
{"Create a group"}
|
{"Create a group"}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
};
|
||||||
},
|
// Note: There's a weird bug when switching from the users page to the groups page
|
||||||
|
// where the two groups buttons are at the bottom. I don't know why.
|
||||||
|
html! {
|
||||||
|
<div>
|
||||||
|
{ group_button.clone() }
|
||||||
|
<GroupTable />
|
||||||
|
{ group_button }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
AppRoute::ListUserSchema => html! {
|
AppRoute::ListUserSchema => html! {
|
||||||
<ListUserSchema />
|
<ListUserSchema />
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::infra::functional::{use_graphql_call, LoadableResult};
|
use crate::infra::functional::{LoadableResult, use_graphql_call};
|
||||||
use graphql_client::GraphQLQuery;
|
use graphql_client::GraphQLQuery;
|
||||||
use yew::{function_component, html, virtual_dom::AttrValue, Properties};
|
use yew::{Properties, function_component, html, virtual_dom::AttrValue};
|
||||||
|
|
||||||
#[derive(GraphQLQuery)]
|
#[derive(GraphQLQuery)]
|
||||||
#[graphql(
|
#[graphql(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::components::{
|
|||||||
router::{AppRoute, Link},
|
router::{AppRoute, Link},
|
||||||
};
|
};
|
||||||
use wasm_bindgen::prelude::wasm_bindgen;
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
use yew::{function_component, html, Callback, Properties};
|
use yew::{Callback, Properties, function_component, html};
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct Props {
|
pub struct Props {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::{
|
|||||||
common_component::{CommonComponent, CommonComponentParts},
|
common_component::{CommonComponent, CommonComponentParts},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{Result, anyhow, bail};
|
||||||
use gloo_console::error;
|
use gloo_console::error;
|
||||||
use lldap_auth::*;
|
use lldap_auth::*;
|
||||||
use validator_derive::Validate;
|
use validator_derive::Validate;
|
||||||
|
|||||||
@@ -7,17 +7,16 @@ use crate::{
|
|||||||
},
|
},
|
||||||
router::AppRoute,
|
router::AppRoute,
|
||||||
},
|
},
|
||||||
convert_attribute_type,
|
|
||||||
infra::{
|
infra::{
|
||||||
common_component::{CommonComponent, CommonComponentParts},
|
common_component::{CommonComponent, CommonComponentParts},
|
||||||
form_utils::{
|
form_utils::{
|
||||||
read_all_form_attributes, AttributeValue, EmailIsRequired, GraphQlAttributeSchema,
|
AttributeValue, EmailIsRequired, GraphQlAttributeSchema, IsAdmin,
|
||||||
IsAdmin,
|
read_all_form_attributes,
|
||||||
},
|
},
|
||||||
schema::AttributeType,
|
schema::AttributeType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use anyhow::{ensure, Result};
|
use anyhow::{Result, ensure};
|
||||||
use gloo_console::log;
|
use gloo_console::log;
|
||||||
use graphql_client::GraphQLQuery;
|
use graphql_client::GraphQLQuery;
|
||||||
use validator_derive::Validate;
|
use validator_derive::Validate;
|
||||||
@@ -30,7 +29,8 @@ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
|
|||||||
schema_path = "../schema.graphql",
|
schema_path = "../schema.graphql",
|
||||||
query_path = "queries/get_group_attributes_schema.graphql",
|
query_path = "queries/get_group_attributes_schema.graphql",
|
||||||
response_derives = "Debug,Clone,PartialEq,Eq",
|
response_derives = "Debug,Clone,PartialEq,Eq",
|
||||||
custom_scalars_module = "crate::infra::graphql"
|
custom_scalars_module = "crate::infra::graphql",
|
||||||
|
extern_enums("AttributeType")
|
||||||
)]
|
)]
|
||||||
pub struct GetGroupAttributesSchema;
|
pub struct GetGroupAttributesSchema;
|
||||||
|
|
||||||
@@ -39,8 +39,6 @@ use get_group_attributes_schema::ResponseData;
|
|||||||
pub type Attribute =
|
pub type Attribute =
|
||||||
get_group_attributes_schema::GetGroupAttributesSchemaSchemaGroupSchemaAttributes;
|
get_group_attributes_schema::GetGroupAttributesSchemaSchemaGroupSchemaAttributes;
|
||||||
|
|
||||||
convert_attribute_type!(get_group_attributes_schema::AttributeType);
|
|
||||||
|
|
||||||
impl From<&Attribute> for GraphQlAttributeSchema {
|
impl From<&Attribute> for GraphQlAttributeSchema {
|
||||||
fn from(attr: &Attribute) -> Self {
|
fn from(attr: &Attribute) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -218,14 +216,14 @@ fn get_custom_attribute_input(attribute_schema: &Attribute) -> Html {
|
|||||||
html! {
|
html! {
|
||||||
<ListAttributeInput
|
<ListAttributeInput
|
||||||
name={attribute_schema.name.clone()}
|
name={attribute_schema.name.clone()}
|
||||||
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
|
attribute_type={attribute_schema.attribute_type}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
html! {
|
html! {
|
||||||
<SingleAttributeInput
|
<SingleAttributeInput
|
||||||
name={attribute_schema.name.clone()}
|
name={attribute_schema.name.clone()}
|
||||||
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
|
attribute_type={attribute_schema.attribute_type}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ use crate::{
|
|||||||
form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit},
|
form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit},
|
||||||
router::AppRoute,
|
router::AppRoute,
|
||||||
},
|
},
|
||||||
convert_attribute_type,
|
|
||||||
infra::{
|
infra::{
|
||||||
common_component::{CommonComponent, CommonComponentParts},
|
common_component::{CommonComponent, CommonComponentParts},
|
||||||
schema::{validate_attribute_type, AttributeType},
|
schema::{AttributeType, validate_attribute_type},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{Result, bail};
|
||||||
use gloo_console::log;
|
use gloo_console::log;
|
||||||
use graphql_client::GraphQLQuery;
|
use graphql_client::GraphQLQuery;
|
||||||
|
use lldap_validation::attributes::validate_attribute_name;
|
||||||
use validator_derive::Validate;
|
use validator_derive::Validate;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_form_derive::Model;
|
use yew_form_derive::Model;
|
||||||
@@ -22,12 +22,11 @@ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
|
|||||||
schema_path = "../schema.graphql",
|
schema_path = "../schema.graphql",
|
||||||
query_path = "queries/create_group_attribute.graphql",
|
query_path = "queries/create_group_attribute.graphql",
|
||||||
response_derives = "Debug",
|
response_derives = "Debug",
|
||||||
custom_scalars_module = "crate::infra::graphql"
|
custom_scalars_module = "crate::infra::graphql",
|
||||||
|
extern_enums("AttributeType")
|
||||||
)]
|
)]
|
||||||
pub struct CreateGroupAttribute;
|
pub struct CreateGroupAttribute;
|
||||||
|
|
||||||
convert_attribute_type!(create_group_attribute::AttributeType);
|
|
||||||
|
|
||||||
pub struct CreateGroupAttributeForm {
|
pub struct CreateGroupAttributeForm {
|
||||||
common: CommonComponentParts<Self>,
|
common: CommonComponentParts<Self>,
|
||||||
form: yew_form::Form<CreateGroupAttributeModel>,
|
form: yew_form::Form<CreateGroupAttributeModel>,
|
||||||
@@ -62,10 +61,18 @@ impl CommonComponent<CreateGroupAttributeForm> for CreateGroupAttributeForm {
|
|||||||
bail!("Check the form for errors");
|
bail!("Check the form for errors");
|
||||||
}
|
}
|
||||||
let model = self.form.model();
|
let model = self.form.model();
|
||||||
let attribute_type = model.attribute_type.parse::<AttributeType>().unwrap();
|
validate_attribute_name(&model.attribute_name).or_else(|invalid_chars| {
|
||||||
|
let invalid = String::from_iter(invalid_chars);
|
||||||
|
bail!(
|
||||||
|
"Attribute name contains one or more invalid characters: {}",
|
||||||
|
invalid
|
||||||
|
);
|
||||||
|
})?;
|
||||||
|
let attribute_type =
|
||||||
|
AttributeType::try_from(model.attribute_type.as_str()).unwrap();
|
||||||
let req = create_group_attribute::Variables {
|
let req = create_group_attribute::Variables {
|
||||||
name: model.attribute_name,
|
name: model.attribute_name,
|
||||||
attribute_type: create_group_attribute::AttributeType::from(attribute_type),
|
attribute_type,
|
||||||
is_list: model.is_list,
|
is_list: model.is_list,
|
||||||
is_visible: model.is_visible,
|
is_visible: model.is_visible,
|
||||||
};
|
};
|
||||||
@@ -137,7 +144,7 @@ impl Component for CreateGroupAttributeForm {
|
|||||||
oninput={link.callback(|_| Msg::Update)}>
|
oninput={link.callback(|_| Msg::Update)}>
|
||||||
<option selected=true value="String">{"String"}</option>
|
<option selected=true value="String">{"String"}</option>
|
||||||
<option value="Integer">{"Integer"}</option>
|
<option value="Integer">{"Integer"}</option>
|
||||||
<option value="Jpeg">{"Jpeg"}</option>
|
<option value="JpegPhoto">{"Jpeg"}</option>
|
||||||
<option value="DateTime">{"DateTime"}</option>
|
<option value="DateTime">{"DateTime"}</option>
|
||||||
</Select<CreateGroupAttributeModel>>
|
</Select<CreateGroupAttributeModel>>
|
||||||
<CheckBox<CreateGroupAttributeModel>
|
<CheckBox<CreateGroupAttributeModel>
|
||||||
|
|||||||
@@ -7,18 +7,17 @@ use crate::{
|
|||||||
},
|
},
|
||||||
router::AppRoute,
|
router::AppRoute,
|
||||||
},
|
},
|
||||||
convert_attribute_type,
|
|
||||||
infra::{
|
infra::{
|
||||||
api::HostService,
|
api::HostService,
|
||||||
common_component::{CommonComponent, CommonComponentParts},
|
common_component::{CommonComponent, CommonComponentParts},
|
||||||
form_utils::{
|
form_utils::{
|
||||||
read_all_form_attributes, AttributeValue, EmailIsRequired, GraphQlAttributeSchema,
|
AttributeValue, EmailIsRequired, GraphQlAttributeSchema, IsAdmin,
|
||||||
IsAdmin,
|
read_all_form_attributes,
|
||||||
},
|
},
|
||||||
schema::AttributeType,
|
schema::AttributeType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use anyhow::{ensure, Result};
|
use anyhow::{Result, ensure};
|
||||||
use gloo_console::log;
|
use gloo_console::log;
|
||||||
use graphql_client::GraphQLQuery;
|
use graphql_client::GraphQLQuery;
|
||||||
use lldap_auth::{opaque, registration};
|
use lldap_auth::{opaque, registration};
|
||||||
@@ -32,7 +31,8 @@ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
|
|||||||
schema_path = "../schema.graphql",
|
schema_path = "../schema.graphql",
|
||||||
query_path = "queries/get_user_attributes_schema.graphql",
|
query_path = "queries/get_user_attributes_schema.graphql",
|
||||||
response_derives = "Debug,Clone,PartialEq,Eq",
|
response_derives = "Debug,Clone,PartialEq,Eq",
|
||||||
custom_scalars_module = "crate::infra::graphql"
|
custom_scalars_module = "crate::infra::graphql",
|
||||||
|
extern_enums("AttributeType")
|
||||||
)]
|
)]
|
||||||
pub struct GetUserAttributesSchema;
|
pub struct GetUserAttributesSchema;
|
||||||
|
|
||||||
@@ -40,8 +40,6 @@ use get_user_attributes_schema::ResponseData;
|
|||||||
|
|
||||||
pub type Attribute = get_user_attributes_schema::GetUserAttributesSchemaSchemaUserSchemaAttributes;
|
pub type Attribute = get_user_attributes_schema::GetUserAttributesSchemaSchemaUserSchemaAttributes;
|
||||||
|
|
||||||
convert_attribute_type!(get_user_attributes_schema::AttributeType);
|
|
||||||
|
|
||||||
impl From<&Attribute> for GraphQlAttributeSchema {
|
impl From<&Attribute> for GraphQlAttributeSchema {
|
||||||
fn from(attr: &Attribute) -> Self {
|
fn from(attr: &Attribute) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -310,14 +308,14 @@ fn get_custom_attribute_input(attribute_schema: &Attribute) -> Html {
|
|||||||
html! {
|
html! {
|
||||||
<ListAttributeInput
|
<ListAttributeInput
|
||||||
name={attribute_schema.name.clone()}
|
name={attribute_schema.name.clone()}
|
||||||
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
|
attribute_type={attribute_schema.attribute_type}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
html! {
|
html! {
|
||||||
<SingleAttributeInput
|
<SingleAttributeInput
|
||||||
name={attribute_schema.name.clone()}
|
name={attribute_schema.name.clone()}
|
||||||
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
|
attribute_type={attribute_schema.attribute_type}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ use crate::{
|
|||||||
form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit},
|
form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit},
|
||||||
router::AppRoute,
|
router::AppRoute,
|
||||||
},
|
},
|
||||||
convert_attribute_type,
|
|
||||||
infra::{
|
infra::{
|
||||||
common_component::{CommonComponent, CommonComponentParts},
|
common_component::{CommonComponent, CommonComponentParts},
|
||||||
schema::{validate_attribute_type, AttributeType},
|
schema::{AttributeType, validate_attribute_type},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{Result, bail};
|
||||||
use gloo_console::log;
|
use gloo_console::log;
|
||||||
use graphql_client::GraphQLQuery;
|
use graphql_client::GraphQLQuery;
|
||||||
|
use lldap_validation::attributes::validate_attribute_name;
|
||||||
use validator_derive::Validate;
|
use validator_derive::Validate;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_form_derive::Model;
|
use yew_form_derive::Model;
|
||||||
@@ -22,12 +22,11 @@ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
|
|||||||
schema_path = "../schema.graphql",
|
schema_path = "../schema.graphql",
|
||||||
query_path = "queries/create_user_attribute.graphql",
|
query_path = "queries/create_user_attribute.graphql",
|
||||||
response_derives = "Debug",
|
response_derives = "Debug",
|
||||||
custom_scalars_module = "crate::infra::graphql"
|
custom_scalars_module = "crate::infra::graphql",
|
||||||
|
extern_enums("AttributeType")
|
||||||
)]
|
)]
|
||||||
pub struct CreateUserAttribute;
|
pub struct CreateUserAttribute;
|
||||||
|
|
||||||
convert_attribute_type!(create_user_attribute::AttributeType);
|
|
||||||
|
|
||||||
pub struct CreateUserAttributeForm {
|
pub struct CreateUserAttributeForm {
|
||||||
common: CommonComponentParts<Self>,
|
common: CommonComponentParts<Self>,
|
||||||
form: yew_form::Form<CreateUserAttributeModel>,
|
form: yew_form::Form<CreateUserAttributeModel>,
|
||||||
@@ -66,10 +65,18 @@ impl CommonComponent<CreateUserAttributeForm> for CreateUserAttributeForm {
|
|||||||
if model.is_editable && !model.is_visible {
|
if model.is_editable && !model.is_visible {
|
||||||
bail!("Editable attributes must also be visible");
|
bail!("Editable attributes must also be visible");
|
||||||
}
|
}
|
||||||
let attribute_type = model.attribute_type.parse::<AttributeType>().unwrap();
|
validate_attribute_name(&model.attribute_name).or_else(|invalid_chars| {
|
||||||
|
let invalid = String::from_iter(invalid_chars);
|
||||||
|
bail!(
|
||||||
|
"Attribute name contains one or more invalid characters: {}",
|
||||||
|
invalid
|
||||||
|
);
|
||||||
|
})?;
|
||||||
|
let attribute_type =
|
||||||
|
AttributeType::try_from(model.attribute_type.as_str()).unwrap();
|
||||||
let req = create_user_attribute::Variables {
|
let req = create_user_attribute::Variables {
|
||||||
name: model.attribute_name,
|
name: model.attribute_name,
|
||||||
attribute_type: create_user_attribute::AttributeType::from(attribute_type),
|
attribute_type,
|
||||||
is_editable: model.is_editable,
|
is_editable: model.is_editable,
|
||||||
is_list: model.is_list,
|
is_list: model.is_list,
|
||||||
is_visible: model.is_visible,
|
is_visible: model.is_visible,
|
||||||
@@ -139,7 +146,7 @@ impl Component for CreateUserAttributeForm {
|
|||||||
oninput={link.callback(|_| Msg::Update)}>
|
oninput={link.callback(|_| Msg::Update)}>
|
||||||
<option selected=true value="String">{"String"}</option>
|
<option selected=true value="String">{"String"}</option>
|
||||||
<option value="Integer">{"Integer"}</option>
|
<option value="Integer">{"Integer"}</option>
|
||||||
<option value="Jpeg">{"Jpeg"}</option>
|
<option value="JpegPhoto">{"Jpeg"}</option>
|
||||||
<option value="DateTime">{"DateTime"}</option>
|
<option value="DateTime">{"DateTime"}</option>
|
||||||
</Select<CreateUserAttributeModel>>
|
</Select<CreateUserAttributeModel>>
|
||||||
<CheckBox<CreateUserAttributeModel>
|
<CheckBox<CreateUserAttributeModel>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use web_sys::Element;
|
use web_sys::Element;
|
||||||
use yew::{
|
use yew::{
|
||||||
function_component, html, use_effect_with_deps, use_node_ref, virtual_dom::AttrValue,
|
Component, Context, Html, Properties, function_component, html, use_effect_with_deps,
|
||||||
Component, Context, Html, Properties,
|
use_node_ref, virtual_dom::AttrValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
@@ -24,12 +24,12 @@ fn attribute_input(props: &AttributeInputProps) -> Html {
|
|||||||
AttributeType::DateTime => {
|
AttributeType::DateTime => {
|
||||||
return html! {
|
return html! {
|
||||||
<DateTimeInput name={props.name.clone()} value={props.value.clone()} />
|
<DateTimeInput name={props.name.clone()} value={props.value.clone()} />
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
AttributeType::Jpeg => {
|
AttributeType::JpegPhoto => {
|
||||||
return html! {
|
return html! {
|
||||||
<JpegFileInput name={props.name.clone()} value={props.value.clone()} />
|
<JpegFileInput name={props.name.clone()} value={props.value.clone()} />
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ fn attribute_label(props: &AttributeLabelProps) -> Html {
|
|||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct SingleAttributeInputProps {
|
pub struct SingleAttributeInputProps {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub attribute_type: AttributeType,
|
pub(crate) attribute_type: AttributeType,
|
||||||
#[prop_or(None)]
|
#[prop_or(None)]
|
||||||
pub value: Option<String>,
|
pub value: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ pub fn single_attribute_input(props: &SingleAttributeInputProps) -> Html {
|
|||||||
<AttributeLabel name={props.name.clone()} />
|
<AttributeLabel name={props.name.clone()} />
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<AttributeInput
|
<AttributeInput
|
||||||
attribute_type={props.attribute_type.clone()}
|
attribute_type={props.attribute_type}
|
||||||
name={props.name.clone()}
|
name={props.name.clone()}
|
||||||
value={props.value.clone()} />
|
value={props.value.clone()} />
|
||||||
</div>
|
</div>
|
||||||
@@ -105,7 +105,7 @@ pub fn single_attribute_input(props: &SingleAttributeInputProps) -> Html {
|
|||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct ListAttributeInputProps {
|
pub struct ListAttributeInputProps {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub attribute_type: AttributeType,
|
pub(crate) attribute_type: AttributeType,
|
||||||
#[prop_or(vec!())]
|
#[prop_or(vec!())]
|
||||||
pub values: Vec<String>,
|
pub values: Vec<String>,
|
||||||
}
|
}
|
||||||
@@ -165,7 +165,7 @@ impl Component for ListAttributeInput {
|
|||||||
{self.indices.iter().map(|&i| html! {
|
{self.indices.iter().map(|&i| html! {
|
||||||
<div class="input-group mb-2" key={i}>
|
<div class="input-group mb-2" key={i}>
|
||||||
<AttributeInput
|
<AttributeInput
|
||||||
attribute_type={props.attribute_type.clone()}
|
attribute_type={props.attribute_type}
|
||||||
name={props.name.clone()}
|
name={props.name.clone()}
|
||||||
value={props.values.get(i).cloned().unwrap_or_default()} />
|
value={props.values.get(i).cloned().unwrap_or_default()} />
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use yew::{function_component, html, virtual_dom::AttrValue, Callback, Properties};
|
use yew::{Callback, Properties, function_component, html, virtual_dom::AttrValue};
|
||||||
use yew_form::{Form, Model};
|
use yew_form::{Form, Model};
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::str::FromStr;
|
|||||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::HtmlInputElement;
|
use web_sys::HtmlInputElement;
|
||||||
use yew::{function_component, html, use_state, virtual_dom::AttrValue, Event, Properties};
|
use yew::{Event, Properties, function_component, html, use_state, virtual_dom::AttrValue};
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct DateTimeInputProps {
|
pub struct DateTimeInputProps {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use yew::{function_component, html, virtual_dom::AttrValue, Callback, InputEvent, Properties};
|
use yew::{Callback, InputEvent, Properties, function_component, html, virtual_dom::AttrValue};
|
||||||
use yew_form::{Form, Model};
|
use yew_form::{Form, Model};
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use std::{fmt::Display, str::FromStr};
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
use anyhow::{bail, Error, Ok, Result};
|
use anyhow::{Error, Ok, Result, bail};
|
||||||
use gloo_file::{
|
use gloo_file::{
|
||||||
callbacks::{read_as_bytes, FileReader},
|
|
||||||
File,
|
File,
|
||||||
|
callbacks::{FileReader, read_as_bytes},
|
||||||
};
|
};
|
||||||
use web_sys::{FileList, HtmlInputElement, InputEvent};
|
use web_sys::{FileList, HtmlInputElement, InputEvent};
|
||||||
use yew::Properties;
|
use yew::Properties;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use yew::{
|
use yew::{
|
||||||
function_component, html, virtual_dom::AttrValue, Callback, Children, InputEvent, Properties,
|
Callback, Children, InputEvent, Properties, function_component, html, virtual_dom::AttrValue,
|
||||||
};
|
};
|
||||||
use yew_form::{Form, Model};
|
use yew_form::{Form, Model};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use yew::{function_component, html, virtual_dom::AttrValue, Children, Properties};
|
use yew::{Children, Properties, function_component, html, virtual_dom::AttrValue};
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct Props {
|
pub struct Props {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use web_sys::MouseEvent;
|
use web_sys::MouseEvent;
|
||||||
use yew::{function_component, html, virtual_dom::AttrValue, Callback, Children, Properties};
|
use yew::{Callback, Children, Properties, function_component, html, virtual_dom::AttrValue};
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct Props {
|
pub struct Props {
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
use crate::infra::attributes::AttributeDescription;
|
||||||
|
use lldap_validation::attributes::{ALLOWED_CHARACTERS_DESCRIPTION, validate_attribute_name};
|
||||||
|
use yew::{Html, html};
|
||||||
|
|
||||||
|
fn render_attribute_aliases(attribute_description: &AttributeDescription) -> Html {
|
||||||
|
if attribute_description.aliases.is_empty() {
|
||||||
|
html! {}
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<br/>
|
||||||
|
<small class="text-muted">
|
||||||
|
{"Aliases: "}
|
||||||
|
{attribute_description.aliases.join(", ")}
|
||||||
|
</small>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_attribute_validation_warnings(attribute_name: &str) -> Html {
|
||||||
|
match validate_attribute_name(attribute_name) {
|
||||||
|
Ok(()) => {
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
Err(_invalid_chars) => {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<br/>
|
||||||
|
<small class="text-warning">
|
||||||
|
{"Warning: This attribute uses one or more invalid characters "}
|
||||||
|
{"("}{ALLOWED_CHARACTERS_DESCRIPTION}{"). "}
|
||||||
|
{"Some clients may not support it."}
|
||||||
|
</small>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_attribute_name(
|
||||||
|
hardcoded: bool,
|
||||||
|
attribute_description: &AttributeDescription,
|
||||||
|
) -> Html {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
{&attribute_description.attribute_name}
|
||||||
|
{if hardcoded {render_attribute_aliases(attribute_description)} else {html!{}}}
|
||||||
|
{render_attribute_validation_warnings(attribute_description.attribute_name)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod attribute_schema;
|
||||||
@@ -5,13 +5,13 @@ use crate::{
|
|||||||
remove_user_from_group::RemoveUserFromGroupComponent,
|
remove_user_from_group::RemoveUserFromGroupComponent,
|
||||||
router::{AppRoute, Link},
|
router::{AppRoute, Link},
|
||||||
},
|
},
|
||||||
convert_attribute_type,
|
|
||||||
infra::{
|
infra::{
|
||||||
common_component::{CommonComponent, CommonComponentParts},
|
common_component::{CommonComponent, CommonComponentParts},
|
||||||
form_utils::GraphQlAttributeSchema,
|
form_utils::GraphQlAttributeSchema,
|
||||||
|
schema::AttributeType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Error, Result};
|
use anyhow::{Error, Result, bail};
|
||||||
use graphql_client::GraphQLQuery;
|
use graphql_client::GraphQLQuery;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
@@ -20,7 +20,8 @@ use yew::prelude::*;
|
|||||||
schema_path = "../schema.graphql",
|
schema_path = "../schema.graphql",
|
||||||
query_path = "queries/get_group_details.graphql",
|
query_path = "queries/get_group_details.graphql",
|
||||||
response_derives = "Debug, Hash, PartialEq, Eq, Clone",
|
response_derives = "Debug, Hash, PartialEq, Eq, Clone",
|
||||||
custom_scalars_module = "crate::infra::graphql"
|
custom_scalars_module = "crate::infra::graphql",
|
||||||
|
extern_enums("AttributeType")
|
||||||
)]
|
)]
|
||||||
pub struct GetGroupDetails;
|
pub struct GetGroupDetails;
|
||||||
|
|
||||||
@@ -29,9 +30,6 @@ pub type User = get_group_details::GetGroupDetailsGroupUsers;
|
|||||||
pub type AddGroupMemberUser = add_group_member::User;
|
pub type AddGroupMemberUser = add_group_member::User;
|
||||||
pub type Attribute = get_group_details::GetGroupDetailsGroupAttributes;
|
pub type Attribute = get_group_details::GetGroupDetailsGroupAttributes;
|
||||||
pub type AttributeSchema = get_group_details::GetGroupDetailsSchemaGroupSchemaAttributes;
|
pub type AttributeSchema = get_group_details::GetGroupDetailsSchemaGroupSchemaAttributes;
|
||||||
pub type AttributeType = get_group_details::AttributeType;
|
|
||||||
|
|
||||||
convert_attribute_type!(AttributeType);
|
|
||||||
|
|
||||||
impl From<&AttributeSchema> for GraphQlAttributeSchema {
|
impl From<&AttributeSchema> for GraphQlAttributeSchema {
|
||||||
fn from(attr: &AttributeSchema) -> Self {
|
fn from(attr: &AttributeSchema) -> Self {
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
infra::{
|
infra::{
|
||||||
common_component::{CommonComponent, CommonComponentParts},
|
common_component::{CommonComponent, CommonComponentParts},
|
||||||
form_utils::{read_all_form_attributes, AttributeValue, EmailIsRequired, IsAdmin},
|
form_utils::{AttributeValue, EmailIsRequired, IsAdmin, read_all_form_attributes},
|
||||||
schema::AttributeType,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use anyhow::{Ok, Result};
|
use anyhow::{Ok, Result};
|
||||||
@@ -174,7 +173,7 @@ fn get_custom_attribute_input(
|
|||||||
html! {
|
html! {
|
||||||
<ListAttributeInput
|
<ListAttributeInput
|
||||||
name={attribute_schema.name.clone()}
|
name={attribute_schema.name.clone()}
|
||||||
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
|
attribute_type={attribute_schema.attribute_type}
|
||||||
values={values}
|
values={values}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -182,7 +181,7 @@ fn get_custom_attribute_input(
|
|||||||
html! {
|
html! {
|
||||||
<SingleAttributeInput
|
<SingleAttributeInput
|
||||||
name={attribute_schema.name.clone()}
|
name={attribute_schema.name.clone()}
|
||||||
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
|
attribute_type={attribute_schema.attribute_type}
|
||||||
value={values.first().cloned().unwrap_or_default()}
|
value={values.first().cloned().unwrap_or_default()}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
components::{
|
components::{
|
||||||
delete_group_attribute::DeleteGroupAttribute,
|
delete_group_attribute::DeleteGroupAttribute,
|
||||||
|
fragments::attribute_schema::render_attribute_name,
|
||||||
router::{AppRoute, Link},
|
router::{AppRoute, Link},
|
||||||
},
|
},
|
||||||
convert_attribute_type,
|
|
||||||
infra::{
|
infra::{
|
||||||
|
attributes::group,
|
||||||
common_component::{CommonComponent, CommonComponentParts},
|
common_component::{CommonComponent, CommonComponentParts},
|
||||||
schema::AttributeType,
|
schema::AttributeType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Error, Result};
|
use anyhow::{Error, Result, anyhow};
|
||||||
use gloo_console::log;
|
use gloo_console::log;
|
||||||
use graphql_client::GraphQLQuery;
|
use graphql_client::GraphQLQuery;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
@@ -19,7 +20,8 @@ use yew::prelude::*;
|
|||||||
schema_path = "../schema.graphql",
|
schema_path = "../schema.graphql",
|
||||||
query_path = "queries/get_group_attributes_schema.graphql",
|
query_path = "queries/get_group_attributes_schema.graphql",
|
||||||
response_derives = "Debug,Clone,PartialEq,Eq",
|
response_derives = "Debug,Clone,PartialEq,Eq",
|
||||||
custom_scalars_module = "crate::infra::graphql"
|
custom_scalars_module = "crate::infra::graphql",
|
||||||
|
extern_enums("AttributeType")
|
||||||
)]
|
)]
|
||||||
pub struct GetGroupAttributesSchema;
|
pub struct GetGroupAttributesSchema;
|
||||||
|
|
||||||
@@ -28,8 +30,6 @@ use get_group_attributes_schema::ResponseData;
|
|||||||
pub type Attribute =
|
pub type Attribute =
|
||||||
get_group_attributes_schema::GetGroupAttributesSchemaSchemaGroupSchemaAttributes;
|
get_group_attributes_schema::GetGroupAttributesSchemaSchemaGroupSchemaAttributes;
|
||||||
|
|
||||||
convert_attribute_type!(get_group_attributes_schema::AttributeType);
|
|
||||||
|
|
||||||
#[derive(yew::Properties, Clone, PartialEq, Eq)]
|
#[derive(yew::Properties, Clone, PartialEq, Eq)]
|
||||||
pub struct Props {
|
pub struct Props {
|
||||||
pub hardcoded: bool,
|
pub hardcoded: bool,
|
||||||
@@ -55,21 +55,21 @@ impl CommonComponent<GroupSchemaTable> for GroupSchemaTable {
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
Msg::OnError(e) => Err(e),
|
Msg::OnError(e) => Err(e),
|
||||||
Msg::OnAttributeDeleted(attribute_name) => {
|
Msg::OnAttributeDeleted(attribute_name) => match self.attributes {
|
||||||
match self.attributes {
|
None => {
|
||||||
None => {
|
log!(format!(
|
||||||
log!(format!("Attribute {attribute_name} was deleted but component has no attributes"));
|
"Attribute {attribute_name} was deleted but component has no attributes"
|
||||||
Err(anyhow!("invalid state"))
|
));
|
||||||
}
|
Err(anyhow!("invalid state"))
|
||||||
Some(_) => {
|
|
||||||
self.attributes
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.retain(|a| a.name != attribute_name);
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
Some(_) => {
|
||||||
|
self.attributes
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.retain(|a| a.name != attribute_name);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,16 +145,17 @@ impl GroupSchemaTable {
|
|||||||
|
|
||||||
fn view_attribute(&self, ctx: &Context<Self>, attribute: &Attribute) -> Html {
|
fn view_attribute(&self, ctx: &Context<Self>, attribute: &Attribute) -> Html {
|
||||||
let link = ctx.link();
|
let link = ctx.link();
|
||||||
let attribute_type = AttributeType::from(attribute.attribute_type.clone());
|
let attribute_type = attribute.attribute_type;
|
||||||
let checkmark = html! {
|
let checkmark = html! {
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
|
||||||
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z"></path>
|
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
};
|
};
|
||||||
let hardcoded = ctx.props().hardcoded;
|
let hardcoded = ctx.props().hardcoded;
|
||||||
|
let desc = group::resolve_group_attribute_description_or_default(&attribute.name);
|
||||||
html! {
|
html! {
|
||||||
<tr key={attribute.name.clone()}>
|
<tr key={attribute.name.clone()}>
|
||||||
<td>{&attribute.name}</td>
|
<td>{render_attribute_name(hardcoded, &desc)}</td>
|
||||||
<td>{if attribute.is_list { format!("List<{attribute_type}>")} else {attribute_type.to_string()}}</td>
|
<td>{if attribute.is_list { format!("List<{attribute_type}>")} else {attribute_type.to_string()}}</td>
|
||||||
<td>{if attribute.is_visible {checkmark.clone()} else {html!{}}}</td>
|
<td>{if attribute.is_visible {checkmark.clone()} else {html!{}}}</td>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::{
|
|||||||
common_component::{CommonComponent, CommonComponentParts},
|
common_component::{CommonComponent, CommonComponentParts},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{Result, anyhow, bail};
|
||||||
use gloo_console::error;
|
use gloo_console::error;
|
||||||
use lldap_auth::*;
|
use lldap_auth::*;
|
||||||
use validator_derive::Validate;
|
use validator_derive::Validate;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ pub mod delete_group_attribute;
|
|||||||
pub mod delete_user;
|
pub mod delete_user;
|
||||||
pub mod delete_user_attribute;
|
pub mod delete_user_attribute;
|
||||||
pub mod form;
|
pub mod form;
|
||||||
|
pub mod fragments;
|
||||||
pub mod group_details;
|
pub mod group_details;
|
||||||
pub mod group_details_form;
|
pub mod group_details_form;
|
||||||
pub mod group_schema_table;
|
pub mod group_schema_table;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::{
|
|||||||
common_component::{CommonComponent, CommonComponentParts},
|
common_component::{CommonComponent, CommonComponentParts},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{Result, bail};
|
||||||
use validator_derive::Validate;
|
use validator_derive::Validate;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_form::Form;
|
use yew_form::Form;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::{
|
|||||||
common_component::{CommonComponent, CommonComponentParts},
|
common_component::{CommonComponent, CommonComponentParts},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{Result, bail};
|
||||||
use lldap_auth::{
|
use lldap_auth::{
|
||||||
opaque::client::registration as opaque_registration,
|
opaque::client::registration as opaque_registration,
|
||||||
password_reset::ServerPasswordResetResponse, registration,
|
password_reset::ServerPasswordResetResponse, registration,
|
||||||
@@ -148,7 +148,7 @@ impl Component for ResetPasswordStep2Form {
|
|||||||
(None, None) => {
|
(None, None) => {
|
||||||
return html! {
|
return html! {
|
||||||
{"Validating token"}
|
{"Validating token"}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
(None, Some(e)) => {
|
(None, Some(e)) => {
|
||||||
return html! {
|
return html! {
|
||||||
@@ -163,7 +163,7 @@ impl Component for ResetPasswordStep2Form {
|
|||||||
{"Back"}
|
{"Back"}
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ use crate::{
|
|||||||
router::{AppRoute, Link},
|
router::{AppRoute, Link},
|
||||||
user_details_form::UserDetailsForm,
|
user_details_form::UserDetailsForm,
|
||||||
},
|
},
|
||||||
convert_attribute_type,
|
|
||||||
infra::{
|
infra::{
|
||||||
common_component::{CommonComponent, CommonComponentParts},
|
common_component::{CommonComponent, CommonComponentParts},
|
||||||
form_utils::GraphQlAttributeSchema,
|
form_utils::GraphQlAttributeSchema,
|
||||||
|
schema::AttributeType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Error, Result};
|
use anyhow::{Error, Result, bail};
|
||||||
use graphql_client::GraphQLQuery;
|
use graphql_client::GraphQLQuery;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
@@ -20,7 +20,8 @@ use yew::prelude::*;
|
|||||||
schema_path = "../schema.graphql",
|
schema_path = "../schema.graphql",
|
||||||
query_path = "queries/get_user_details.graphql",
|
query_path = "queries/get_user_details.graphql",
|
||||||
response_derives = "Debug, Hash, PartialEq, Eq, Clone",
|
response_derives = "Debug, Hash, PartialEq, Eq, Clone",
|
||||||
custom_scalars_module = "crate::infra::graphql"
|
custom_scalars_module = "crate::infra::graphql",
|
||||||
|
extern_enums("AttributeType")
|
||||||
)]
|
)]
|
||||||
pub struct GetUserDetails;
|
pub struct GetUserDetails;
|
||||||
|
|
||||||
@@ -28,9 +29,6 @@ pub type User = get_user_details::GetUserDetailsUser;
|
|||||||
pub type Group = get_user_details::GetUserDetailsUserGroups;
|
pub type Group = get_user_details::GetUserDetailsUserGroups;
|
||||||
pub type Attribute = get_user_details::GetUserDetailsUserAttributes;
|
pub type Attribute = get_user_details::GetUserDetailsUserAttributes;
|
||||||
pub type AttributeSchema = get_user_details::GetUserDetailsSchemaUserSchemaAttributes;
|
pub type AttributeSchema = get_user_details::GetUserDetailsSchemaUserSchemaAttributes;
|
||||||
pub type AttributeType = get_user_details::AttributeType;
|
|
||||||
|
|
||||||
convert_attribute_type!(AttributeType);
|
|
||||||
|
|
||||||
impl From<&AttributeSchema> for GraphQlAttributeSchema {
|
impl From<&AttributeSchema> for GraphQlAttributeSchema {
|
||||||
fn from(attr: &AttributeSchema) -> Self {
|
fn from(attr: &AttributeSchema) -> Self {
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ use crate::{
|
|||||||
},
|
},
|
||||||
infra::{
|
infra::{
|
||||||
common_component::{CommonComponent, CommonComponentParts},
|
common_component::{CommonComponent, CommonComponentParts},
|
||||||
form_utils::{read_all_form_attributes, AttributeValue, EmailIsRequired, IsAdmin},
|
form_utils::{AttributeValue, EmailIsRequired, IsAdmin, read_all_form_attributes},
|
||||||
schema::AttributeType,
|
schema::AttributeType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use anyhow::{Ok, Result};
|
use anyhow::{Ok, Result};
|
||||||
|
use gloo_console::console;
|
||||||
use graphql_client::GraphQLQuery;
|
use graphql_client::GraphQLQuery;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
@@ -168,7 +169,7 @@ fn get_custom_attribute_input(
|
|||||||
html! {
|
html! {
|
||||||
<ListAttributeInput
|
<ListAttributeInput
|
||||||
name={attribute_schema.name.clone()}
|
name={attribute_schema.name.clone()}
|
||||||
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
|
attribute_type={attribute_schema.attribute_type}
|
||||||
values={values}
|
values={values}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -176,7 +177,7 @@ fn get_custom_attribute_input(
|
|||||||
html! {
|
html! {
|
||||||
<SingleAttributeInput
|
<SingleAttributeInput
|
||||||
name={attribute_schema.name.clone()}
|
name={attribute_schema.name.clone()}
|
||||||
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
|
attribute_type={attribute_schema.attribute_type}
|
||||||
value={values.first().cloned().unwrap_or_default()}
|
value={values.first().cloned().unwrap_or_default()}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -192,9 +193,19 @@ fn get_custom_attribute_static(
|
|||||||
.find(|a| a.name == attribute_schema.name)
|
.find(|a| a.name == attribute_schema.name)
|
||||||
.map(|attribute| attribute.value.clone())
|
.map(|attribute| attribute.value.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
let value_to_str = match attribute_schema.attribute_type {
|
||||||
|
AttributeType::String | AttributeType::Integer => |v: String| v,
|
||||||
|
AttributeType::DateTime => |v: String| {
|
||||||
|
console!(format!("Parsing date: {}", &v));
|
||||||
|
chrono::DateTime::parse_from_rfc3339(&v)
|
||||||
|
.map(|dt| dt.naive_utc().to_string())
|
||||||
|
.unwrap_or_else(|_| "Invalid date".to_string())
|
||||||
|
},
|
||||||
|
AttributeType::JpegPhoto => |_: String| "Unimplemented JPEG display".to_string(),
|
||||||
|
};
|
||||||
html! {
|
html! {
|
||||||
<StaticValue label={attribute_schema.name.clone()} id={attribute_schema.name.clone()}>
|
<StaticValue label={attribute_schema.name.clone()} id={attribute_schema.name.clone()}>
|
||||||
{values.into_iter().map(|x| html!{<div>{x}</div>}).collect::<Vec<_>>()}
|
{values.into_iter().map(|x| html!{<div>{value_to_str(x)}</div>}).collect::<Vec<_>>()}
|
||||||
</StaticValue>
|
</StaticValue>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
components::{
|
components::{
|
||||||
delete_user_attribute::DeleteUserAttribute,
|
delete_user_attribute::DeleteUserAttribute,
|
||||||
|
fragments::attribute_schema::render_attribute_name,
|
||||||
router::{AppRoute, Link},
|
router::{AppRoute, Link},
|
||||||
},
|
},
|
||||||
convert_attribute_type,
|
|
||||||
infra::{
|
infra::{
|
||||||
|
attributes::user,
|
||||||
common_component::{CommonComponent, CommonComponentParts},
|
common_component::{CommonComponent, CommonComponentParts},
|
||||||
schema::AttributeType,
|
schema::AttributeType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Error, Result};
|
use anyhow::{Error, Result, anyhow};
|
||||||
use gloo_console::log;
|
use gloo_console::log;
|
||||||
use graphql_client::GraphQLQuery;
|
use graphql_client::GraphQLQuery;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
@@ -19,7 +20,8 @@ use yew::prelude::*;
|
|||||||
schema_path = "../schema.graphql",
|
schema_path = "../schema.graphql",
|
||||||
query_path = "queries/get_user_attributes_schema.graphql",
|
query_path = "queries/get_user_attributes_schema.graphql",
|
||||||
response_derives = "Debug,Clone,PartialEq,Eq",
|
response_derives = "Debug,Clone,PartialEq,Eq",
|
||||||
custom_scalars_module = "crate::infra::graphql"
|
custom_scalars_module = "crate::infra::graphql",
|
||||||
|
extern_enums("AttributeType")
|
||||||
)]
|
)]
|
||||||
pub struct GetUserAttributesSchema;
|
pub struct GetUserAttributesSchema;
|
||||||
|
|
||||||
@@ -27,8 +29,6 @@ use get_user_attributes_schema::ResponseData;
|
|||||||
|
|
||||||
pub type Attribute = get_user_attributes_schema::GetUserAttributesSchemaSchemaUserSchemaAttributes;
|
pub type Attribute = get_user_attributes_schema::GetUserAttributesSchemaSchemaUserSchemaAttributes;
|
||||||
|
|
||||||
convert_attribute_type!(get_user_attributes_schema::AttributeType);
|
|
||||||
|
|
||||||
#[derive(yew::Properties, Clone, PartialEq, Eq)]
|
#[derive(yew::Properties, Clone, PartialEq, Eq)]
|
||||||
pub struct Props {
|
pub struct Props {
|
||||||
pub hardcoded: bool,
|
pub hardcoded: bool,
|
||||||
@@ -53,21 +53,21 @@ impl CommonComponent<UserSchemaTable> for UserSchemaTable {
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
Msg::OnError(e) => Err(e),
|
Msg::OnError(e) => Err(e),
|
||||||
Msg::OnAttributeDeleted(attribute_name) => {
|
Msg::OnAttributeDeleted(attribute_name) => match self.attributes {
|
||||||
match self.attributes {
|
None => {
|
||||||
None => {
|
log!(format!(
|
||||||
log!(format!("Attribute {attribute_name} was deleted but component has no attributes"));
|
"Attribute {attribute_name} was deleted but component has no attributes"
|
||||||
Err(anyhow!("invalid state"))
|
));
|
||||||
}
|
Err(anyhow!("invalid state"))
|
||||||
Some(_) => {
|
|
||||||
self.attributes
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.retain(|a| a.name != attribute_name);
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
Some(_) => {
|
||||||
|
self.attributes
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.retain(|a| a.name != attribute_name);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,16 +144,17 @@ impl UserSchemaTable {
|
|||||||
|
|
||||||
fn view_attribute(&self, ctx: &Context<Self>, attribute: &Attribute) -> Html {
|
fn view_attribute(&self, ctx: &Context<Self>, attribute: &Attribute) -> Html {
|
||||||
let link = ctx.link();
|
let link = ctx.link();
|
||||||
let attribute_type = AttributeType::from(attribute.attribute_type.clone());
|
let attribute_type = attribute.attribute_type;
|
||||||
let checkmark = html! {
|
let checkmark = html! {
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
|
||||||
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z"></path>
|
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
};
|
};
|
||||||
let hardcoded = ctx.props().hardcoded;
|
let hardcoded = ctx.props().hardcoded;
|
||||||
|
let desc = user::resolve_user_attribute_description_or_default(&attribute.name);
|
||||||
html! {
|
html! {
|
||||||
<tr key={attribute.name.clone()}>
|
<tr key={attribute.name.clone()}>
|
||||||
<td>{&attribute.name}</td>
|
<td>{render_attribute_name(hardcoded, &desc)}</td>
|
||||||
<td>{if attribute.is_list { format!("List<{attribute_type}>")} else {attribute_type.to_string()}}</td>
|
<td>{if attribute.is_list { format!("List<{attribute_type}>")} else {attribute_type.to_string()}}</td>
|
||||||
<td>{if attribute.is_editable {checkmark.clone()} else {html!{}}}</td>
|
<td>{if attribute.is_editable {checkmark.clone()} else {html!{}}}</td>
|
||||||
<td>{if attribute.is_visible {checkmark.clone()} else {html!{}}}</td>
|
<td>{if attribute.is_visible {checkmark.clone()} else {html!{}}}</td>
|
||||||
|
|||||||
+13
-14
@@ -1,10 +1,11 @@
|
|||||||
use super::cookies::set_cookie;
|
use super::cookies::set_cookie;
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use gloo_net::http::{Method, RequestBuilder};
|
use gloo_net::http::{Method, RequestBuilder};
|
||||||
use graphql_client::GraphQLQuery;
|
use graphql_client::GraphQLQuery;
|
||||||
use lldap_auth::{login, registration, JWTClaims};
|
use lldap_auth::{JWTClaims, login, registration};
|
||||||
|
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use lldap_frontend_options::Options;
|
||||||
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
use web_sys::RequestCredentials;
|
use web_sys::RequestCredentials;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -137,6 +138,15 @@ impl HostService {
|
|||||||
.and_then(set_cookies_from_jwt)
|
.and_then(set_cookies_from_jwt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_settings() -> Result<Options> {
|
||||||
|
call_server_json_with_error_message::<Options, _>(
|
||||||
|
&(base_url() + "/settings"),
|
||||||
|
GET_REQUEST,
|
||||||
|
"Could not fetch settings: ",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn register_start(
|
pub async fn register_start(
|
||||||
request: registration::ClientRegistrationStartRequest,
|
request: registration::ClientRegistrationStartRequest,
|
||||||
) -> Result<Box<registration::ServerRegistrationStartResponse>> {
|
) -> Result<Box<registration::ServerRegistrationStartResponse>> {
|
||||||
@@ -202,15 +212,4 @@ impl HostService {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn probe_password_reset() -> Result<bool> {
|
|
||||||
Ok(gloo_net::http::Request::post(
|
|
||||||
&(base_url() + "/auth/reset/step1/lldap_unlikely_very_long_user_name"),
|
|
||||||
)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.status()
|
|
||||||
!= http::StatusCode::NOT_FOUND)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
pub struct AttributeDescription<'a> {
|
||||||
|
pub attribute_identifier: &'a str,
|
||||||
|
pub attribute_name: &'a str,
|
||||||
|
pub aliases: Vec<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod group {
|
||||||
|
|
||||||
|
use super::AttributeDescription;
|
||||||
|
|
||||||
|
pub fn resolve_group_attribute_description(name: &str) -> Option<AttributeDescription> {
|
||||||
|
match name {
|
||||||
|
"creation_date" => Some(AttributeDescription {
|
||||||
|
attribute_identifier: name,
|
||||||
|
attribute_name: "creationdate",
|
||||||
|
aliases: vec![name, "createtimestamp", "modifytimestamp"],
|
||||||
|
}),
|
||||||
|
"display_name" => Some(AttributeDescription {
|
||||||
|
attribute_identifier: name,
|
||||||
|
attribute_name: "displayname",
|
||||||
|
aliases: vec![name, "cn", "uid", "id"],
|
||||||
|
}),
|
||||||
|
"group_id" => Some(AttributeDescription {
|
||||||
|
attribute_identifier: name,
|
||||||
|
attribute_name: "groupid",
|
||||||
|
aliases: vec![name],
|
||||||
|
}),
|
||||||
|
"uuid" => Some(AttributeDescription {
|
||||||
|
attribute_identifier: name,
|
||||||
|
attribute_name: name,
|
||||||
|
aliases: vec!["entryuuid"],
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_group_attribute_description_or_default(name: &str) -> AttributeDescription {
|
||||||
|
match resolve_group_attribute_description(name) {
|
||||||
|
Some(d) => d,
|
||||||
|
None => AttributeDescription {
|
||||||
|
attribute_identifier: name,
|
||||||
|
attribute_name: name,
|
||||||
|
aliases: vec![],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod user {
|
||||||
|
|
||||||
|
use super::AttributeDescription;
|
||||||
|
|
||||||
|
pub fn resolve_user_attribute_description(name: &str) -> Option<AttributeDescription> {
|
||||||
|
match name {
|
||||||
|
"avatar" => Some(AttributeDescription {
|
||||||
|
attribute_identifier: name,
|
||||||
|
attribute_name: name,
|
||||||
|
aliases: vec!["jpegphoto"],
|
||||||
|
}),
|
||||||
|
"creation_date" => Some(AttributeDescription {
|
||||||
|
attribute_identifier: name,
|
||||||
|
attribute_name: "creationdate",
|
||||||
|
aliases: vec![name, "createtimestamp", "modifytimestamp"],
|
||||||
|
}),
|
||||||
|
"display_name" => Some(AttributeDescription {
|
||||||
|
attribute_identifier: name,
|
||||||
|
attribute_name: "displayname",
|
||||||
|
aliases: vec![name, "cn"],
|
||||||
|
}),
|
||||||
|
"first_name" => Some(AttributeDescription {
|
||||||
|
attribute_identifier: name,
|
||||||
|
attribute_name: "firstname",
|
||||||
|
aliases: vec![name, "givenname"],
|
||||||
|
}),
|
||||||
|
"last_name" => Some(AttributeDescription {
|
||||||
|
attribute_identifier: name,
|
||||||
|
attribute_name: "lastname",
|
||||||
|
aliases: vec![name, "sn"],
|
||||||
|
}),
|
||||||
|
"mail" => Some(AttributeDescription {
|
||||||
|
attribute_identifier: name,
|
||||||
|
attribute_name: name,
|
||||||
|
aliases: vec!["email"],
|
||||||
|
}),
|
||||||
|
"user_id" => Some(AttributeDescription {
|
||||||
|
attribute_identifier: name,
|
||||||
|
attribute_name: "uid",
|
||||||
|
aliases: vec![name, "id"],
|
||||||
|
}),
|
||||||
|
"uuid" => Some(AttributeDescription {
|
||||||
|
attribute_identifier: name,
|
||||||
|
attribute_name: name,
|
||||||
|
aliases: vec!["entryuuid"],
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_user_attribute_description_or_default(name: &str) -> AttributeDescription {
|
||||||
|
match resolve_user_attribute_description(name) {
|
||||||
|
Some(d) => d,
|
||||||
|
None => AttributeDescription {
|
||||||
|
attribute_identifier: name,
|
||||||
|
attribute_name: name,
|
||||||
|
aliases: vec![],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use anyhow::{anyhow, Result};
|
use anyhow::{Result, anyhow};
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::HtmlDocument;
|
use web_sys::HtmlDocument;
|
||||||
|
|||||||
+11
-13
@@ -1,4 +1,4 @@
|
|||||||
use anyhow::{anyhow, ensure, Result};
|
use anyhow::{Result, anyhow, ensure};
|
||||||
use validator::validate_email;
|
use validator::validate_email;
|
||||||
use web_sys::{FormData, HtmlFormElement};
|
use web_sys::{FormData, HtmlFormElement};
|
||||||
use yew::NodeRef;
|
use yew::NodeRef;
|
||||||
@@ -16,18 +16,14 @@ pub struct GraphQlAttributeSchema {
|
|||||||
pub is_editable: bool,
|
pub is_editable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_attributes(
|
fn validate_email_attributes(all_values: &[AttributeValue]) -> Result<()> {
|
||||||
all_values: &[AttributeValue],
|
|
||||||
email_is_required: EmailIsRequired,
|
|
||||||
) -> Result<()> {
|
|
||||||
let maybe_email_values = all_values.iter().find(|a| a.name == "mail");
|
let maybe_email_values = all_values.iter().find(|a| a.name == "mail");
|
||||||
if email_is_required.0 || maybe_email_values.is_some() {
|
let email_values = &maybe_email_values
|
||||||
let email_values = &maybe_email_values
|
.ok_or_else(|| anyhow!("Email is required"))?
|
||||||
.ok_or_else(|| anyhow!("Email is required"))?
|
.values;
|
||||||
.values;
|
ensure!(!email_values.is_empty(), "Email is required");
|
||||||
ensure!(email_values.len() == 1, "Email is required");
|
ensure!(email_values.len() == 1, "Multiple emails are not supported");
|
||||||
ensure!(validate_email(&email_values[0]), "Email is not valid");
|
ensure!(validate_email(&email_values[0]), "Email is not valid");
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +61,8 @@ pub fn read_all_form_attributes(
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>>>()?;
|
.collect::<Result<Vec<_>>>()?;
|
||||||
validate_attributes(&all_values, email_is_required)?;
|
if email_is_required.0 {
|
||||||
|
validate_email_attributes(&all_values)?;
|
||||||
|
}
|
||||||
Ok(all_values)
|
Ok(all_values)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::infra::api::HostService;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use graphql_client::GraphQLQuery;
|
use graphql_client::GraphQLQuery;
|
||||||
use wasm_bindgen_futures::spawn_local;
|
use wasm_bindgen_futures::spawn_local;
|
||||||
use yew::{use_effect_with_deps, use_state_eq, UseStateHandle};
|
use yew::{UseStateHandle, use_effect_with_deps, use_state_eq};
|
||||||
|
|
||||||
// Enum to represent a result that is fetched asynchronously.
|
// Enum to represent a result that is fetched asynchronously.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod api;
|
pub mod api;
|
||||||
|
pub mod attributes;
|
||||||
pub mod common_component;
|
pub mod common_component;
|
||||||
pub mod cookies;
|
pub mod cookies;
|
||||||
pub mod form_utils;
|
pub mod form_utils;
|
||||||
|
|||||||
+31
-55
@@ -1,66 +1,42 @@
|
|||||||
use anyhow::Result;
|
use derive_more::Display;
|
||||||
use std::{fmt::Display, str::FromStr};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use strum::EnumString;
|
||||||
use validator::ValidationError;
|
use validator::ValidationError;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash, EnumString, Display)]
|
||||||
pub enum AttributeType {
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
pub(crate) enum AttributeType {
|
||||||
String,
|
String,
|
||||||
Integer,
|
Integer,
|
||||||
|
#[strum(serialize = "DATE_TIME", serialize = "DATETIME")]
|
||||||
DateTime,
|
DateTime,
|
||||||
Jpeg,
|
#[strum(serialize = "JPEG_PHOTO", serialize = "JPEGPHOTO")]
|
||||||
}
|
JpegPhoto,
|
||||||
|
|
||||||
impl Display for AttributeType {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{:?}", self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for AttributeType {
|
|
||||||
type Err = ();
|
|
||||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
|
||||||
match value {
|
|
||||||
"String" => Ok(AttributeType::String),
|
|
||||||
"Integer" => Ok(AttributeType::Integer),
|
|
||||||
"DateTime" => Ok(AttributeType::DateTime),
|
|
||||||
"Jpeg" => Ok(AttributeType::Jpeg),
|
|
||||||
_ => Err(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Macro to generate traits for converting between AttributeType and the
|
|
||||||
// graphql generated equivalents.
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! convert_attribute_type {
|
|
||||||
($source_type:ty) => {
|
|
||||||
impl From<$source_type> for $crate::infra::schema::AttributeType {
|
|
||||||
fn from(value: $source_type) -> Self {
|
|
||||||
match value {
|
|
||||||
<$source_type>::STRING => $crate::infra::schema::AttributeType::String,
|
|
||||||
<$source_type>::INTEGER => $crate::infra::schema::AttributeType::Integer,
|
|
||||||
<$source_type>::DATE_TIME => $crate::infra::schema::AttributeType::DateTime,
|
|
||||||
<$source_type>::JPEG_PHOTO => $crate::infra::schema::AttributeType::Jpeg,
|
|
||||||
_ => panic!("Unknown attribute type"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<$crate::infra::schema::AttributeType> for $source_type {
|
|
||||||
fn from(value: $crate::infra::schema::AttributeType) -> Self {
|
|
||||||
match value {
|
|
||||||
$crate::infra::schema::AttributeType::String => <$source_type>::STRING,
|
|
||||||
$crate::infra::schema::AttributeType::Integer => <$source_type>::INTEGER,
|
|
||||||
$crate::infra::schema::AttributeType::DateTime => <$source_type>::DATE_TIME,
|
|
||||||
$crate::infra::schema::AttributeType::Jpeg => <$source_type>::JPEG_PHOTO,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_attribute_type(attribute_type: &str) -> Result<(), ValidationError> {
|
pub fn validate_attribute_type(attribute_type: &str) -> Result<(), ValidationError> {
|
||||||
AttributeType::from_str(attribute_type)
|
AttributeType::try_from(attribute_type)
|
||||||
.map_err(|_| ValidationError::new("Invalid attribute type"))?;
|
.map_err(|_| ValidationError::new("Invalid attribute type"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_attribute_type() {
|
||||||
|
let attr_type: AttributeType = "STRING".try_into().unwrap();
|
||||||
|
assert_eq!(attr_type, AttributeType::String);
|
||||||
|
|
||||||
|
let attr_type: AttributeType = "Integer".try_into().unwrap();
|
||||||
|
assert_eq!(attr_type, AttributeType::Integer);
|
||||||
|
|
||||||
|
let attr_type: AttributeType = "DATE_TIME".try_into().unwrap();
|
||||||
|
assert_eq!(attr_type, AttributeType::DateTime);
|
||||||
|
|
||||||
|
let attr_type: AttributeType = "JpegPhoto".try_into().unwrap();
|
||||||
|
assert_eq!(attr_type, AttributeType::JpegPhoto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod infra;
|
pub mod infra;
|
||||||
|
|
||||||
use wasm_bindgen::prelude::{wasm_bindgen, JsValue};
|
use wasm_bindgen::prelude::{JsValue, wasm_bindgen};
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn run_app() -> Result<(), JsValue> {
|
pub fn run_app() -> Result<(), JsValue> {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
[package]
|
||||||
|
name = "lldap_access_control"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Access control wrappers for LLDAP"
|
||||||
|
authors.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tracing = "*"
|
||||||
|
async-trait = "0.1"
|
||||||
|
|
||||||
|
[dependencies.lldap_auth]
|
||||||
|
path = "../auth"
|
||||||
|
features = ["opaque_server", "opaque_client", "sea_orm"]
|
||||||
|
|
||||||
|
[dependencies.lldap_domain]
|
||||||
|
path = "../domain"
|
||||||
|
|
||||||
|
[dependencies.lldap_domain_handlers]
|
||||||
|
path = "../domain-handlers"
|
||||||
|
|
||||||
|
[dependencies.lldap_domain_model]
|
||||||
|
path = "../domain-model"
|
||||||
@@ -1,78 +1,25 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use tracing::info;
|
use lldap_auth::access_control::{Permission, ValidationResults};
|
||||||
|
use lldap_domain::{
|
||||||
use crate::domain::{
|
public_schema::PublicSchema,
|
||||||
error::Result,
|
requests::{
|
||||||
handler::{
|
CreateAttributeRequest, CreateGroupRequest, CreateUserRequest, UpdateGroupRequest,
|
||||||
AttributeSchema, BackendHandler, CreateAttributeRequest, CreateGroupRequest,
|
UpdateUserRequest,
|
||||||
CreateUserRequest, GroupBackendHandler, GroupListerBackendHandler, GroupRequestFilter,
|
|
||||||
ReadSchemaBackendHandler, Schema, SchemaBackendHandler, UpdateGroupRequest,
|
|
||||||
UpdateUserRequest, UserBackendHandler, UserListerBackendHandler, UserRequestFilter,
|
|
||||||
},
|
},
|
||||||
schema::PublicSchema,
|
schema::{AttributeSchema, Schema},
|
||||||
types::{
|
types::{
|
||||||
AttributeName, Group, GroupDetails, GroupId, GroupName, LdapObjectClass, User,
|
AttributeName, Group, GroupDetails, GroupId, GroupName, LdapObjectClass, User,
|
||||||
UserAndGroups, UserId,
|
UserAndGroups, UserId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use lldap_domain_handlers::handler::{
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
BackendHandler, GroupBackendHandler, GroupListerBackendHandler, GroupRequestFilter,
|
||||||
pub enum Permission {
|
ReadSchemaBackendHandler, SchemaBackendHandler, UserBackendHandler, UserListerBackendHandler,
|
||||||
Admin,
|
UserRequestFilter,
|
||||||
PasswordManager,
|
};
|
||||||
Readonly,
|
use lldap_domain_model::error::Result;
|
||||||
Regular,
|
use std::collections::HashSet;
|
||||||
}
|
use tracing::info;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct ValidationResults {
|
|
||||||
pub user: UserId,
|
|
||||||
pub permission: Permission,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ValidationResults {
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn admin() -> Self {
|
|
||||||
Self {
|
|
||||||
user: UserId::new("admin"),
|
|
||||||
permission: Permission::Admin,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn is_admin(&self) -> bool {
|
|
||||||
self.permission == Permission::Admin
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn can_read_all(&self) -> bool {
|
|
||||||
self.permission == Permission::Admin
|
|
||||||
|| self.permission == Permission::Readonly
|
|
||||||
|| self.permission == Permission::PasswordManager
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn can_read(&self, user: &UserId) -> bool {
|
|
||||||
self.permission == Permission::Admin
|
|
||||||
|| self.permission == Permission::PasswordManager
|
|
||||||
|| self.permission == Permission::Readonly
|
|
||||||
|| &self.user == user
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn can_change_password(&self, user: &UserId, user_is_admin: bool) -> bool {
|
|
||||||
self.permission == Permission::Admin
|
|
||||||
|| (self.permission == Permission::PasswordManager && !user_is_admin)
|
|
||||||
|| &self.user == user
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn can_write(&self, user: &UserId) -> bool {
|
|
||||||
self.permission == Permission::Admin || &self.user == user
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait UserReadableBackendHandler: ReadSchemaBackendHandler {
|
pub trait UserReadableBackendHandler: ReadSchemaBackendHandler {
|
||||||
@@ -231,17 +178,24 @@ impl<Handler: BackendHandler> AccessControlledBackendHandler<Handler> {
|
|||||||
Self { handler }
|
Self { handler }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_schema_only_handler(
|
||||||
|
&self,
|
||||||
|
_validation_result: &ValidationResults,
|
||||||
|
) -> Option<&impl ReadSchemaBackendHandler> {
|
||||||
|
Some(&self.handler)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_admin_handler(
|
pub fn get_admin_handler(
|
||||||
&self,
|
&self,
|
||||||
validation_result: &ValidationResults,
|
validation_result: &ValidationResults,
|
||||||
) -> Option<&impl AdminBackendHandler> {
|
) -> Option<&(impl AdminBackendHandler + use<Handler>)> {
|
||||||
validation_result.is_admin().then_some(&self.handler)
|
validation_result.is_admin().then_some(&self.handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_readonly_handler(
|
pub fn get_readonly_handler(
|
||||||
&self,
|
&self,
|
||||||
validation_result: &ValidationResults,
|
validation_result: &ValidationResults,
|
||||||
) -> Option<&impl ReadonlyBackendHandler> {
|
) -> Option<&(impl ReadonlyBackendHandler + use<Handler>)> {
|
||||||
validation_result.can_read_all().then_some(&self.handler)
|
validation_result.can_read_all().then_some(&self.handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +203,7 @@ impl<Handler: BackendHandler> AccessControlledBackendHandler<Handler> {
|
|||||||
&self,
|
&self,
|
||||||
validation_result: &ValidationResults,
|
validation_result: &ValidationResults,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
) -> Option<&impl UserWriteableBackendHandler> {
|
) -> Option<&(impl UserWriteableBackendHandler + use<Handler>)> {
|
||||||
validation_result
|
validation_result
|
||||||
.can_write(user_id)
|
.can_write(user_id)
|
||||||
.then_some(&self.handler)
|
.then_some(&self.handler)
|
||||||
@@ -259,7 +213,7 @@ impl<Handler: BackendHandler> AccessControlledBackendHandler<Handler> {
|
|||||||
&self,
|
&self,
|
||||||
validation_result: &ValidationResults,
|
validation_result: &ValidationResults,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
) -> Option<&impl UserReadableBackendHandler> {
|
) -> Option<&(impl UserReadableBackendHandler + use<Handler>)> {
|
||||||
validation_result.can_read(user_id).then_some(&self.handler)
|
validation_result.can_read(user_id).then_some(&self.handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,12 +264,12 @@ impl<Handler: BackendHandler> AccessControlledBackendHandler<Handler> {
|
|||||||
|
|
||||||
pub struct UserRestrictedListerBackendHandler<'a, Handler> {
|
pub struct UserRestrictedListerBackendHandler<'a, Handler> {
|
||||||
handler: &'a Handler,
|
handler: &'a Handler,
|
||||||
pub user_filter: Option<UserId>,
|
user_filter: Option<UserId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<'a, Handler: ReadSchemaBackendHandler + Sync> ReadSchemaBackendHandler
|
impl<Handler: ReadSchemaBackendHandler + Sync> ReadSchemaBackendHandler
|
||||||
for UserRestrictedListerBackendHandler<'a, Handler>
|
for UserRestrictedListerBackendHandler<'_, Handler>
|
||||||
{
|
{
|
||||||
async fn get_schema(&self) -> Result<Schema> {
|
async fn get_schema(&self) -> Result<Schema> {
|
||||||
let mut schema = self.handler.get_schema().await?;
|
let mut schema = self.handler.get_schema().await?;
|
||||||
@@ -331,8 +285,8 @@ impl<'a, Handler: ReadSchemaBackendHandler + Sync> ReadSchemaBackendHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<'a, Handler: UserListerBackendHandler + Sync> UserListerBackendHandler
|
impl<Handler: UserListerBackendHandler + Sync> UserListerBackendHandler
|
||||||
for UserRestrictedListerBackendHandler<'a, Handler>
|
for UserRestrictedListerBackendHandler<'_, Handler>
|
||||||
{
|
{
|
||||||
async fn list_users(
|
async fn list_users(
|
||||||
&self,
|
&self,
|
||||||
@@ -354,8 +308,8 @@ impl<'a, Handler: UserListerBackendHandler + Sync> UserListerBackendHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<'a, Handler: GroupListerBackendHandler + Sync> GroupListerBackendHandler
|
impl<Handler: GroupListerBackendHandler + Sync> GroupListerBackendHandler
|
||||||
for UserRestrictedListerBackendHandler<'a, Handler>
|
for UserRestrictedListerBackendHandler<'_, Handler>
|
||||||
{
|
{
|
||||||
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>> {
|
async fn list_groups(&self, filters: Option<GroupRequestFilter>) -> Result<Vec<Group>> {
|
||||||
let group_filter = self
|
let group_filter = self
|
||||||
@@ -376,10 +330,14 @@ impl<'a, Handler: GroupListerBackendHandler + Sync> GroupListerBackendHandler
|
|||||||
pub trait UserAndGroupListerBackendHandler:
|
pub trait UserAndGroupListerBackendHandler:
|
||||||
UserListerBackendHandler + GroupListerBackendHandler
|
UserListerBackendHandler + GroupListerBackendHandler
|
||||||
{
|
{
|
||||||
|
fn user_filter(&self) -> &Option<UserId>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<'a, Handler: GroupListerBackendHandler + UserListerBackendHandler + Sync>
|
impl<Handler: GroupListerBackendHandler + UserListerBackendHandler + Sync>
|
||||||
UserAndGroupListerBackendHandler for UserRestrictedListerBackendHandler<'a, Handler>
|
UserAndGroupListerBackendHandler for UserRestrictedListerBackendHandler<'_, Handler>
|
||||||
{
|
{
|
||||||
|
fn user_filter(&self) -> &Option<UserId> {
|
||||||
|
&self.user_filter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
[package]
|
[package]
|
||||||
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
|
|
||||||
description = "Authentication protocol for LLDAP"
|
|
||||||
edition = "2021"
|
|
||||||
homepage = "https://github.com/lldap/lldap"
|
|
||||||
license = "GPL-3.0-only"
|
|
||||||
name = "lldap_auth"
|
name = "lldap_auth"
|
||||||
repository = "https://github.com/lldap/lldap"
|
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
|
description = "Authentication protocol for LLDAP"
|
||||||
|
edition.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["opaque_server", "opaque_client"]
|
default = ["opaque_server", "opaque_client"]
|
||||||
@@ -14,16 +14,16 @@ opaque_server = []
|
|||||||
opaque_client = []
|
opaque_client = []
|
||||||
js = []
|
js = []
|
||||||
sea_orm = ["dep:sea-orm"]
|
sea_orm = ["dep:sea-orm"]
|
||||||
|
test = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rust-argon2 = "0.8"
|
rust-argon2 = "2"
|
||||||
curve25519-dalek = "3"
|
curve25519-dalek = "3"
|
||||||
digest = "0.9"
|
digest = "0.9"
|
||||||
generic-array = "0.14"
|
generic-array = "0.14"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
serde = "*"
|
|
||||||
sha2 = "0.9"
|
sha2 = "0.9"
|
||||||
thiserror = "*"
|
thiserror = "2"
|
||||||
|
|
||||||
[dependencies.derive_more]
|
[dependencies.derive_more]
|
||||||
features = ["debug", "display"]
|
features = ["debug", "display"]
|
||||||
@@ -38,11 +38,13 @@ version = "*"
|
|||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
|
|
||||||
[dependencies.sea-orm]
|
[dependencies.sea-orm]
|
||||||
version = "0.12"
|
workspace = true
|
||||||
default-features = false
|
|
||||||
features = ["macros"]
|
features = ["macros"]
|
||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
# For WASM targets, use the JS getrandom.
|
# For WASM targets, use the JS getrandom.
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.getrandom]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.getrandom]
|
||||||
version = "0.2"
|
version = "0.2"
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
use crate::types::UserId;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Permission {
|
||||||
|
Admin,
|
||||||
|
PasswordManager,
|
||||||
|
Readonly,
|
||||||
|
Regular,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ValidationResults {
|
||||||
|
pub user: UserId,
|
||||||
|
pub permission: Permission,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValidationResults {
|
||||||
|
#[cfg(feature = "test")]
|
||||||
|
pub fn admin() -> Self {
|
||||||
|
Self {
|
||||||
|
user: UserId::new("admin"),
|
||||||
|
permission: Permission::Admin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_admin(&self) -> bool {
|
||||||
|
self.permission == Permission::Admin
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn can_read_all(&self) -> bool {
|
||||||
|
self.permission == Permission::Admin
|
||||||
|
|| self.permission == Permission::Readonly
|
||||||
|
|| self.permission == Permission::PasswordManager
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn can_read(&self, user: &UserId) -> bool {
|
||||||
|
self.permission == Permission::Admin
|
||||||
|
|| self.permission == Permission::PasswordManager
|
||||||
|
|| self.permission == Permission::Readonly
|
||||||
|
|| &self.user == user
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn can_change_password(&self, user: &UserId, user_is_admin: bool) -> bool {
|
||||||
|
self.permission == Permission::Admin
|
||||||
|
|| (self.permission == Permission::PasswordManager && !user_is_admin)
|
||||||
|
|| &self.user == user
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn can_write(&self, user: &UserId) -> bool {
|
||||||
|
self.permission == Permission::Admin || &self.user == user
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
pub mod access_control;
|
||||||
pub mod opaque;
|
pub mod opaque;
|
||||||
|
|
||||||
/// The messages for the 3-step OPAQUE and simple login process.
|
/// The messages for the 3-step OPAQUE and simple login process.
|
||||||
@@ -32,7 +32,6 @@ impl ArgonHasher {
|
|||||||
lanes: 1,
|
lanes: 1,
|
||||||
mem_cost: 50 * 1024, // 50 MB, in KB
|
mem_cost: 50 * 1024, // 50 MB, in KB
|
||||||
secret: &[],
|
secret: &[],
|
||||||
thread_mode: argon2::ThreadMode::Sequential,
|
|
||||||
time_cost: 1,
|
time_cost: 1,
|
||||||
variant: argon2::Variant::Argon2id,
|
variant: argon2::Variant::Argon2id,
|
||||||
version: argon2::Version::Version13,
|
version: argon2::Version::Version13,
|
||||||
@@ -133,6 +132,12 @@ pub mod server {
|
|||||||
pub use super::*;
|
pub use super::*;
|
||||||
pub type ServerRegistration = opaque_ke::ServerRegistration<DefaultSuite>;
|
pub type ServerRegistration = opaque_ke::ServerRegistration<DefaultSuite>;
|
||||||
pub type ServerSetup = opaque_ke::ServerSetup<DefaultSuite>;
|
pub type ServerSetup = opaque_ke::ServerSetup<DefaultSuite>;
|
||||||
|
|
||||||
|
pub fn generate_random_private_key() -> ServerSetup {
|
||||||
|
let mut rng = rand::rngs::OsRng;
|
||||||
|
ServerSetup::new(&mut rng)
|
||||||
|
}
|
||||||
|
|
||||||
/// Methods to register a new user, from the server side.
|
/// Methods to register a new user, from the server side.
|
||||||
pub mod registration {
|
pub mod registration {
|
||||||
pub use super::*;
|
pub use super::*;
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
[package]
|
||||||
|
name = "lldap_domain_handlers"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-trait = "0.1"
|
||||||
|
base64 = "0.21"
|
||||||
|
ldap3_proto = "0.6.0"
|
||||||
|
serde_bytes = "0.11"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pretty_assertions = "1"
|
||||||
|
|
||||||
|
[dependencies.chrono]
|
||||||
|
features = ["serde"]
|
||||||
|
version = "0.4"
|
||||||
|
|
||||||
|
[dependencies.derive_more]
|
||||||
|
features = ["debug", "display", "from", "from_str"]
|
||||||
|
default-features = false
|
||||||
|
version = "1"
|
||||||
|
|
||||||
|
[dependencies.lldap_auth]
|
||||||
|
path = "../auth"
|
||||||
|
features = ["opaque_server", "opaque_client", "sea_orm"]
|
||||||
|
|
||||||
|
[dependencies.lldap_domain]
|
||||||
|
path = "../domain"
|
||||||
|
|
||||||
|
[dependencies.lldap_domain_model]
|
||||||
|
path = "../domain-model"
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies.uuid]
|
||||||
|
features = ["v1", "v3"]
|
||||||
|
version = "1"
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
use crate::domain::{
|
use async_trait::async_trait;
|
||||||
error::Result,
|
use ldap3_proto::proto::LdapSubstringFilter;
|
||||||
|
use lldap_domain::{
|
||||||
|
requests::{
|
||||||
|
CreateAttributeRequest, CreateGroupRequest, CreateUserRequest, UpdateGroupRequest,
|
||||||
|
UpdateUserRequest,
|
||||||
|
},
|
||||||
|
schema::Schema,
|
||||||
types::{
|
types::{
|
||||||
AttributeName, AttributeType, AttributeValue, Email, Group, GroupDetails, GroupId,
|
AttributeName, AttributeValue, Group, GroupDetails, GroupId, GroupName, LdapObjectClass,
|
||||||
GroupName, JpegPhoto, LdapObjectClass, Serialized, User, UserAndGroups, UserColumn, UserId,
|
User, UserAndGroups, UserId, Uuid,
|
||||||
Uuid,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use lldap_domain_model::{error::Result, model::UserColumn};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
@@ -47,15 +52,33 @@ impl SubStringFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<LdapSubstringFilter> for SubStringFilter {
|
||||||
|
fn from(
|
||||||
|
LdapSubstringFilter {
|
||||||
|
initial,
|
||||||
|
any,
|
||||||
|
final_,
|
||||||
|
}: LdapSubstringFilter,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
initial,
|
||||||
|
any,
|
||||||
|
final_,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
|
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
|
||||||
pub enum UserRequestFilter {
|
pub enum UserRequestFilter {
|
||||||
|
True,
|
||||||
|
False,
|
||||||
And(Vec<UserRequestFilter>),
|
And(Vec<UserRequestFilter>),
|
||||||
Or(Vec<UserRequestFilter>),
|
Or(Vec<UserRequestFilter>),
|
||||||
Not(Box<UserRequestFilter>),
|
Not(Box<UserRequestFilter>),
|
||||||
UserId(UserId),
|
UserId(UserId),
|
||||||
UserIdSubString(SubStringFilter),
|
UserIdSubString(SubStringFilter),
|
||||||
Equality(UserColumn, String),
|
Equality(UserColumn, String),
|
||||||
AttributeEquality(AttributeName, Serialized),
|
AttributeEquality(AttributeName, AttributeValue),
|
||||||
SubString(UserColumn, SubStringFilter),
|
SubString(UserColumn, SubStringFilter),
|
||||||
// Check if a user belongs to a group identified by name.
|
// Check if a user belongs to a group identified by name.
|
||||||
MemberOf(GroupName),
|
MemberOf(GroupName),
|
||||||
@@ -66,16 +89,14 @@ pub enum UserRequestFilter {
|
|||||||
|
|
||||||
impl From<bool> for UserRequestFilter {
|
impl From<bool> for UserRequestFilter {
|
||||||
fn from(val: bool) -> Self {
|
fn from(val: bool) -> Self {
|
||||||
if val {
|
if val { Self::True } else { Self::False }
|
||||||
Self::And(vec![])
|
|
||||||
} else {
|
|
||||||
Self::Not(Box::new(Self::And(vec![])))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
|
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
|
||||||
pub enum GroupRequestFilter {
|
pub enum GroupRequestFilter {
|
||||||
|
True,
|
||||||
|
False,
|
||||||
And(Vec<GroupRequestFilter>),
|
And(Vec<GroupRequestFilter>),
|
||||||
Or(Vec<GroupRequestFilter>),
|
Or(Vec<GroupRequestFilter>),
|
||||||
Not(Box<GroupRequestFilter>),
|
Not(Box<GroupRequestFilter>),
|
||||||
@@ -85,104 +106,16 @@ pub enum GroupRequestFilter {
|
|||||||
GroupId(GroupId),
|
GroupId(GroupId),
|
||||||
// Check if the group contains a user identified by uid.
|
// Check if the group contains a user identified by uid.
|
||||||
Member(UserId),
|
Member(UserId),
|
||||||
AttributeEquality(AttributeName, Serialized),
|
AttributeEquality(AttributeName, AttributeValue),
|
||||||
CustomAttributePresent(AttributeName),
|
CustomAttributePresent(AttributeName),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<bool> for GroupRequestFilter {
|
impl From<bool> for GroupRequestFilter {
|
||||||
fn from(val: bool) -> Self {
|
fn from(val: bool) -> Self {
|
||||||
if val {
|
if val { Self::True } else { Self::False }
|
||||||
Self::And(vec![])
|
|
||||||
} else {
|
|
||||||
Self::Not(Box::new(Self::And(vec![])))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)]
|
|
||||||
pub struct CreateUserRequest {
|
|
||||||
// Same fields as User, but no creation_date, and with password.
|
|
||||||
pub user_id: UserId,
|
|
||||||
pub email: Email,
|
|
||||||
pub display_name: Option<String>,
|
|
||||||
pub first_name: Option<String>,
|
|
||||||
pub last_name: Option<String>,
|
|
||||||
pub avatar: Option<JpegPhoto>,
|
|
||||||
pub attributes: Vec<AttributeValue>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)]
|
|
||||||
pub struct UpdateUserRequest {
|
|
||||||
// Same fields as CreateUserRequest, but no with an extra layer of Option.
|
|
||||||
pub user_id: UserId,
|
|
||||||
pub email: Option<Email>,
|
|
||||||
pub display_name: Option<String>,
|
|
||||||
pub first_name: Option<String>,
|
|
||||||
pub last_name: Option<String>,
|
|
||||||
pub avatar: Option<JpegPhoto>,
|
|
||||||
pub delete_attributes: Vec<AttributeName>,
|
|
||||||
pub insert_attributes: Vec<AttributeValue>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)]
|
|
||||||
pub struct CreateGroupRequest {
|
|
||||||
pub display_name: GroupName,
|
|
||||||
pub attributes: Vec<AttributeValue>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct UpdateGroupRequest {
|
|
||||||
pub group_id: GroupId,
|
|
||||||
pub display_name: Option<GroupName>,
|
|
||||||
pub delete_attributes: Vec<AttributeName>,
|
|
||||||
pub insert_attributes: Vec<AttributeValue>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct AttributeSchema {
|
|
||||||
pub name: AttributeName,
|
|
||||||
//TODO: pub aliases: Vec<String>,
|
|
||||||
pub attribute_type: AttributeType,
|
|
||||||
pub is_list: bool,
|
|
||||||
pub is_visible: bool,
|
|
||||||
pub is_editable: bool,
|
|
||||||
pub is_hardcoded: bool,
|
|
||||||
pub is_readonly: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct CreateAttributeRequest {
|
|
||||||
pub name: AttributeName,
|
|
||||||
pub attribute_type: AttributeType,
|
|
||||||
pub is_list: bool,
|
|
||||||
pub is_visible: bool,
|
|
||||||
pub is_editable: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct AttributeList {
|
|
||||||
pub attributes: Vec<AttributeSchema>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AttributeList {
|
|
||||||
pub fn get_attribute_schema(&self, name: &AttributeName) -> Option<&AttributeSchema> {
|
|
||||||
self.attributes.iter().find(|a| a.name == *name)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_attribute_type(&self, name: &AttributeName) -> Option<(AttributeType, bool)> {
|
|
||||||
self.get_attribute_schema(name)
|
|
||||||
.map(|a| (a.attribute_type, a.is_list))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct Schema {
|
|
||||||
pub user_attributes: AttributeList,
|
|
||||||
pub group_attributes: AttributeList,
|
|
||||||
pub extra_user_object_classes: Vec<LdapObjectClass>,
|
|
||||||
pub extra_group_object_classes: Vec<LdapObjectClass>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait LoginHandler: Send + Sync {
|
pub trait LoginHandler: Send + Sync {
|
||||||
async fn bind(&self, request: BindRequest) -> Result<()>;
|
async fn bind(&self, request: BindRequest) -> Result<()>;
|
||||||
@@ -257,6 +190,7 @@ pub trait BackendHandler:
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
|
use lldap_domain::types::JpegPhoto;
|
||||||
use pretty_assertions::assert_ne;
|
use pretty_assertions::assert_ne;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod handler;
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
[package]
|
||||||
|
name = "lldap_domain_model"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
base64 = "0.21"
|
||||||
|
bincode = "1.3"
|
||||||
|
orion = "0.17"
|
||||||
|
serde_bytes = "0.11"
|
||||||
|
thiserror = "2"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pretty_assertions = "1"
|
||||||
|
|
||||||
|
[dependencies.chrono]
|
||||||
|
features = ["serde"]
|
||||||
|
version = "0.4"
|
||||||
|
|
||||||
|
[dependencies.derive_more]
|
||||||
|
features = ["debug", "display", "from", "from_str"]
|
||||||
|
default-features = false
|
||||||
|
version = "1"
|
||||||
|
|
||||||
|
[dependencies.lldap_auth]
|
||||||
|
path = "../auth"
|
||||||
|
features = ["opaque_server", "opaque_client", "sea_orm"]
|
||||||
|
|
||||||
|
[dependencies.lldap_domain]
|
||||||
|
path = "../domain"
|
||||||
|
|
||||||
|
[dependencies.sea-orm]
|
||||||
|
workspace = true
|
||||||
|
features = ["macros"]
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies.uuid]
|
||||||
|
features = ["v1", "v3"]
|
||||||
|
version = "1"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod error;
|
||||||
|
pub mod model;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
use crate::error::DomainError;
|
||||||
|
use lldap_domain::{
|
||||||
|
schema::AttributeList,
|
||||||
|
types::{Attribute, AttributeName, AttributeType, AttributeValue, Cardinality, Serialized},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Value must be a serialized attribute value of the type denoted by typ,
|
||||||
|
// and either a singleton or unbounded list, depending on is_list.
|
||||||
|
pub fn deserialize_attribute_value(
|
||||||
|
value: &Serialized,
|
||||||
|
typ: AttributeType,
|
||||||
|
is_list: bool,
|
||||||
|
) -> AttributeValue {
|
||||||
|
match (typ, is_list) {
|
||||||
|
(AttributeType::String, false) => {
|
||||||
|
AttributeValue::String(Cardinality::Singleton(value.unwrap()))
|
||||||
|
}
|
||||||
|
(AttributeType::String, true) => {
|
||||||
|
AttributeValue::String(Cardinality::Unbounded(value.unwrap()))
|
||||||
|
}
|
||||||
|
(AttributeType::Integer, false) => {
|
||||||
|
AttributeValue::Integer(Cardinality::Singleton(value.unwrap::<i64>()))
|
||||||
|
}
|
||||||
|
(AttributeType::Integer, true) => {
|
||||||
|
AttributeValue::Integer(Cardinality::Unbounded(value.unwrap()))
|
||||||
|
}
|
||||||
|
(AttributeType::DateTime, false) => {
|
||||||
|
AttributeValue::DateTime(Cardinality::Singleton(value.unwrap()))
|
||||||
|
}
|
||||||
|
(AttributeType::DateTime, true) => {
|
||||||
|
AttributeValue::DateTime(Cardinality::Unbounded(value.unwrap()))
|
||||||
|
}
|
||||||
|
(AttributeType::JpegPhoto, false) => {
|
||||||
|
AttributeValue::JpegPhoto(Cardinality::Singleton(value.unwrap()))
|
||||||
|
}
|
||||||
|
(AttributeType::JpegPhoto, true) => {
|
||||||
|
AttributeValue::JpegPhoto(Cardinality::Unbounded(value.unwrap()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize_attribute(
|
||||||
|
name: AttributeName,
|
||||||
|
value: &Serialized,
|
||||||
|
schema: &AttributeList,
|
||||||
|
) -> Result<Attribute, DomainError> {
|
||||||
|
match schema.get_attribute_type(&name) {
|
||||||
|
Some((typ, is_list)) => Ok(Attribute {
|
||||||
|
name,
|
||||||
|
value: deserialize_attribute_value(value, typ, is_list),
|
||||||
|
}),
|
||||||
|
None => Err(DomainError::InternalError(format!(
|
||||||
|
"Unable to find schema for attribute named '{}'",
|
||||||
|
name.into_string()
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::domain::{
|
use lldap_domain::{
|
||||||
handler::AttributeSchema,
|
schema::AttributeSchema,
|
||||||
types::{AttributeName, AttributeType},
|
types::{AttributeName, AttributeType},
|
||||||
};
|
};
|
||||||
|
|
||||||
+1
-16
@@ -1,7 +1,7 @@
|
|||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::domain::types::{AttributeName, AttributeValue, GroupId, Serialized};
|
use lldap_domain::types::{AttributeName, GroupId, Serialized};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||||
#[sea_orm(table_name = "group_attributes")]
|
#[sea_orm(table_name = "group_attributes")]
|
||||||
@@ -55,18 +55,3 @@ impl Related<super::GroupAttributeSchema> for Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
||||||
impl From<Model> for AttributeValue {
|
|
||||||
fn from(
|
|
||||||
Model {
|
|
||||||
group_id: _,
|
|
||||||
attribute_name,
|
|
||||||
value,
|
|
||||||
}: Model,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
name: attribute_name,
|
|
||||||
value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::domain::types::LdapObjectClass;
|
use lldap_domain::types::LdapObjectClass;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||||
#[sea_orm(table_name = "group_object_classes")]
|
#[sea_orm(table_name = "group_object_classes")]
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::domain::types::{GroupId, GroupName, Uuid};
|
use lldap_domain::types::{GroupId, GroupName, Uuid};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||||
#[sea_orm(table_name = "groups")]
|
#[sea_orm(table_name = "groups")]
|
||||||
@@ -30,7 +30,7 @@ impl Related<super::memberships::Entity> for Entity {
|
|||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
||||||
impl From<Model> for crate::domain::types::Group {
|
impl From<Model> for lldap_domain::types::Group {
|
||||||
fn from(group: Model) -> Self {
|
fn from(group: Model) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: group.group_id,
|
id: group.group_id,
|
||||||
@@ -43,7 +43,7 @@ impl From<Model> for crate::domain::types::Group {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Model> for crate::domain::types::GroupDetails {
|
impl From<Model> for lldap_domain::types::GroupDetails {
|
||||||
fn from(group: Model) -> Self {
|
fn from(group: Model) -> Self {
|
||||||
Self {
|
Self {
|
||||||
group_id: group.group_id,
|
group_id: group.group_id,
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::domain::types::UserId;
|
use lldap_domain::types::UserId;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||||
#[sea_orm(table_name = "jwt_refresh_storage")]
|
#[sea_orm(table_name = "jwt_refresh_storage")]
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::domain::types::UserId;
|
use lldap_domain::types::UserId;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||||
#[sea_orm(table_name = "jwt_storage")]
|
#[sea_orm(table_name = "jwt_storage")]
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::domain::types::{GroupId, UserId};
|
use lldap_domain::types::{GroupId, UserId};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||||
#[sea_orm(table_name = "memberships")]
|
#[sea_orm(table_name = "memberships")]
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
|
|
||||||
|
pub mod deserialize;
|
||||||
pub mod groups;
|
pub mod groups;
|
||||||
pub mod jwt_refresh_storage;
|
pub mod jwt_refresh_storage;
|
||||||
pub mod jwt_storage;
|
pub mod jwt_storage;
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::domain::types::UserId;
|
use lldap_domain::types::UserId;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||||
#[sea_orm(table_name = "password_reset_tokens")]
|
#[sea_orm(table_name = "password_reset_tokens")]
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::domain::{
|
use lldap_domain::{
|
||||||
handler::AttributeSchema,
|
schema::AttributeSchema,
|
||||||
types::{AttributeName, AttributeType},
|
types::{AttributeName, AttributeType},
|
||||||
};
|
};
|
||||||
|
|
||||||
+1
-16
@@ -1,7 +1,7 @@
|
|||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::domain::types::{AttributeName, AttributeValue, Serialized, UserId};
|
use lldap_domain::types::{AttributeName, Serialized, UserId};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||||
#[sea_orm(table_name = "user_attributes")]
|
#[sea_orm(table_name = "user_attributes")]
|
||||||
@@ -55,18 +55,3 @@ impl Related<super::UserAttributeSchema> for Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
||||||
impl From<Model> for AttributeValue {
|
|
||||||
fn from(
|
|
||||||
Model {
|
|
||||||
user_id: _,
|
|
||||||
attribute_name,
|
|
||||||
value,
|
|
||||||
}: Model,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
name: attribute_name,
|
|
||||||
value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::domain::types::LdapObjectClass;
|
use lldap_domain::types::LdapObjectClass;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||||
#[sea_orm(table_name = "user_object_classes")]
|
#[sea_orm(table_name = "user_object_classes")]
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.3
|
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.3
|
||||||
|
|
||||||
use sea_orm::{entity::prelude::*, sea_query::BlobSize};
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::domain::types::{Email, UserId, Uuid};
|
use lldap_domain::types::{Email, UserId, Uuid};
|
||||||
|
|
||||||
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
|
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
|
||||||
pub struct Entity;
|
pub struct Entity;
|
||||||
@@ -47,15 +47,15 @@ impl ColumnTrait for Column {
|
|||||||
|
|
||||||
fn def(&self) -> ColumnDef {
|
fn def(&self) -> ColumnDef {
|
||||||
match self {
|
match self {
|
||||||
Column::UserId => ColumnType::String(Some(255)),
|
Column::UserId => ColumnType::String(StringLen::N(255)),
|
||||||
Column::Email => ColumnType::String(Some(255)),
|
Column::Email => ColumnType::String(StringLen::N(255)),
|
||||||
Column::LowercaseEmail => ColumnType::String(Some(255)),
|
Column::LowercaseEmail => ColumnType::String(StringLen::N(255)),
|
||||||
Column::DisplayName => ColumnType::String(Some(255)),
|
Column::DisplayName => ColumnType::String(StringLen::N(255)),
|
||||||
Column::CreationDate => ColumnType::DateTime,
|
Column::CreationDate => ColumnType::DateTime,
|
||||||
Column::PasswordHash => ColumnType::Binary(BlobSize::Medium),
|
Column::PasswordHash => ColumnType::Blob,
|
||||||
Column::TotpSecret => ColumnType::String(Some(64)),
|
Column::TotpSecret => ColumnType::String(StringLen::N(64)),
|
||||||
Column::MfaType => ColumnType::String(Some(64)),
|
Column::MfaType => ColumnType::String(StringLen::N(64)),
|
||||||
Column::Uuid => ColumnType::String(Some(36)),
|
Column::Uuid => ColumnType::String(StringLen::N(36)),
|
||||||
}
|
}
|
||||||
.def()
|
.def()
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ impl Related<super::password_reset_tokens::Entity> for Entity {
|
|||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
||||||
impl From<Model> for crate::domain::types::User {
|
impl From<Model> for lldap_domain::types::User {
|
||||||
fn from(user: Model) -> Self {
|
fn from(user: Model) -> Self {
|
||||||
Self {
|
Self {
|
||||||
user_id: user.user_id,
|
user_id: user.user_id,
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
[package]
|
||||||
|
name = "lldap_domain"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = [
|
||||||
|
"Valentin Tolmer <valentin@tolmer.fr>",
|
||||||
|
"Simon Broeng Jensen <sbj@cwconsult.dk>",
|
||||||
|
]
|
||||||
|
edition.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "*"
|
||||||
|
base64 = "0.21"
|
||||||
|
bincode = "1.3"
|
||||||
|
itertools = "0.10"
|
||||||
|
juniper = "0.15"
|
||||||
|
serde_bytes = "0.11"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pretty_assertions = "1"
|
||||||
|
|
||||||
|
[dependencies.chrono]
|
||||||
|
features = ["serde"]
|
||||||
|
version = "*"
|
||||||
|
|
||||||
|
[dependencies.derive_more]
|
||||||
|
features = ["debug", "display", "from", "from_str"]
|
||||||
|
default-features = false
|
||||||
|
version = "1"
|
||||||
|
|
||||||
|
[dependencies.image]
|
||||||
|
features = ["jpeg"]
|
||||||
|
default-features = false
|
||||||
|
version = "0.24"
|
||||||
|
|
||||||
|
[dependencies.lldap_auth]
|
||||||
|
path = "../auth"
|
||||||
|
features = ["opaque_server", "opaque_client", "sea_orm"]
|
||||||
|
|
||||||
|
[dependencies.sea-orm]
|
||||||
|
workspace = true
|
||||||
|
features = [
|
||||||
|
"macros",
|
||||||
|
"with-chrono",
|
||||||
|
"with-uuid",
|
||||||
|
"sqlx-all",
|
||||||
|
"runtime-actix-rustls",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies.strum]
|
||||||
|
features = ["derive"]
|
||||||
|
version = "0.25"
|
||||||
|
|
||||||
|
[dependencies.uuid]
|
||||||
|
features = ["v1", "v3"]
|
||||||
|
version = "1"
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
use crate::types::{AttributeType, AttributeValue, JpegPhoto};
|
||||||
|
use anyhow::{Context as AnyhowContext, Result, bail};
|
||||||
|
|
||||||
|
pub fn deserialize_attribute_value(
|
||||||
|
value: &[String],
|
||||||
|
typ: AttributeType,
|
||||||
|
is_list: bool,
|
||||||
|
) -> Result<AttributeValue> {
|
||||||
|
if !is_list && value.len() != 1 {
|
||||||
|
bail!("Attribute is not a list, but multiple values were provided",);
|
||||||
|
}
|
||||||
|
let parse_int = |value: &String| -> Result<i64> {
|
||||||
|
value
|
||||||
|
.parse::<i64>()
|
||||||
|
.with_context(|| format!("Invalid integer value {value}"))
|
||||||
|
};
|
||||||
|
let parse_date = |value: &String| -> Result<chrono::NaiveDateTime> {
|
||||||
|
Ok(chrono::DateTime::parse_from_rfc3339(value)
|
||||||
|
.with_context(|| format!("Invalid date value {value}"))?
|
||||||
|
.naive_utc())
|
||||||
|
};
|
||||||
|
let parse_photo = |value: &String| -> Result<JpegPhoto> {
|
||||||
|
JpegPhoto::try_from(value.as_str()).context("Provided image is not a valid JPEG")
|
||||||
|
};
|
||||||
|
Ok(match (typ, is_list) {
|
||||||
|
(AttributeType::String, false) => value[0].clone().into(),
|
||||||
|
(AttributeType::String, true) => value.to_vec().into(),
|
||||||
|
(AttributeType::Integer, false) => (parse_int(&value[0])?).into(),
|
||||||
|
(AttributeType::Integer, true) => {
|
||||||
|
(value.iter().map(parse_int).collect::<Result<Vec<_>>>()?).into()
|
||||||
|
}
|
||||||
|
(AttributeType::DateTime, false) => (parse_date(&value[0])?).into(),
|
||||||
|
(AttributeType::DateTime, true) => {
|
||||||
|
(value.iter().map(parse_date).collect::<Result<Vec<_>>>()?).into()
|
||||||
|
}
|
||||||
|
(AttributeType::JpegPhoto, false) => (parse_photo(&value[0])?).into(),
|
||||||
|
(AttributeType::JpegPhoto, true) => {
|
||||||
|
(value.iter().map(parse_photo).collect::<Result<Vec<_>>>()?).into()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod deserialize;
|
||||||
|
pub mod public_schema;
|
||||||
|
pub mod requests;
|
||||||
|
pub mod schema;
|
||||||
|
pub mod types;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::domain::{
|
use crate::{
|
||||||
handler::{AttributeList, AttributeSchema, Schema},
|
schema::{AttributeSchema, Schema},
|
||||||
types::AttributeType,
|
types::AttributeType,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -13,26 +13,6 @@ impl PublicSchema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait SchemaAttributeExtractor: std::marker::Send {
|
|
||||||
fn get_attributes(schema: &PublicSchema) -> &AttributeList;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SchemaUserAttributeExtractor;
|
|
||||||
|
|
||||||
impl SchemaAttributeExtractor for SchemaUserAttributeExtractor {
|
|
||||||
fn get_attributes(schema: &PublicSchema) -> &AttributeList {
|
|
||||||
&schema.get_schema().user_attributes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SchemaGroupAttributeExtractor;
|
|
||||||
|
|
||||||
impl SchemaAttributeExtractor for SchemaGroupAttributeExtractor {
|
|
||||||
fn get_attributes(schema: &PublicSchema) -> &AttributeList {
|
|
||||||
&schema.get_schema().group_attributes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Schema> for PublicSchema {
|
impl From<Schema> for PublicSchema {
|
||||||
fn from(mut schema: Schema) -> Self {
|
fn from(mut schema: Schema) -> Self {
|
||||||
schema.user_attributes.attributes.extend_from_slice(&[
|
schema.user_attributes.attributes.extend_from_slice(&[
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::types::{Attribute, AttributeName, AttributeType, Email, GroupId, GroupName, UserId};
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)]
|
||||||
|
pub struct CreateUserRequest {
|
||||||
|
// Same fields as User, but no creation_date, and with password.
|
||||||
|
pub user_id: UserId,
|
||||||
|
pub email: Email,
|
||||||
|
pub display_name: Option<String>,
|
||||||
|
pub attributes: Vec<Attribute>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)]
|
||||||
|
pub struct UpdateUserRequest {
|
||||||
|
// Same fields as CreateUserRequest, but no with an extra layer of Option.
|
||||||
|
pub user_id: UserId,
|
||||||
|
pub email: Option<Email>,
|
||||||
|
pub display_name: Option<String>,
|
||||||
|
pub delete_attributes: Vec<AttributeName>,
|
||||||
|
pub insert_attributes: Vec<Attribute>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)]
|
||||||
|
pub struct CreateGroupRequest {
|
||||||
|
pub display_name: GroupName,
|
||||||
|
pub attributes: Vec<Attribute>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct UpdateGroupRequest {
|
||||||
|
pub group_id: GroupId,
|
||||||
|
pub display_name: Option<GroupName>,
|
||||||
|
pub delete_attributes: Vec<AttributeName>,
|
||||||
|
pub insert_attributes: Vec<Attribute>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct CreateAttributeRequest {
|
||||||
|
pub name: AttributeName,
|
||||||
|
pub attribute_type: AttributeType,
|
||||||
|
pub is_list: bool,
|
||||||
|
pub is_visible: bool,
|
||||||
|
pub is_editable: bool,
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::types::{AttributeName, AttributeType, LdapObjectClass};
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct Schema {
|
||||||
|
pub user_attributes: AttributeList,
|
||||||
|
pub group_attributes: AttributeList,
|
||||||
|
pub extra_user_object_classes: Vec<LdapObjectClass>,
|
||||||
|
pub extra_group_object_classes: Vec<LdapObjectClass>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct AttributeSchema {
|
||||||
|
pub name: AttributeName,
|
||||||
|
//TODO: pub aliases: Vec<String>,
|
||||||
|
pub attribute_type: AttributeType,
|
||||||
|
pub is_list: bool,
|
||||||
|
pub is_visible: bool,
|
||||||
|
pub is_editable: bool,
|
||||||
|
pub is_hardcoded: bool,
|
||||||
|
pub is_readonly: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct AttributeList {
|
||||||
|
pub attributes: Vec<AttributeSchema>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttributeList {
|
||||||
|
pub fn get_attribute_schema(&self, name: &AttributeName) -> Option<&AttributeSchema> {
|
||||||
|
self.attributes.iter().find(|a| a.name == *name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_attribute_type(&self, name: &AttributeName) -> Option<(AttributeType, bool)> {
|
||||||
|
self.get_attribute_schema(name)
|
||||||
|
.map(|a| (a.attribute_type, a.is_list))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_for_ldap_schema_description(&self) -> String {
|
||||||
|
self.attributes
|
||||||
|
.iter()
|
||||||
|
.map(|a| a.name.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" $ ")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,14 +4,16 @@ use base64::Engine;
|
|||||||
use chrono::{NaiveDateTime, TimeZone};
|
use chrono::{NaiveDateTime, TimeZone};
|
||||||
use lldap_auth::types::CaseInsensitiveString;
|
use lldap_auth::types::CaseInsensitiveString;
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
entity::IntoActiveValue,
|
|
||||||
sea_query::{value::ValueType, ArrayType, BlobSize, ColumnType, Nullable, ValueTypeErr},
|
|
||||||
DbErr, DeriveValueType, QueryResult, TryFromU64, TryGetError, TryGetable, Value,
|
DbErr, DeriveValueType, QueryResult, TryFromU64, TryGetError, TryGetable, Value,
|
||||||
|
entity::IntoActiveValue,
|
||||||
|
sea_query::{
|
||||||
|
ArrayType, ColumnType, Nullable, SeaRc, StringLen, ValueTypeErr,
|
||||||
|
extension::mysql::MySqlType, value::ValueType,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use strum::{EnumString, IntoStaticStr};
|
use strum::{EnumString, IntoStaticStr};
|
||||||
|
|
||||||
pub use super::model::UserColumn;
|
|
||||||
pub use lldap_auth::types::UserId;
|
pub use lldap_auth::types::UserId;
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
@@ -27,7 +29,7 @@ pub use lldap_auth::types::UserId;
|
|||||||
derive_more::Display,
|
derive_more::Display,
|
||||||
)]
|
)]
|
||||||
#[serde(try_from = "&str")]
|
#[serde(try_from = "&str")]
|
||||||
#[sea_orm(column_type = "String(Some(36))")]
|
#[sea_orm(column_type = "String(StringLen::N(36))")]
|
||||||
#[debug(r#""{_0}""#)]
|
#[debug(r#""{_0}""#)]
|
||||||
#[display("{_0}")]
|
#[display("{_0}")]
|
||||||
pub struct Uuid(String);
|
pub struct Uuid(String);
|
||||||
@@ -66,16 +68,19 @@ impl<'a> std::convert::TryFrom<&'a str> for Uuid {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(feature = "test")]
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! uuid {
|
macro_rules! uuid {
|
||||||
($s:literal) => {
|
($s:literal) => {
|
||||||
<$crate::domain::types::Uuid as std::convert::TryFrom<_>>::try_from($s).unwrap()
|
<lldap_domain::types::Uuid as std::convert::TryFrom<_>>::try_from($s).unwrap()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, DeriveValueType)]
|
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, DeriveValueType)]
|
||||||
#[sea_orm(column_type = "Binary(BlobSize::Long)", array_type = "Bytes")]
|
#[sea_orm(
|
||||||
|
column_type = "Custom(SeaRc::new(MySqlType::LongBlob))",
|
||||||
|
array_type = "Bytes"
|
||||||
|
)]
|
||||||
pub struct Serialized(Vec<u8>);
|
pub struct Serialized(Vec<u8>);
|
||||||
|
|
||||||
const SERIALIZED_I64_LEN: usize = 8;
|
const SERIALIZED_I64_LEN: usize = 8;
|
||||||
@@ -131,6 +136,21 @@ impl Serialized {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<AttributeValue> for Serialized {
|
||||||
|
fn from(val: AttributeValue) -> Serialized {
|
||||||
|
match &val {
|
||||||
|
AttributeValue::String(Cardinality::Singleton(s)) => Serialized::from(s),
|
||||||
|
AttributeValue::String(Cardinality::Unbounded(l)) => Serialized::from(l),
|
||||||
|
AttributeValue::Integer(Cardinality::Singleton(i)) => Serialized::from(i),
|
||||||
|
AttributeValue::Integer(Cardinality::Unbounded(l)) => Serialized::from(l),
|
||||||
|
AttributeValue::JpegPhoto(Cardinality::Singleton(p)) => Serialized::from(p),
|
||||||
|
AttributeValue::JpegPhoto(Cardinality::Unbounded(l)) => Serialized::from(l),
|
||||||
|
AttributeValue::DateTime(Cardinality::Singleton(dt)) => Serialized::from(dt),
|
||||||
|
AttributeValue::DateTime(Cardinality::Unbounded(l)) => Serialized::from(l),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn compare_str_case_insensitive(s1: &str, s2: &str) -> Ordering {
|
fn compare_str_case_insensitive(s1: &str, s2: &str) -> Ordering {
|
||||||
let mut it_1 = s1.chars().flat_map(|c| c.to_lowercase());
|
let mut it_1 = s1.chars().flat_map(|c| c.to_lowercase());
|
||||||
let mut it_2 = s2.chars().flat_map(|c| c.to_lowercase());
|
let mut it_2 = s2.chars().flat_map(|c| c.to_lowercase());
|
||||||
@@ -293,8 +313,11 @@ impl AsRef<GroupName> for GroupName {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone, Serialize, Deserialize, DeriveValueType)]
|
#[derive(PartialEq, Eq, Clone, Serialize, Deserialize, DeriveValueType, Hash)]
|
||||||
#[sea_orm(column_type = "Binary(BlobSize::Long)", array_type = "Bytes")]
|
#[sea_orm(
|
||||||
|
column_type = "Custom(SeaRc::new(MySqlType::LongBlob))",
|
||||||
|
array_type = "Bytes"
|
||||||
|
)]
|
||||||
pub struct JpegPhoto(#[serde(with = "serde_bytes")] Vec<u8>);
|
pub struct JpegPhoto(#[serde(with = "serde_bytes")] Vec<u8>);
|
||||||
|
|
||||||
impl From<&JpegPhoto> for Value {
|
impl From<&JpegPhoto> for Value {
|
||||||
@@ -354,7 +377,7 @@ impl std::fmt::Debug for JpegPhoto {
|
|||||||
encoded.push_str(" ...");
|
encoded.push_str(" ...");
|
||||||
};
|
};
|
||||||
f.debug_tuple("JpegPhoto")
|
f.debug_tuple("JpegPhoto")
|
||||||
.field(&format!("b64[{}]", encoded))
|
.field(&format!("b64[{encoded}]"))
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -372,7 +395,7 @@ impl JpegPhoto {
|
|||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(any(feature = "test", test))]
|
||||||
pub fn for_tests() -> Self {
|
pub fn for_tests() -> Self {
|
||||||
use image::{ImageOutputFormat, Rgb, RgbImage};
|
use image::{ImageOutputFormat, Rgb, RgbImage};
|
||||||
let img = RgbImage::from_fn(32, 32, |x, y| {
|
let img = RgbImage::from_fn(32, 32, |x, y| {
|
||||||
@@ -408,10 +431,111 @@ impl IntoActiveValue<Serialized> for JpegPhoto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Represents values that can be either a singleton or a list of a specific type
|
||||||
|
// Used by AttributeValue to model attributes with types that might be a list.
|
||||||
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Hash)]
|
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Hash)]
|
||||||
pub struct AttributeValue {
|
pub enum Cardinality<T: Clone> {
|
||||||
|
Singleton(T),
|
||||||
|
Unbounded(Vec<T>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> Cardinality<T> {
|
||||||
|
pub fn into_vec(self) -> Vec<T> {
|
||||||
|
match self {
|
||||||
|
Self::Singleton(v) => vec![v],
|
||||||
|
Self::Unbounded(l) => l,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Hash)]
|
||||||
|
pub enum AttributeValue {
|
||||||
|
String(Cardinality<String>),
|
||||||
|
Integer(Cardinality<i64>),
|
||||||
|
JpegPhoto(Cardinality<JpegPhoto>),
|
||||||
|
DateTime(Cardinality<NaiveDateTime>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttributeValue {
|
||||||
|
pub fn get_attribute_type(&self) -> AttributeType {
|
||||||
|
match self {
|
||||||
|
Self::String(_) => AttributeType::String,
|
||||||
|
Self::Integer(_) => AttributeType::Integer,
|
||||||
|
Self::JpegPhoto(_) => AttributeType::JpegPhoto,
|
||||||
|
Self::DateTime(_) => AttributeType::DateTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn as_str(&self) -> Option<&str> {
|
||||||
|
if let AttributeValue::String(Cardinality::Singleton(s)) = self {
|
||||||
|
Some(s.as_str())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn into_string(self) -> Option<String> {
|
||||||
|
if let AttributeValue::String(Cardinality::Singleton(s)) = self {
|
||||||
|
Some(s)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn as_jpeg_photo(&self) -> Option<&JpegPhoto> {
|
||||||
|
if let AttributeValue::JpegPhoto(Cardinality::Singleton(p)) = self {
|
||||||
|
Some(p)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for AttributeValue {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
AttributeValue::String(Cardinality::Singleton(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<Vec<String>> for AttributeValue {
|
||||||
|
fn from(l: Vec<String>) -> Self {
|
||||||
|
AttributeValue::String(Cardinality::Unbounded(l))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i64> for AttributeValue {
|
||||||
|
fn from(i: i64) -> Self {
|
||||||
|
AttributeValue::Integer(Cardinality::Singleton(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<Vec<i64>> for AttributeValue {
|
||||||
|
fn from(l: Vec<i64>) -> Self {
|
||||||
|
AttributeValue::Integer(Cardinality::Unbounded(l))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<JpegPhoto> for AttributeValue {
|
||||||
|
fn from(j: JpegPhoto) -> Self {
|
||||||
|
AttributeValue::JpegPhoto(Cardinality::Singleton(j))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<Vec<JpegPhoto>> for AttributeValue {
|
||||||
|
fn from(l: Vec<JpegPhoto>) -> Self {
|
||||||
|
AttributeValue::JpegPhoto(Cardinality::Unbounded(l))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NaiveDateTime> for AttributeValue {
|
||||||
|
fn from(dt: NaiveDateTime) -> Self {
|
||||||
|
AttributeValue::DateTime(Cardinality::Singleton(dt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<Vec<NaiveDateTime>> for AttributeValue {
|
||||||
|
fn from(l: Vec<NaiveDateTime>) -> Self {
|
||||||
|
AttributeValue::DateTime(Cardinality::Unbounded(l))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Hash)]
|
||||||
|
pub struct Attribute {
|
||||||
pub name: AttributeName,
|
pub name: AttributeName,
|
||||||
pub value: Serialized,
|
pub value: AttributeValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
|
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -421,10 +545,10 @@ pub struct User {
|
|||||||
pub display_name: Option<String>,
|
pub display_name: Option<String>,
|
||||||
pub creation_date: NaiveDateTime,
|
pub creation_date: NaiveDateTime,
|
||||||
pub uuid: Uuid,
|
pub uuid: Uuid,
|
||||||
pub attributes: Vec<AttributeValue>,
|
pub attributes: Vec<Attribute>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(feature = "test")]
|
||||||
impl Default for User {
|
impl Default for User {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let epoch = chrono::Utc.timestamp_opt(0, 0).unwrap().naive_utc();
|
let epoch = chrono::Utc.timestamp_opt(0, 0).unwrap().naive_utc();
|
||||||
@@ -518,7 +642,7 @@ impl ValueType for AttributeType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn column_type() -> ColumnType {
|
fn column_type() -> ColumnType {
|
||||||
ColumnType::String(Some(64))
|
ColumnType::String(StringLen::N(64))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,7 +653,7 @@ pub struct Group {
|
|||||||
pub creation_date: NaiveDateTime,
|
pub creation_date: NaiveDateTime,
|
||||||
pub uuid: Uuid,
|
pub uuid: Uuid,
|
||||||
pub users: Vec<UserId>,
|
pub users: Vec<UserId>,
|
||||||
pub attributes: Vec<AttributeValue>,
|
pub attributes: Vec<Attribute>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
@@ -538,7 +662,7 @@ pub struct GroupDetails {
|
|||||||
pub display_name: GroupName,
|
pub display_name: GroupName,
|
||||||
pub creation_date: NaiveDateTime,
|
pub creation_date: NaiveDateTime,
|
||||||
pub uuid: Uuid,
|
pub uuid: Uuid,
|
||||||
pub attributes: Vec<AttributeValue>,
|
pub attributes: Vec<Attribute>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "lldap_frontend_options"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Frontend options for LLDAP"
|
||||||
|
authors.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
workspace = true
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Options {
|
||||||
|
pub password_reset_enabled: bool,
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
[package]
|
||||||
|
name = "lldap_graphql_server"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "GraphQL server for LLDAP"
|
||||||
|
edition.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "*"
|
||||||
|
juniper = "0.15"
|
||||||
|
serde_json = "1"
|
||||||
|
tracing = "*"
|
||||||
|
urlencoding = "2"
|
||||||
|
|
||||||
|
[dependencies.chrono]
|
||||||
|
features = ["serde"]
|
||||||
|
version = "*"
|
||||||
|
|
||||||
|
[dependencies.lldap_access_control]
|
||||||
|
path = "../access-control"
|
||||||
|
|
||||||
|
[dependencies.lldap_auth]
|
||||||
|
path = "../auth"
|
||||||
|
features = ["opaque_server", "opaque_client", "sea_orm"]
|
||||||
|
|
||||||
|
[dependencies.lldap_domain]
|
||||||
|
path = "../domain"
|
||||||
|
|
||||||
|
[dependencies.lldap_domain_model]
|
||||||
|
path = "../domain-model"
|
||||||
|
|
||||||
|
[dependencies.lldap_domain_handlers]
|
||||||
|
path = "../domain-handlers"
|
||||||
|
|
||||||
|
[dependencies.lldap_ldap]
|
||||||
|
path = "../ldap"
|
||||||
|
|
||||||
|
[dependencies.lldap_sql_backend_handler]
|
||||||
|
path = "../sql-backend-handler"
|
||||||
|
|
||||||
|
[dependencies.lldap_validation]
|
||||||
|
path = "../validation"
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies.uuid]
|
||||||
|
features = ["v1", "v3"]
|
||||||
|
version = "1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
mockall = "0.11.4"
|
||||||
|
pretty_assertions = "1"
|
||||||
|
|
||||||
|
#[dev-dependencies.lldap_auth]
|
||||||
|
#path = "../auth"
|
||||||
|
#features = ["test"]
|
||||||
|
#
|
||||||
|
#[dev-dependencies.lldap_opaque_handler]
|
||||||
|
#path = "../opaque-handler"
|
||||||
|
#features = ["test"]
|
||||||
|
|
||||||
|
[dev-dependencies.lldap_test_utils]
|
||||||
|
path = "../test-utils"
|
||||||
|
#
|
||||||
|
#[dev-dependencies.lldap_sql_backend_handler]
|
||||||
|
#path = "../sql-backend-handler"
|
||||||
|
#features = ["test"]
|
||||||
|
|
||||||
|
[dev-dependencies.tokio]
|
||||||
|
features = ["full"]
|
||||||
|
version = "1.25"
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
use crate::{mutation::Mutation, query::Query};
|
||||||
|
use juniper::{EmptySubscription, FieldError, RootNode};
|
||||||
|
use lldap_access_control::{
|
||||||
|
AccessControlledBackendHandler, AdminBackendHandler, ReadonlyBackendHandler,
|
||||||
|
UserReadableBackendHandler, UserWriteableBackendHandler,
|
||||||
|
};
|
||||||
|
use lldap_auth::{access_control::ValidationResults, types::UserId};
|
||||||
|
use lldap_domain_handlers::handler::BackendHandler;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
pub struct Context<Handler: BackendHandler> {
|
||||||
|
pub handler: AccessControlledBackendHandler<Handler>,
|
||||||
|
pub validation_result: ValidationResults,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn field_error_callback<'a>(
|
||||||
|
span: &'a tracing::Span,
|
||||||
|
error_message: &'a str,
|
||||||
|
) -> impl 'a + FnOnce() -> FieldError {
|
||||||
|
move || {
|
||||||
|
span.in_scope(|| debug!("Unauthorized"));
|
||||||
|
FieldError::from(error_message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Handler: BackendHandler> Context<Handler> {
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn new_for_tests(handler: Handler, validation_result: ValidationResults) -> Self {
|
||||||
|
Self {
|
||||||
|
handler: AccessControlledBackendHandler::new(handler),
|
||||||
|
validation_result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_admin_handler(&self) -> Option<&(impl AdminBackendHandler + use<Handler>)> {
|
||||||
|
self.handler.get_admin_handler(&self.validation_result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_readonly_handler(&self) -> Option<&(impl ReadonlyBackendHandler + use<Handler>)> {
|
||||||
|
self.handler.get_readonly_handler(&self.validation_result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_writeable_handler(
|
||||||
|
&self,
|
||||||
|
user_id: &UserId,
|
||||||
|
) -> Option<&(impl UserWriteableBackendHandler + use<Handler>)> {
|
||||||
|
self.handler
|
||||||
|
.get_writeable_handler(&self.validation_result, user_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_readable_handler(
|
||||||
|
&self,
|
||||||
|
user_id: &UserId,
|
||||||
|
) -> Option<&(impl UserReadableBackendHandler + use<Handler>)> {
|
||||||
|
self.handler
|
||||||
|
.get_readable_handler(&self.validation_result, user_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Handler: BackendHandler> juniper::Context for Context<Handler> {}
|
||||||
|
|
||||||
|
type Schema<Handler> =
|
||||||
|
RootNode<'static, Query<Handler>, Mutation<Handler>, EmptySubscription<Context<Handler>>>;
|
||||||
|
|
||||||
|
pub fn schema<Handler: BackendHandler>() -> Schema<Handler> {
|
||||||
|
Schema::new(
|
||||||
|
Query::<Handler>::new(),
|
||||||
|
Mutation::<Handler>::new(),
|
||||||
|
EmptySubscription::<Context<Handler>>::new(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn export_schema(output_file: Option<String>) -> anyhow::Result<()> {
|
||||||
|
use anyhow::Context;
|
||||||
|
use lldap_sql_backend_handler::SqlBackendHandler;
|
||||||
|
let output = schema::<SqlBackendHandler>().as_schema_language();
|
||||||
|
match output_file {
|
||||||
|
None => println!("{output}"),
|
||||||
|
Some(path) => {
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::prelude::*;
|
||||||
|
use std::path::Path;
|
||||||
|
let path = Path::new(&path);
|
||||||
|
let mut file =
|
||||||
|
File::create(path).context(format!("unable to open '{}'", path.display()))?;
|
||||||
|
file.write_all(output.as_bytes())
|
||||||
|
.context(format!("unable to write in '{}'", path.display()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,30 +1,27 @@
|
|||||||
use std::sync::Arc;
|
use crate::api::{Context, field_error_callback};
|
||||||
|
use anyhow::{Context as AnyhowContext, anyhow};
|
||||||
use crate::{
|
use juniper::{FieldError, FieldResult, GraphQLInputObject, GraphQLObject, graphql_object};
|
||||||
domain::{
|
use lldap_access_control::{
|
||||||
deserialize::deserialize_attribute_value,
|
AdminBackendHandler, ReadonlyBackendHandler, UserReadableBackendHandler,
|
||||||
handler::{
|
UserWriteableBackendHandler,
|
||||||
AttributeList, BackendHandler, CreateAttributeRequest, CreateGroupRequest,
|
};
|
||||||
CreateUserRequest, UpdateGroupRequest, UpdateUserRequest,
|
use lldap_domain::{
|
||||||
},
|
deserialize::deserialize_attribute_value,
|
||||||
schema::PublicSchema,
|
public_schema::PublicSchema,
|
||||||
types::{
|
requests::{
|
||||||
AttributeName, AttributeType, AttributeValue as DomainAttributeValue, Email, GroupId,
|
CreateAttributeRequest, CreateGroupRequest, CreateUserRequest, UpdateGroupRequest,
|
||||||
JpegPhoto, LdapObjectClass, UserId,
|
UpdateUserRequest,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
infra::{
|
schema::AttributeList,
|
||||||
access_control::{
|
types::{
|
||||||
AdminBackendHandler, ReadonlyBackendHandler, UserReadableBackendHandler,
|
Attribute as DomainAttribute, AttributeName, AttributeType, Email, GroupId,
|
||||||
UserWriteableBackendHandler,
|
LdapObjectClass, UserId,
|
||||||
},
|
|
||||||
graphql::api::{field_error_callback, Context},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context as AnyhowContext};
|
use lldap_domain_handlers::handler::BackendHandler;
|
||||||
use base64::Engine;
|
use lldap_validation::attributes::{ALLOWED_CHARACTERS_DESCRIPTION, validate_attribute_name};
|
||||||
use juniper::{graphql_object, FieldResult, GraphQLInputObject, GraphQLObject};
|
use std::{collections::BTreeMap, sync::Arc};
|
||||||
use tracing::{debug, debug_span, Instrument, Span};
|
use tracing::{Instrument, Span, debug, debug_span};
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug)]
|
#[derive(PartialEq, Eq, Debug)]
|
||||||
/// The top-level GraphQL mutation type.
|
/// The top-level GraphQL mutation type.
|
||||||
@@ -32,6 +29,12 @@ pub struct Mutation<Handler: BackendHandler> {
|
|||||||
_phantom: std::marker::PhantomData<Box<Handler>>,
|
_phantom: std::marker::PhantomData<Box<Handler>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<Handler: BackendHandler> Default for Mutation<Handler> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<Handler: BackendHandler> Mutation<Handler> {
|
impl<Handler: BackendHandler> Mutation<Handler> {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -62,11 +65,16 @@ pub struct CreateUserInput {
|
|||||||
// The email can be specified as an attribute, but one of the two is required.
|
// The email can be specified as an attribute, but one of the two is required.
|
||||||
email: Option<String>,
|
email: Option<String>,
|
||||||
display_name: Option<String>,
|
display_name: Option<String>,
|
||||||
|
/// First name of user. Deprecated: use attribute instead.
|
||||||
|
/// If both field and corresponding attribute is supplied, the attribute will take precedence.
|
||||||
first_name: Option<String>,
|
first_name: Option<String>,
|
||||||
|
/// Last name of user. Deprecated: use attribute instead.
|
||||||
|
/// If both field and corresponding attribute is supplied, the attribute will take precedence.
|
||||||
last_name: Option<String>,
|
last_name: Option<String>,
|
||||||
/// Base64 encoded JpegPhoto.
|
/// Base64 encoded JpegPhoto. Deprecated: use attribute instead.
|
||||||
|
/// If both field and corresponding attribute is supplied, the attribute will take precedence.
|
||||||
avatar: Option<String>,
|
avatar: Option<String>,
|
||||||
/// User-defined attributes.
|
/// Attributes.
|
||||||
attributes: Option<Vec<AttributeValue>>,
|
attributes: Option<Vec<AttributeValue>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,9 +92,14 @@ pub struct UpdateUserInput {
|
|||||||
id: String,
|
id: String,
|
||||||
email: Option<String>,
|
email: Option<String>,
|
||||||
display_name: Option<String>,
|
display_name: Option<String>,
|
||||||
|
/// First name of user. Deprecated: use attribute instead.
|
||||||
|
/// If both field and corresponding attribute is supplied, the attribute will take precedence.
|
||||||
first_name: Option<String>,
|
first_name: Option<String>,
|
||||||
|
/// Last name of user. Deprecated: use attribute instead.
|
||||||
|
/// If both field and corresponding attribute is supplied, the attribute will take precedence.
|
||||||
last_name: Option<String>,
|
last_name: Option<String>,
|
||||||
/// Base64 encoded JpegPhoto.
|
/// Base64 encoded JpegPhoto. Deprecated: use attribute instead.
|
||||||
|
/// If both field and corresponding attribute is supplied, the attribute will take precedence.
|
||||||
avatar: Option<String>,
|
avatar: Option<String>,
|
||||||
/// Attribute names to remove.
|
/// Attribute names to remove.
|
||||||
/// They are processed before insertions.
|
/// They are processed before insertions.
|
||||||
@@ -125,7 +138,7 @@ impl Success {
|
|||||||
struct UnpackedAttributes {
|
struct UnpackedAttributes {
|
||||||
email: Option<Email>,
|
email: Option<Email>,
|
||||||
display_name: Option<String>,
|
display_name: Option<String>,
|
||||||
attributes: Vec<DomainAttributeValue>,
|
attributes: Vec<DomainAttribute>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unpack_attributes(
|
fn unpack_attributes(
|
||||||
@@ -139,7 +152,7 @@ fn unpack_attributes(
|
|||||||
.cloned()
|
.cloned()
|
||||||
.map(|attr| deserialize_attribute(&schema.get_schema().user_attributes, attr, is_admin))
|
.map(|attr| deserialize_attribute(&schema.get_schema().user_attributes, attr, is_admin))
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.map(|attr| attr.value.unwrap::<String>())
|
.map(|attr| attr.value.into_string().unwrap())
|
||||||
.map(Email::from);
|
.map(Email::from);
|
||||||
let display_name = attributes
|
let display_name = attributes
|
||||||
.iter()
|
.iter()
|
||||||
@@ -147,7 +160,7 @@ fn unpack_attributes(
|
|||||||
.cloned()
|
.cloned()
|
||||||
.map(|attr| deserialize_attribute(&schema.get_schema().user_attributes, attr, is_admin))
|
.map(|attr| deserialize_attribute(&schema.get_schema().user_attributes, attr, is_admin))
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.map(|attr| attr.value.unwrap::<String>());
|
.map(|attr| attr.value.into_string().unwrap());
|
||||||
let attributes = attributes
|
let attributes = attributes
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|attr| attr.name != "mail" && attr.name != "display_name")
|
.filter(|attr| attr.name != "mail" && attr.name != "display_name")
|
||||||
@@ -160,6 +173,52 @@ fn unpack_attributes(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Consolidates caller supplied user fields and attributes into a list of attributes.
|
||||||
|
///
|
||||||
|
/// A number of user fields are internally represented as attributes, but are still also
|
||||||
|
/// available as fields on user objects. This function consolidates these fields and the
|
||||||
|
/// given attributes into a resulting attribute list. If a value is supplied for both a
|
||||||
|
/// field and the corresponding attribute, the attribute will take precedence.
|
||||||
|
fn consolidate_attributes(
|
||||||
|
attributes: Vec<AttributeValue>,
|
||||||
|
first_name: Option<String>,
|
||||||
|
last_name: Option<String>,
|
||||||
|
avatar: Option<String>,
|
||||||
|
) -> Vec<AttributeValue> {
|
||||||
|
// Prepare map of the client provided attributes
|
||||||
|
let mut provided_attributes: BTreeMap<AttributeName, AttributeValue> = attributes
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| {
|
||||||
|
(
|
||||||
|
x.name.clone().into(),
|
||||||
|
AttributeValue {
|
||||||
|
name: x.name.to_ascii_lowercase(),
|
||||||
|
value: x.value,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
// Prepare list of fallback attribute values
|
||||||
|
let field_attrs = [
|
||||||
|
("first_name", first_name),
|
||||||
|
("last_name", last_name),
|
||||||
|
("avatar", avatar),
|
||||||
|
];
|
||||||
|
for (name, value) in field_attrs.into_iter() {
|
||||||
|
if let Some(val) = value {
|
||||||
|
let attr_name: AttributeName = name.into();
|
||||||
|
provided_attributes
|
||||||
|
.entry(attr_name)
|
||||||
|
.or_insert_with(|| AttributeValue {
|
||||||
|
name: name.to_string(),
|
||||||
|
value: vec![val],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Return the values of the resulting map
|
||||||
|
provided_attributes.into_values().collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[graphql_object(context = Context<Handler>)]
|
#[graphql_object(context = Context<Handler>)]
|
||||||
impl<Handler: BackendHandler> Mutation<Handler> {
|
impl<Handler: BackendHandler> Mutation<Handler> {
|
||||||
async fn create_user(
|
async fn create_user(
|
||||||
@@ -174,20 +233,18 @@ impl<Handler: BackendHandler> Mutation<Handler> {
|
|||||||
.get_admin_handler()
|
.get_admin_handler()
|
||||||
.ok_or_else(field_error_callback(&span, "Unauthorized user creation"))?;
|
.ok_or_else(field_error_callback(&span, "Unauthorized user creation"))?;
|
||||||
let user_id = UserId::new(&user.id);
|
let user_id = UserId::new(&user.id);
|
||||||
let avatar = user
|
|
||||||
.avatar
|
|
||||||
.map(|bytes| base64::engine::general_purpose::STANDARD.decode(bytes))
|
|
||||||
.transpose()
|
|
||||||
.context("Invalid base64 image")?
|
|
||||||
.map(JpegPhoto::try_from)
|
|
||||||
.transpose()
|
|
||||||
.context("Provided image is not a valid JPEG")?;
|
|
||||||
let schema = handler.get_schema().await?;
|
let schema = handler.get_schema().await?;
|
||||||
|
let consolidated_attributes = consolidate_attributes(
|
||||||
|
user.attributes.unwrap_or_default(),
|
||||||
|
user.first_name,
|
||||||
|
user.last_name,
|
||||||
|
user.avatar,
|
||||||
|
);
|
||||||
let UnpackedAttributes {
|
let UnpackedAttributes {
|
||||||
email,
|
email,
|
||||||
display_name,
|
display_name,
|
||||||
attributes,
|
attributes,
|
||||||
} = unpack_attributes(user.attributes.unwrap_or_default(), &schema, true)?;
|
} = unpack_attributes(consolidated_attributes, &schema, true)?;
|
||||||
handler
|
handler
|
||||||
.create_user(CreateUserRequest {
|
.create_user(CreateUserRequest {
|
||||||
user_id: user_id.clone(),
|
user_id: user_id.clone(),
|
||||||
@@ -197,9 +254,6 @@ impl<Handler: BackendHandler> Mutation<Handler> {
|
|||||||
.or(email)
|
.or(email)
|
||||||
.ok_or_else(|| anyhow!("Email is required when creating a new user"))?,
|
.ok_or_else(|| anyhow!("Email is required when creating a new user"))?,
|
||||||
display_name: user.display_name.or(display_name),
|
display_name: user.display_name.or(display_name),
|
||||||
first_name: user.first_name,
|
|
||||||
last_name: user.last_name,
|
|
||||||
avatar,
|
|
||||||
attributes,
|
attributes,
|
||||||
})
|
})
|
||||||
.instrument(span.clone())
|
.instrument(span.clone())
|
||||||
@@ -250,26 +304,32 @@ impl<Handler: BackendHandler> Mutation<Handler> {
|
|||||||
.get_writeable_handler(&user_id)
|
.get_writeable_handler(&user_id)
|
||||||
.ok_or_else(field_error_callback(&span, "Unauthorized user update"))?;
|
.ok_or_else(field_error_callback(&span, "Unauthorized user update"))?;
|
||||||
let is_admin = context.validation_result.is_admin();
|
let is_admin = context.validation_result.is_admin();
|
||||||
let avatar = user
|
|
||||||
.avatar
|
|
||||||
.map(|bytes| base64::engine::general_purpose::STANDARD.decode(bytes))
|
|
||||||
.transpose()
|
|
||||||
.context("Invalid base64 image")?
|
|
||||||
.map(JpegPhoto::try_from)
|
|
||||||
.transpose()
|
|
||||||
.context("Provided image is not a valid JPEG")?;
|
|
||||||
let schema = handler.get_schema().await?;
|
let schema = handler.get_schema().await?;
|
||||||
let user_insert_attributes = user.insert_attributes.unwrap_or_default();
|
// Consolidate attributes and fields into a combined attribute list
|
||||||
|
let consolidated_attributes = consolidate_attributes(
|
||||||
|
user.insert_attributes.unwrap_or_default(),
|
||||||
|
user.first_name,
|
||||||
|
user.last_name,
|
||||||
|
user.avatar,
|
||||||
|
);
|
||||||
|
// Extract any empty attributes into a list of attributes for deletion
|
||||||
|
let (delete_attrs, insert_attrs): (Vec<_>, Vec<_>) = consolidated_attributes
|
||||||
|
.into_iter()
|
||||||
|
.partition(|a| a.value == vec!["".to_string()]);
|
||||||
|
// Combine lists of attributes for removal
|
||||||
|
let mut delete_attributes: Vec<String> =
|
||||||
|
delete_attrs.iter().map(|a| a.name.to_owned()).collect();
|
||||||
|
delete_attributes.extend(user.remove_attributes.unwrap_or_default());
|
||||||
|
// Unpack attributes for update
|
||||||
let UnpackedAttributes {
|
let UnpackedAttributes {
|
||||||
email,
|
email,
|
||||||
display_name,
|
display_name,
|
||||||
attributes: insert_attributes,
|
attributes: insert_attributes,
|
||||||
} = unpack_attributes(user_insert_attributes, &schema, is_admin)?;
|
} = unpack_attributes(insert_attrs, &schema, is_admin)?;
|
||||||
let display_name = display_name.or_else(|| {
|
let display_name = display_name.or_else(|| {
|
||||||
// If the display name is not inserted, but removed, reset it.
|
// If the display name is not inserted, but removed, reset it.
|
||||||
user.remove_attributes
|
delete_attributes
|
||||||
.iter()
|
.iter()
|
||||||
.flatten()
|
|
||||||
.find(|attr| *attr == "display_name")
|
.find(|attr| *attr == "display_name")
|
||||||
.map(|_| String::new())
|
.map(|_| String::new())
|
||||||
});
|
});
|
||||||
@@ -278,12 +338,7 @@ impl<Handler: BackendHandler> Mutation<Handler> {
|
|||||||
user_id,
|
user_id,
|
||||||
email: user.email.map(Into::into).or(email),
|
email: user.email.map(Into::into).or(email),
|
||||||
display_name: user.display_name.or(display_name),
|
display_name: user.display_name.or(display_name),
|
||||||
first_name: user.first_name,
|
delete_attributes: delete_attributes
|
||||||
last_name: user.last_name,
|
|
||||||
avatar,
|
|
||||||
delete_attributes: user
|
|
||||||
.remove_attributes
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|attr| attr != "mail" && attr != "display_name")
|
.filter(|attr| attr != "mail" && attr != "display_name")
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
@@ -440,6 +495,22 @@ impl<Handler: BackendHandler> Mutation<Handler> {
|
|||||||
span.in_scope(|| {
|
span.in_scope(|| {
|
||||||
debug!(?name, ?attribute_type, is_list, is_visible, is_editable);
|
debug!(?name, ?attribute_type, is_list, is_visible, is_editable);
|
||||||
});
|
});
|
||||||
|
validate_attribute_name(&name).map_err(|invalid_chars: Vec<char>| -> FieldError {
|
||||||
|
let chars = String::from_iter(invalid_chars);
|
||||||
|
span.in_scope(|| {
|
||||||
|
debug!(
|
||||||
|
"Cannot create attribute with invalid name. Valid characters: {}. Invalid chars found: {}",
|
||||||
|
ALLOWED_CHARACTERS_DESCRIPTION,
|
||||||
|
chars
|
||||||
|
)
|
||||||
|
});
|
||||||
|
anyhow!(
|
||||||
|
"Cannot create attribute with invalid name. Valid characters: {}. Invalid chars found: {}",
|
||||||
|
ALLOWED_CHARACTERS_DESCRIPTION,
|
||||||
|
chars
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
})?;
|
||||||
let handler = context
|
let handler = context
|
||||||
.get_admin_handler()
|
.get_admin_handler()
|
||||||
.ok_or_else(field_error_callback(
|
.ok_or_else(field_error_callback(
|
||||||
@@ -471,6 +542,20 @@ impl<Handler: BackendHandler> Mutation<Handler> {
|
|||||||
span.in_scope(|| {
|
span.in_scope(|| {
|
||||||
debug!(?name, ?attribute_type, is_list, is_visible, is_editable);
|
debug!(?name, ?attribute_type, is_list, is_visible, is_editable);
|
||||||
});
|
});
|
||||||
|
validate_attribute_name(&name).map_err(|invalid_chars: Vec<char>| -> FieldError {
|
||||||
|
let chars = String::from_iter(invalid_chars);
|
||||||
|
span.in_scope(|| {
|
||||||
|
debug!(
|
||||||
|
"Cannot create attribute with invalid name. Invalid chars found: {}",
|
||||||
|
chars
|
||||||
|
)
|
||||||
|
});
|
||||||
|
anyhow!(
|
||||||
|
"Cannot create attribute with invalid name. Valid characters: {}",
|
||||||
|
ALLOWED_CHARACTERS_DESCRIPTION
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
})?;
|
||||||
let handler = context
|
let handler = context
|
||||||
.get_admin_handler()
|
.get_admin_handler()
|
||||||
.ok_or_else(field_error_callback(
|
.ok_or_else(field_error_callback(
|
||||||
@@ -665,7 +750,7 @@ fn deserialize_attribute(
|
|||||||
attribute_schema: &AttributeList,
|
attribute_schema: &AttributeList,
|
||||||
attribute: AttributeValue,
|
attribute: AttributeValue,
|
||||||
is_admin: bool,
|
is_admin: bool,
|
||||||
) -> FieldResult<DomainAttributeValue> {
|
) -> FieldResult<DomainAttribute> {
|
||||||
let attribute_name = AttributeName::from(attribute.name.as_str());
|
let attribute_name = AttributeName::from(attribute.name.as_str());
|
||||||
let attribute_schema = attribute_schema
|
let attribute_schema = attribute_schema
|
||||||
.get_attribute_schema(&attribute_name)
|
.get_attribute_schema(&attribute_name)
|
||||||
@@ -690,8 +775,344 @@ fn deserialize_attribute(
|
|||||||
attribute_schema.is_list,
|
attribute_schema.is_list,
|
||||||
)
|
)
|
||||||
.context(format!("While deserializing attribute {}", attribute.name))?;
|
.context(format!("While deserializing attribute {}", attribute.name))?;
|
||||||
Ok(DomainAttributeValue {
|
Ok(DomainAttribute {
|
||||||
name: attribute_name,
|
name: attribute_name,
|
||||||
value: deserialized_values,
|
value: deserialized_values,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::query::Query;
|
||||||
|
use juniper::{
|
||||||
|
DefaultScalarValue, EmptySubscription, GraphQLType, InputValue, RootNode, Variables,
|
||||||
|
execute, graphql_value,
|
||||||
|
};
|
||||||
|
use lldap_auth::access_control::{Permission, ValidationResults};
|
||||||
|
use lldap_domain::types::{AttributeName, AttributeType};
|
||||||
|
use lldap_test_utils::MockTestBackendHandler;
|
||||||
|
use mockall::predicate::eq;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
fn mutation_schema<'q, C, Q, M>(
|
||||||
|
query_root: Q,
|
||||||
|
mutation_root: M,
|
||||||
|
) -> RootNode<'q, Q, M, EmptySubscription<C>>
|
||||||
|
where
|
||||||
|
Q: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()> + 'q,
|
||||||
|
M: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()> + 'q,
|
||||||
|
{
|
||||||
|
RootNode::new(query_root, mutation_root, EmptySubscription::<C>::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_user_attribute_valid() {
|
||||||
|
const QUERY: &str = r#"
|
||||||
|
mutation CreateUserAttribute($name: String!, $attributeType: AttributeType!, $isList: Boolean!, $isVisible: Boolean!, $isEditable: Boolean!) {
|
||||||
|
addUserAttribute(name: $name, attributeType: $attributeType, isList: $isList, isVisible: $isVisible, isEditable: $isEditable) {
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_add_user_attribute()
|
||||||
|
.with(eq(CreateAttributeRequest {
|
||||||
|
name: AttributeName::new("AttrName0"),
|
||||||
|
attribute_type: AttributeType::String,
|
||||||
|
is_list: false,
|
||||||
|
is_visible: false,
|
||||||
|
is_editable: false,
|
||||||
|
}))
|
||||||
|
.return_once(|_| Ok(()));
|
||||||
|
let context = Context::<MockTestBackendHandler>::new_for_tests(
|
||||||
|
mock,
|
||||||
|
ValidationResults {
|
||||||
|
user: UserId::new("bob"),
|
||||||
|
permission: Permission::Admin,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let vars = Variables::from([
|
||||||
|
("name".to_string(), InputValue::scalar("AttrName0")),
|
||||||
|
(
|
||||||
|
"attributeType".to_string(),
|
||||||
|
InputValue::enum_value("STRING"),
|
||||||
|
),
|
||||||
|
("isList".to_string(), InputValue::scalar(false)),
|
||||||
|
("isVisible".to_string(), InputValue::scalar(false)),
|
||||||
|
("isEditable".to_string(), InputValue::scalar(false)),
|
||||||
|
]);
|
||||||
|
let schema = mutation_schema(
|
||||||
|
Query::<MockTestBackendHandler>::new(),
|
||||||
|
Mutation::<MockTestBackendHandler>::new(),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
execute(QUERY, None, &schema, &vars, &context).await,
|
||||||
|
Ok((
|
||||||
|
graphql_value!(
|
||||||
|
{
|
||||||
|
"addUserAttribute": {
|
||||||
|
"ok": true
|
||||||
|
}
|
||||||
|
} ),
|
||||||
|
vec![]
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_user_attribute_invalid() {
|
||||||
|
const QUERY: &str = r#"
|
||||||
|
mutation CreateUserAttribute($name: String!, $attributeType: AttributeType!, $isList: Boolean!, $isVisible: Boolean!, $isEditable: Boolean!) {
|
||||||
|
addUserAttribute(name: $name, attributeType: $attributeType, isList: $isList, isVisible: $isVisible, isEditable: $isEditable) {
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let mock = MockTestBackendHandler::new();
|
||||||
|
let context = Context::<MockTestBackendHandler>::new_for_tests(
|
||||||
|
mock,
|
||||||
|
ValidationResults {
|
||||||
|
user: UserId::new("bob"),
|
||||||
|
permission: Permission::Admin,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let vars = Variables::from([
|
||||||
|
("name".to_string(), InputValue::scalar("AttrName_0")),
|
||||||
|
(
|
||||||
|
"attributeType".to_string(),
|
||||||
|
InputValue::enum_value("STRING"),
|
||||||
|
),
|
||||||
|
("isList".to_string(), InputValue::scalar(false)),
|
||||||
|
("isVisible".to_string(), InputValue::scalar(false)),
|
||||||
|
("isEditable".to_string(), InputValue::scalar(false)),
|
||||||
|
]);
|
||||||
|
let schema = mutation_schema(
|
||||||
|
Query::<MockTestBackendHandler>::new(),
|
||||||
|
Mutation::<MockTestBackendHandler>::new(),
|
||||||
|
);
|
||||||
|
let result = execute(QUERY, None, &schema, &vars, &context).await;
|
||||||
|
match result {
|
||||||
|
Ok(res) => {
|
||||||
|
let (response, errors) = res;
|
||||||
|
assert!(response.is_null());
|
||||||
|
let expected_error_msg =
|
||||||
|
"Cannot create attribute with invalid name. Valid characters: a-z, A-Z, 0-9, and dash (-). Invalid chars found: _"
|
||||||
|
.to_string();
|
||||||
|
assert!(
|
||||||
|
errors
|
||||||
|
.iter()
|
||||||
|
.all(|e| e.error().message() == expected_error_msg)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_group_attribute_valid() {
|
||||||
|
const QUERY: &str = r#"
|
||||||
|
mutation CreateGroupAttribute($name: String!, $attributeType: AttributeType!, $isList: Boolean!, $isVisible: Boolean!) {
|
||||||
|
addGroupAttribute(name: $name, attributeType: $attributeType, isList: $isList, isVisible: $isVisible, isEditable: false) {
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_add_group_attribute()
|
||||||
|
.with(eq(CreateAttributeRequest {
|
||||||
|
name: AttributeName::new("AttrName0"),
|
||||||
|
attribute_type: AttributeType::String,
|
||||||
|
is_list: false,
|
||||||
|
is_visible: false,
|
||||||
|
is_editable: false,
|
||||||
|
}))
|
||||||
|
.return_once(|_| Ok(()));
|
||||||
|
let context = Context::<MockTestBackendHandler>::new_for_tests(
|
||||||
|
mock,
|
||||||
|
ValidationResults {
|
||||||
|
user: UserId::new("bob"),
|
||||||
|
permission: Permission::Admin,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let vars = Variables::from([
|
||||||
|
("name".to_string(), InputValue::scalar("AttrName0")),
|
||||||
|
(
|
||||||
|
"attributeType".to_string(),
|
||||||
|
InputValue::enum_value("STRING"),
|
||||||
|
),
|
||||||
|
("isList".to_string(), InputValue::scalar(false)),
|
||||||
|
("isVisible".to_string(), InputValue::scalar(false)),
|
||||||
|
("isEditable".to_string(), InputValue::scalar(false)),
|
||||||
|
]);
|
||||||
|
let schema = mutation_schema(
|
||||||
|
Query::<MockTestBackendHandler>::new(),
|
||||||
|
Mutation::<MockTestBackendHandler>::new(),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
execute(QUERY, None, &schema, &vars, &context).await,
|
||||||
|
Ok((
|
||||||
|
graphql_value!(
|
||||||
|
{
|
||||||
|
"addGroupAttribute": {
|
||||||
|
"ok": true
|
||||||
|
}
|
||||||
|
} ),
|
||||||
|
vec![]
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_group_attribute_invalid() {
|
||||||
|
const QUERY: &str = r#"
|
||||||
|
mutation CreateUserAttribute($name: String!, $attributeType: AttributeType!, $isList: Boolean!, $isVisible: Boolean!, $isEditable: Boolean!) {
|
||||||
|
addUserAttribute(name: $name, attributeType: $attributeType, isList: $isList, isVisible: $isVisible, isEditable: $isEditable) {
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let mock = MockTestBackendHandler::new();
|
||||||
|
let context = Context::<MockTestBackendHandler>::new_for_tests(
|
||||||
|
mock,
|
||||||
|
ValidationResults {
|
||||||
|
user: UserId::new("bob"),
|
||||||
|
permission: Permission::Admin,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let vars = Variables::from([
|
||||||
|
("name".to_string(), InputValue::scalar("AttrName_0")),
|
||||||
|
(
|
||||||
|
"attributeType".to_string(),
|
||||||
|
InputValue::enum_value("STRING"),
|
||||||
|
),
|
||||||
|
("isList".to_string(), InputValue::scalar(false)),
|
||||||
|
("isVisible".to_string(), InputValue::scalar(false)),
|
||||||
|
("isEditable".to_string(), InputValue::scalar(false)),
|
||||||
|
]);
|
||||||
|
let schema = mutation_schema(
|
||||||
|
Query::<MockTestBackendHandler>::new(),
|
||||||
|
Mutation::<MockTestBackendHandler>::new(),
|
||||||
|
);
|
||||||
|
let result = execute(QUERY, None, &schema, &vars, &context).await;
|
||||||
|
match result {
|
||||||
|
Ok(res) => {
|
||||||
|
let (response, errors) = res;
|
||||||
|
assert!(response.is_null());
|
||||||
|
let expected_error_msg =
|
||||||
|
"Cannot create attribute with invalid name. Valid characters: a-z, A-Z, 0-9, and dash (-). Invalid chars found: _"
|
||||||
|
.to_string();
|
||||||
|
assert!(
|
||||||
|
errors
|
||||||
|
.iter()
|
||||||
|
.all(|e| e.error().message() == expected_error_msg)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_attribute_consolidation_attr_precedence() {
|
||||||
|
let attributes = vec![
|
||||||
|
AttributeValue {
|
||||||
|
name: "first_name".to_string(),
|
||||||
|
value: vec!["expected-first".to_string()],
|
||||||
|
},
|
||||||
|
AttributeValue {
|
||||||
|
name: "last_name".to_string(),
|
||||||
|
value: vec!["expected-last".to_string()],
|
||||||
|
},
|
||||||
|
AttributeValue {
|
||||||
|
name: "avatar".to_string(),
|
||||||
|
value: vec!["expected-avatar".to_string()],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let res = consolidate_attributes(
|
||||||
|
attributes.clone(),
|
||||||
|
Some("overridden-first".to_string()),
|
||||||
|
Some("overridden-last".to_string()),
|
||||||
|
Some("overriden-avatar".to_string()),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
res,
|
||||||
|
vec![
|
||||||
|
AttributeValue {
|
||||||
|
name: "avatar".to_string(),
|
||||||
|
value: vec!["expected-avatar".to_string()],
|
||||||
|
},
|
||||||
|
AttributeValue {
|
||||||
|
name: "first_name".to_string(),
|
||||||
|
value: vec!["expected-first".to_string()],
|
||||||
|
},
|
||||||
|
AttributeValue {
|
||||||
|
name: "last_name".to_string(),
|
||||||
|
value: vec!["expected-last".to_string()],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_attribute_consolidation_field_fallback() {
|
||||||
|
let attributes = Vec::new();
|
||||||
|
let res = consolidate_attributes(
|
||||||
|
attributes.clone(),
|
||||||
|
Some("expected-first".to_string()),
|
||||||
|
Some("expected-last".to_string()),
|
||||||
|
Some("expected-avatar".to_string()),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
res,
|
||||||
|
vec![
|
||||||
|
AttributeValue {
|
||||||
|
name: "avatar".to_string(),
|
||||||
|
value: vec!["expected-avatar".to_string()],
|
||||||
|
},
|
||||||
|
AttributeValue {
|
||||||
|
name: "first_name".to_string(),
|
||||||
|
value: vec!["expected-first".to_string()],
|
||||||
|
},
|
||||||
|
AttributeValue {
|
||||||
|
name: "last_name".to_string(),
|
||||||
|
value: vec!["expected-last".to_string()],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_attribute_consolidation_field_fallback_2() {
|
||||||
|
let attributes = vec![AttributeValue {
|
||||||
|
name: "First_Name".to_string(),
|
||||||
|
value: vec!["expected-first".to_string()],
|
||||||
|
}];
|
||||||
|
let res = consolidate_attributes(
|
||||||
|
attributes.clone(),
|
||||||
|
Some("overriden-first".to_string()),
|
||||||
|
Some("expected-last".to_string()),
|
||||||
|
Some("expected-avatar".to_string()),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
res,
|
||||||
|
vec![
|
||||||
|
AttributeValue {
|
||||||
|
name: "avatar".to_string(),
|
||||||
|
value: vec!["expected-avatar".to_string()],
|
||||||
|
},
|
||||||
|
AttributeValue {
|
||||||
|
name: "first_name".to_string(),
|
||||||
|
value: vec!["expected-first".to_string()],
|
||||||
|
},
|
||||||
|
AttributeValue {
|
||||||
|
name: "last_name".to_string(),
|
||||||
|
value: vec!["expected-last".to_string()],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +1,31 @@
|
|||||||
use std::sync::Arc;
|
use crate::api::{Context, field_error_callback};
|
||||||
|
|
||||||
use crate::{
|
|
||||||
domain::{
|
|
||||||
deserialize::deserialize_attribute_value,
|
|
||||||
handler::{BackendHandler, ReadSchemaBackendHandler},
|
|
||||||
ldap::utils::{map_user_field, UserFieldType},
|
|
||||||
model::UserColumn,
|
|
||||||
schema::PublicSchema,
|
|
||||||
types::{
|
|
||||||
AttributeType, GroupDetails, GroupId, JpegPhoto, LdapObjectClass, Serialized, UserId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
infra::{
|
|
||||||
access_control::{ReadonlyBackendHandler, UserReadableBackendHandler},
|
|
||||||
graphql::api::{field_error_callback, Context},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use anyhow::Context as AnyhowContext;
|
use anyhow::Context as AnyhowContext;
|
||||||
use chrono::{NaiveDateTime, TimeZone};
|
use chrono::TimeZone;
|
||||||
use juniper::{graphql_object, FieldResult, GraphQLInputObject};
|
use juniper::{FieldResult, GraphQLInputObject, graphql_object};
|
||||||
|
use lldap_access_control::{ReadonlyBackendHandler, UserReadableBackendHandler};
|
||||||
|
use lldap_domain::{
|
||||||
|
deserialize::deserialize_attribute_value,
|
||||||
|
public_schema::PublicSchema,
|
||||||
|
types::{AttributeType, Cardinality, GroupDetails, GroupId, LdapObjectClass, UserId},
|
||||||
|
};
|
||||||
|
use lldap_domain_handlers::handler::{BackendHandler, ReadSchemaBackendHandler};
|
||||||
|
use lldap_domain_model::model::UserColumn;
|
||||||
|
use lldap_ldap::{
|
||||||
|
UserFieldType, get_default_group_object_classes, get_default_user_object_classes,
|
||||||
|
map_user_field,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::{debug, debug_span, Instrument, Span};
|
use std::sync::Arc;
|
||||||
|
use tracing::{Instrument, Span, debug, debug_span};
|
||||||
|
|
||||||
type DomainRequestFilter = crate::domain::handler::UserRequestFilter;
|
type DomainRequestFilter = lldap_domain_handlers::handler::UserRequestFilter;
|
||||||
type DomainUser = crate::domain::types::User;
|
type DomainUser = lldap_domain::types::User;
|
||||||
type DomainGroup = crate::domain::types::Group;
|
type DomainGroup = lldap_domain::types::Group;
|
||||||
type DomainUserAndGroups = crate::domain::types::UserAndGroups;
|
type DomainUserAndGroups = lldap_domain::types::UserAndGroups;
|
||||||
type DomainAttributeList = crate::domain::handler::AttributeList;
|
type DomainAttributeList = lldap_domain::schema::AttributeList;
|
||||||
type DomainAttributeSchema = crate::domain::handler::AttributeSchema;
|
type DomainAttributeSchema = lldap_domain::schema::AttributeSchema;
|
||||||
type DomainAttributeValue = crate::domain::types::AttributeValue;
|
type DomainAttribute = lldap_domain::types::Attribute;
|
||||||
|
type DomainAttributeValue = lldap_domain::types::AttributeValue;
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, GraphQLInputObject)]
|
#[derive(PartialEq, Eq, Debug, GraphQLInputObject)]
|
||||||
/// A filter for requests, specifying a boolean expression based on field constraints. Only one of
|
/// A filter for requests, specifying a boolean expression based on field constraints. Only one of
|
||||||
@@ -116,6 +113,12 @@ pub struct Query<Handler: BackendHandler> {
|
|||||||
_phantom: std::marker::PhantomData<Box<Handler>>,
|
_phantom: std::marker::PhantomData<Box<Handler>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<Handler: BackendHandler> Default for Query<Handler> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<Handler: BackendHandler> Query<Handler> {
|
impl<Handler: BackendHandler> Query<Handler> {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -296,23 +299,30 @@ impl<Handler: BackendHandler> User<Handler> {
|
|||||||
self.attributes
|
self.attributes
|
||||||
.iter()
|
.iter()
|
||||||
.find(|a| a.attribute.name.as_str() == "first_name")
|
.find(|a| a.attribute.name.as_str() == "first_name")
|
||||||
.map(|a| a.attribute.value.unwrap())
|
.map(|a| a.attribute.value.as_str().unwrap_or_default())
|
||||||
.unwrap_or("")
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn last_name(&self) -> &str {
|
fn last_name(&self) -> &str {
|
||||||
self.attributes
|
self.attributes
|
||||||
.iter()
|
.iter()
|
||||||
.find(|a| a.attribute.name.as_str() == "last_name")
|
.find(|a| a.attribute.name.as_str() == "last_name")
|
||||||
.map(|a| a.attribute.value.unwrap())
|
.map(|a| a.attribute.value.as_str().unwrap_or_default())
|
||||||
.unwrap_or("")
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn avatar(&self) -> Option<String> {
|
fn avatar(&self) -> Option<String> {
|
||||||
self.attributes
|
self.attributes
|
||||||
.iter()
|
.iter()
|
||||||
.find(|a| a.attribute.name.as_str() == "avatar")
|
.find(|a| a.attribute.name.as_str() == "avatar")
|
||||||
.map(|a| String::from(&a.attribute.value.unwrap::<JpegPhoto>()))
|
.map(|a| {
|
||||||
|
String::from(
|
||||||
|
a.attribute
|
||||||
|
.value
|
||||||
|
.as_jpeg_photo()
|
||||||
|
.expect("Invalid JPEG returned by the DB"),
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn creation_date(&self) -> chrono::DateTime<chrono::Utc> {
|
fn creation_date(&self) -> chrono::DateTime<chrono::Utc> {
|
||||||
@@ -515,10 +525,28 @@ impl<Handler: BackendHandler> From<DomainAttributeSchema> for AttributeSchema<Ha
|
|||||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||||
pub struct AttributeList<Handler: BackendHandler> {
|
pub struct AttributeList<Handler: BackendHandler> {
|
||||||
attributes: DomainAttributeList,
|
attributes: DomainAttributeList,
|
||||||
|
default_classes: Vec<LdapObjectClass>,
|
||||||
extra_classes: Vec<LdapObjectClass>,
|
extra_classes: Vec<LdapObjectClass>,
|
||||||
_phantom: std::marker::PhantomData<Box<Handler>>,
|
_phantom: std::marker::PhantomData<Box<Handler>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ObjectClassInfo {
|
||||||
|
object_class: String,
|
||||||
|
is_hardcoded: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
impl ObjectClassInfo {
|
||||||
|
fn object_class(&self) -> &str {
|
||||||
|
&self.object_class
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_hardcoded(&self) -> bool {
|
||||||
|
self.is_hardcoded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[graphql_object(context = Context<Handler>)]
|
#[graphql_object(context = Context<Handler>)]
|
||||||
impl<Handler: BackendHandler> AttributeList<Handler> {
|
impl<Handler: BackendHandler> AttributeList<Handler> {
|
||||||
fn attributes(&self) -> Vec<AttributeSchema<Handler>> {
|
fn attributes(&self) -> Vec<AttributeSchema<Handler>> {
|
||||||
@@ -533,12 +561,35 @@ impl<Handler: BackendHandler> AttributeList<Handler> {
|
|||||||
fn extra_ldap_object_classes(&self) -> Vec<String> {
|
fn extra_ldap_object_classes(&self) -> Vec<String> {
|
||||||
self.extra_classes.iter().map(|c| c.to_string()).collect()
|
self.extra_classes.iter().map(|c| c.to_string()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ldap_object_classes(&self) -> Vec<ObjectClassInfo> {
|
||||||
|
let mut all_object_classes: Vec<ObjectClassInfo> = self
|
||||||
|
.default_classes
|
||||||
|
.iter()
|
||||||
|
.map(|c| ObjectClassInfo {
|
||||||
|
object_class: c.to_string(),
|
||||||
|
is_hardcoded: true,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
all_object_classes.extend(self.extra_classes.iter().map(|c| ObjectClassInfo {
|
||||||
|
object_class: c.to_string(),
|
||||||
|
is_hardcoded: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
all_object_classes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Handler: BackendHandler> AttributeList<Handler> {
|
impl<Handler: BackendHandler> AttributeList<Handler> {
|
||||||
fn new(attributes: DomainAttributeList, extra_classes: Vec<LdapObjectClass>) -> Self {
|
fn new(
|
||||||
|
attributes: DomainAttributeList,
|
||||||
|
default_classes: Vec<LdapObjectClass>,
|
||||||
|
extra_classes: Vec<LdapObjectClass>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
attributes,
|
attributes,
|
||||||
|
default_classes,
|
||||||
extra_classes,
|
extra_classes,
|
||||||
_phantom: std::marker::PhantomData,
|
_phantom: std::marker::PhantomData,
|
||||||
}
|
}
|
||||||
@@ -556,12 +607,14 @@ impl<Handler: BackendHandler> Schema<Handler> {
|
|||||||
fn user_schema(&self) -> AttributeList<Handler> {
|
fn user_schema(&self) -> AttributeList<Handler> {
|
||||||
AttributeList::<Handler>::new(
|
AttributeList::<Handler>::new(
|
||||||
self.schema.get_schema().user_attributes.clone(),
|
self.schema.get_schema().user_attributes.clone(),
|
||||||
|
get_default_user_object_classes(),
|
||||||
self.schema.get_schema().extra_user_object_classes.clone(),
|
self.schema.get_schema().extra_user_object_classes.clone(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
fn group_schema(&self) -> AttributeList<Handler> {
|
fn group_schema(&self) -> AttributeList<Handler> {
|
||||||
AttributeList::<Handler>::new(
|
AttributeList::<Handler>::new(
|
||||||
self.schema.get_schema().group_attributes.clone(),
|
self.schema.get_schema().group_attributes.clone(),
|
||||||
|
get_default_group_object_classes(),
|
||||||
self.schema.get_schema().extra_group_object_classes.clone(),
|
self.schema.get_schema().extra_group_object_classes.clone(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -578,7 +631,7 @@ impl<Handler: BackendHandler> From<PublicSchema> for Schema<Handler> {
|
|||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||||
pub struct AttributeValue<Handler: BackendHandler> {
|
pub struct AttributeValue<Handler: BackendHandler> {
|
||||||
attribute: DomainAttributeValue,
|
attribute: DomainAttribute,
|
||||||
schema: AttributeSchema<Handler>,
|
schema: AttributeSchema<Handler>,
|
||||||
_phantom: std::marker::PhantomData<Box<Handler>>,
|
_phantom: std::marker::PhantomData<Box<Handler>>,
|
||||||
}
|
}
|
||||||
@@ -590,7 +643,7 @@ impl<Handler: BackendHandler> AttributeValue<Handler> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn value(&self) -> FieldResult<Vec<String>> {
|
fn value(&self) -> FieldResult<Vec<String>> {
|
||||||
Ok(serialize_attribute(&self.attribute, &self.schema.schema))
|
Ok(serialize_attribute_to_graphql(&self.attribute.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn schema(&self) -> &AttributeSchema<Handler> {
|
fn schema(&self) -> &AttributeSchema<Handler> {
|
||||||
@@ -599,9 +652,9 @@ impl<Handler: BackendHandler> AttributeValue<Handler> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<Handler: BackendHandler> AttributeValue<Handler> {
|
impl<Handler: BackendHandler> AttributeValue<Handler> {
|
||||||
fn from_domain(value: DomainAttributeValue, schema: DomainAttributeSchema) -> Self {
|
fn from_value(attr: DomainAttribute, schema: DomainAttributeSchema) -> Self {
|
||||||
Self {
|
Self {
|
||||||
attribute: value,
|
attribute: attr,
|
||||||
schema: AttributeSchema::<Handler> {
|
schema: AttributeSchema::<Handler> {
|
||||||
schema,
|
schema,
|
||||||
_phantom: std::marker::PhantomData,
|
_phantom: std::marker::PhantomData,
|
||||||
@@ -621,54 +674,31 @@ impl<Handler: BackendHandler> Clone for AttributeValue<Handler> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serialize_attribute(
|
pub fn serialize_attribute_to_graphql(attribute_value: &DomainAttributeValue) -> Vec<String> {
|
||||||
attribute: &DomainAttributeValue,
|
let convert_date = |&date| chrono::Utc.from_utc_datetime(&date).to_rfc3339();
|
||||||
attribute_schema: &DomainAttributeSchema,
|
match attribute_value {
|
||||||
) -> Vec<String> {
|
DomainAttributeValue::String(Cardinality::Singleton(s)) => vec![s.clone()],
|
||||||
let convert_date = |date| chrono::Utc.from_utc_datetime(&date).to_rfc3339();
|
DomainAttributeValue::String(Cardinality::Unbounded(l)) => l.clone(),
|
||||||
match (attribute_schema.attribute_type, attribute_schema.is_list) {
|
DomainAttributeValue::Integer(Cardinality::Singleton(i)) => vec![i.to_string()],
|
||||||
(AttributeType::String, false) => vec![attribute.value.unwrap::<String>()],
|
DomainAttributeValue::Integer(Cardinality::Unbounded(l)) => {
|
||||||
(AttributeType::Integer, false) => {
|
l.iter().map(|i| i.to_string()).collect()
|
||||||
// LDAP integers are encoded as strings.
|
|
||||||
vec![attribute.value.unwrap::<i64>().to_string()]
|
|
||||||
}
|
}
|
||||||
(AttributeType::JpegPhoto, false) => {
|
DomainAttributeValue::DateTime(Cardinality::Singleton(dt)) => vec![convert_date(dt)],
|
||||||
vec![String::from(&attribute.value.unwrap::<JpegPhoto>())]
|
DomainAttributeValue::DateTime(Cardinality::Unbounded(l)) => {
|
||||||
|
l.iter().map(convert_date).collect()
|
||||||
}
|
}
|
||||||
(AttributeType::DateTime, false) => {
|
DomainAttributeValue::JpegPhoto(Cardinality::Singleton(p)) => vec![String::from(p)],
|
||||||
vec![convert_date(attribute.value.unwrap::<NaiveDateTime>())]
|
DomainAttributeValue::JpegPhoto(Cardinality::Unbounded(l)) => {
|
||||||
|
l.iter().map(String::from).collect()
|
||||||
}
|
}
|
||||||
(AttributeType::String, true) => attribute
|
|
||||||
.value
|
|
||||||
.unwrap::<Vec<String>>()
|
|
||||||
.into_iter()
|
|
||||||
.collect(),
|
|
||||||
(AttributeType::Integer, true) => attribute
|
|
||||||
.value
|
|
||||||
.unwrap::<Vec<i64>>()
|
|
||||||
.into_iter()
|
|
||||||
.map(|i| i.to_string())
|
|
||||||
.collect(),
|
|
||||||
(AttributeType::JpegPhoto, true) => attribute
|
|
||||||
.value
|
|
||||||
.unwrap::<Vec<JpegPhoto>>()
|
|
||||||
.iter()
|
|
||||||
.map(String::from)
|
|
||||||
.collect(),
|
|
||||||
(AttributeType::DateTime, true) => attribute
|
|
||||||
.value
|
|
||||||
.unwrap::<Vec<NaiveDateTime>>()
|
|
||||||
.into_iter()
|
|
||||||
.map(convert_date)
|
|
||||||
.collect(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Handler: BackendHandler> AttributeValue<Handler> {
|
impl<Handler: BackendHandler> AttributeValue<Handler> {
|
||||||
fn from_schema(a: DomainAttributeValue, schema: &DomainAttributeList) -> Option<Self> {
|
fn from_schema(a: DomainAttribute, schema: &DomainAttributeList) -> Option<Self> {
|
||||||
schema
|
schema
|
||||||
.get_attribute_schema(&a.name)
|
.get_attribute_schema(&a.name)
|
||||||
.map(|s| AttributeValue::<Handler>::from_domain(a, s.clone()))
|
.map(|s| AttributeValue::<Handler>::from_value(a, s.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn user_attributes_from_schema(
|
fn user_attributes_from_schema(
|
||||||
@@ -682,25 +712,25 @@ impl<Handler: BackendHandler> AttributeValue<Handler> {
|
|||||||
.attributes
|
.attributes
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|a| a.is_hardcoded)
|
.filter(|a| a.is_hardcoded)
|
||||||
.flat_map(|attribute| {
|
.flat_map(|attribute_schema| {
|
||||||
let value = match attribute.name.as_str() {
|
let value: Option<DomainAttributeValue> = match attribute_schema.name.as_str() {
|
||||||
"user_id" => Some(Serialized::from(&user.user_id)),
|
"user_id" => Some(user.user_id.clone().into_string().into()),
|
||||||
"creation_date" => Some(Serialized::from(&user.creation_date)),
|
"creation_date" => Some(user.creation_date.into()),
|
||||||
"mail" => Some(Serialized::from(&user.email)),
|
"mail" => Some(user.email.clone().into_string().into()),
|
||||||
"uuid" => Some(Serialized::from(&user.uuid)),
|
"uuid" => Some(user.uuid.clone().into_string().into()),
|
||||||
"display_name" => user.display_name.as_ref().map(Serialized::from),
|
"display_name" => user.display_name.as_ref().map(|d| d.clone().into()),
|
||||||
"avatar" | "first_name" | "last_name" => None,
|
"avatar" | "first_name" | "last_name" => None,
|
||||||
_ => panic!("Unexpected hardcoded attribute: {}", attribute.name),
|
_ => panic!("Unexpected hardcoded attribute: {}", attribute_schema.name),
|
||||||
};
|
};
|
||||||
value.map(|v| (attribute, v))
|
value.map(|v| (attribute_schema, v))
|
||||||
})
|
})
|
||||||
.map(|(attribute, value)| {
|
.map(|(attribute_schema, value)| {
|
||||||
AttributeValue::<Handler>::from_domain(
|
AttributeValue::<Handler>::from_value(
|
||||||
DomainAttributeValue {
|
DomainAttribute {
|
||||||
name: attribute.name.clone(),
|
name: attribute_schema.name.clone(),
|
||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
attribute.clone(),
|
attribute_schema.clone(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -724,25 +754,25 @@ impl<Handler: BackendHandler> AttributeValue<Handler> {
|
|||||||
.attributes
|
.attributes
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|a| a.is_hardcoded)
|
.filter(|a| a.is_hardcoded)
|
||||||
.map(|attribute| {
|
.map(|attribute_schema| {
|
||||||
(
|
(
|
||||||
attribute,
|
attribute_schema,
|
||||||
match attribute.name.as_str() {
|
match attribute_schema.name.as_str() {
|
||||||
"group_id" => Serialized::from(&(group.id.0 as i64)),
|
"group_id" => (group.id.0 as i64).into(),
|
||||||
"creation_date" => Serialized::from(&group.creation_date),
|
"creation_date" => group.creation_date.into(),
|
||||||
"uuid" => Serialized::from(&group.uuid),
|
"uuid" => group.uuid.clone().into_string().into(),
|
||||||
"display_name" => Serialized::from(&group.display_name),
|
"display_name" => group.display_name.clone().into_string().into(),
|
||||||
_ => panic!("Unexpected hardcoded attribute: {}", attribute.name),
|
_ => panic!("Unexpected hardcoded attribute: {}", attribute_schema.name),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.map(|(attribute, value)| {
|
.map(|(attribute_schema, value)| {
|
||||||
AttributeValue::<Handler>::from_domain(
|
AttributeValue::<Handler>::from_value(
|
||||||
DomainAttributeValue {
|
DomainAttribute {
|
||||||
name: attribute.name.clone(),
|
name: attribute_schema.name.clone(),
|
||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
attribute.clone(),
|
attribute_schema.clone(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -766,25 +796,25 @@ impl<Handler: BackendHandler> AttributeValue<Handler> {
|
|||||||
.attributes
|
.attributes
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|a| a.is_hardcoded)
|
.filter(|a| a.is_hardcoded)
|
||||||
.map(|attribute| {
|
.map(|attribute_schema| {
|
||||||
(
|
(
|
||||||
attribute,
|
attribute_schema,
|
||||||
match attribute.name.as_str() {
|
match attribute_schema.name.as_str() {
|
||||||
"group_id" => Serialized::from(&(group.group_id.0 as i64)),
|
"group_id" => (group.group_id.0 as i64).into(),
|
||||||
"creation_date" => Serialized::from(&group.creation_date),
|
"creation_date" => group.creation_date.into(),
|
||||||
"uuid" => Serialized::from(&group.uuid),
|
"uuid" => group.uuid.clone().into_string().into(),
|
||||||
"display_name" => Serialized::from(&group.display_name),
|
"display_name" => group.display_name.clone().into_string().into(),
|
||||||
_ => panic!("Unexpected hardcoded attribute: {}", attribute.name),
|
_ => panic!("Unexpected hardcoded attribute: {}", attribute_schema.name),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.map(|(attribute, value)| {
|
.map(|(attribute_schema, value)| {
|
||||||
AttributeValue::<Handler>::from_domain(
|
AttributeValue::<Handler>::from_value(
|
||||||
DomainAttributeValue {
|
DomainAttribute {
|
||||||
name: attribute.name.clone(),
|
name: attribute_schema.name.clone(),
|
||||||
value,
|
value,
|
||||||
},
|
},
|
||||||
attribute.clone(),
|
attribute_schema.clone(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -801,21 +831,17 @@ impl<Handler: BackendHandler> AttributeValue<Handler> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
|
||||||
domain::{
|
|
||||||
handler::AttributeList,
|
|
||||||
types::{AttributeName, AttributeType, LdapObjectClass, Serialized},
|
|
||||||
},
|
|
||||||
infra::{
|
|
||||||
access_control::{Permission, ValidationResults},
|
|
||||||
test_utils::{setup_default_schema, MockTestBackendHandler},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use chrono::TimeZone;
|
use chrono::TimeZone;
|
||||||
use juniper::{
|
use juniper::{
|
||||||
execute, graphql_value, DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLType,
|
DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLType, RootNode, Variables,
|
||||||
RootNode, Variables,
|
execute, graphql_value,
|
||||||
};
|
};
|
||||||
|
use lldap_auth::access_control::{Permission, ValidationResults};
|
||||||
|
use lldap_domain::{
|
||||||
|
schema::{AttributeList, Schema},
|
||||||
|
types::{AttributeName, AttributeType, LdapObjectClass},
|
||||||
|
};
|
||||||
|
use lldap_test_utils::{MockTestBackendHandler, setup_default_schema};
|
||||||
use mockall::predicate::eq;
|
use mockall::predicate::eq;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
@@ -860,7 +886,7 @@ mod tests {
|
|||||||
|
|
||||||
let mut mock = MockTestBackendHandler::new();
|
let mut mock = MockTestBackendHandler::new();
|
||||||
mock.expect_get_schema().returning(|| {
|
mock.expect_get_schema().returning(|| {
|
||||||
Ok(crate::domain::handler::Schema {
|
Ok(Schema {
|
||||||
user_attributes: DomainAttributeList {
|
user_attributes: DomainAttributeList {
|
||||||
attributes: vec![
|
attributes: vec![
|
||||||
DomainAttributeSchema {
|
DomainAttributeSchema {
|
||||||
@@ -908,15 +934,15 @@ mod tests {
|
|||||||
user_id: UserId::new("bob"),
|
user_id: UserId::new("bob"),
|
||||||
email: "bob@bobbers.on".into(),
|
email: "bob@bobbers.on".into(),
|
||||||
creation_date: chrono::Utc.timestamp_millis_opt(42).unwrap().naive_utc(),
|
creation_date: chrono::Utc.timestamp_millis_opt(42).unwrap().naive_utc(),
|
||||||
uuid: crate::uuid!("b1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
|
uuid: lldap_domain::uuid!("b1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
|
||||||
attributes: vec![
|
attributes: vec![
|
||||||
DomainAttributeValue {
|
DomainAttribute {
|
||||||
name: "first_name".into(),
|
name: "first_name".into(),
|
||||||
value: Serialized::from("Bob"),
|
value: "Bob".to_string().into(),
|
||||||
},
|
},
|
||||||
DomainAttributeValue {
|
DomainAttribute {
|
||||||
name: "last_name".into(),
|
name: "last_name".into(),
|
||||||
value: Serialized::from("Bobberson"),
|
value: "Bobberson".to_string().into(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -927,17 +953,17 @@ mod tests {
|
|||||||
group_id: GroupId(3),
|
group_id: GroupId(3),
|
||||||
display_name: "Bobbersons".into(),
|
display_name: "Bobbersons".into(),
|
||||||
creation_date: chrono::Utc.timestamp_nanos(42).naive_utc(),
|
creation_date: chrono::Utc.timestamp_nanos(42).naive_utc(),
|
||||||
uuid: crate::uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
|
uuid: lldap_domain::uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
|
||||||
attributes: vec![DomainAttributeValue {
|
attributes: vec![DomainAttribute {
|
||||||
name: "club_name".into(),
|
name: "club_name".into(),
|
||||||
value: Serialized::from("Gang of Four"),
|
value: "Gang of Four".to_string().into(),
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
groups.insert(GroupDetails {
|
groups.insert(GroupDetails {
|
||||||
group_id: GroupId(7),
|
group_id: GroupId(7),
|
||||||
display_name: "Jefferees".into(),
|
display_name: "Jefferees".into(),
|
||||||
creation_date: chrono::Utc.timestamp_nanos(12).naive_utc(),
|
creation_date: chrono::Utc.timestamp_nanos(12).naive_utc(),
|
||||||
uuid: crate::uuid!("b1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
|
uuid: lldap_domain::uuid!("b1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
|
||||||
attributes: Vec::new(),
|
attributes: Vec::new(),
|
||||||
});
|
});
|
||||||
mock.expect_get_user_groups()
|
mock.expect_get_user_groups()
|
||||||
@@ -1076,7 +1102,7 @@ mod tests {
|
|||||||
),
|
),
|
||||||
DomainRequestFilter::AttributeEquality(
|
DomainRequestFilter::AttributeEquality(
|
||||||
AttributeName::from("first_name"),
|
AttributeName::from("first_name"),
|
||||||
Serialized::from("robert"),
|
"robert".to_string().into(),
|
||||||
),
|
),
|
||||||
]))),
|
]))),
|
||||||
eq(false),
|
eq(false),
|
||||||
@@ -1299,7 +1325,7 @@ mod tests {
|
|||||||
let mut mock = MockTestBackendHandler::new();
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
|
||||||
mock.expect_get_schema().times(1).return_once(|| {
|
mock.expect_get_schema().times(1).return_once(|| {
|
||||||
Ok(crate::domain::handler::Schema {
|
Ok(Schema {
|
||||||
user_attributes: AttributeList {
|
user_attributes: AttributeList {
|
||||||
attributes: vec![DomainAttributeSchema {
|
attributes: vec![DomainAttributeSchema {
|
||||||
name: "invisible".into(),
|
name: "invisible".into(),
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
[package]
|
||||||
|
name = "lldap_ldap"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "LDAP operations support"
|
||||||
|
authors.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "*"
|
||||||
|
ldap3_proto = "0.6.0"
|
||||||
|
tracing = "*"
|
||||||
|
itertools = "0.10"
|
||||||
|
|
||||||
|
[dependencies.derive_more]
|
||||||
|
features = ["from"]
|
||||||
|
default-features = false
|
||||||
|
version = "1"
|
||||||
|
|
||||||
|
[dependencies.chrono]
|
||||||
|
features = ["serde"]
|
||||||
|
version = "*"
|
||||||
|
|
||||||
|
[dependencies.rand]
|
||||||
|
features = ["small_rng", "getrandom"]
|
||||||
|
version = "0.8"
|
||||||
|
|
||||||
|
[dependencies.uuid]
|
||||||
|
version = "1"
|
||||||
|
features = ["v1", "v3"]
|
||||||
|
|
||||||
|
[dependencies.lldap_access_control]
|
||||||
|
path = "../access-control"
|
||||||
|
|
||||||
|
[dependencies.lldap_auth]
|
||||||
|
path = "../auth"
|
||||||
|
features = ["opaque_server", "opaque_client", "sea_orm"]
|
||||||
|
|
||||||
|
[dependencies.lldap_domain]
|
||||||
|
path = "../domain"
|
||||||
|
|
||||||
|
[dependencies.lldap_domain_handlers]
|
||||||
|
path = "../domain-handlers"
|
||||||
|
|
||||||
|
[dependencies.lldap_domain_model]
|
||||||
|
path = "../domain-model"
|
||||||
|
|
||||||
|
[dependencies.lldap_opaque_handler]
|
||||||
|
path = "../opaque-handler"
|
||||||
|
|
||||||
|
[dev-dependencies.lldap_test_utils]
|
||||||
|
path = "../test-utils"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
mockall = "0.11.4"
|
||||||
|
pretty_assertions = "1"
|
||||||
|
|
||||||
|
[dev-dependencies.tokio]
|
||||||
|
features = ["full"]
|
||||||
|
version = "1.25"
|
||||||
|
|
||||||
|
[dev-dependencies.lldap_domain]
|
||||||
|
path = "../domain"
|
||||||
|
features = ["test"]
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
use crate::core::error::{LdapError, LdapResult};
|
||||||
|
use ldap3_proto::proto::{LdapCompareRequest, LdapOp, LdapResult as LdapResultOp, LdapResultCode};
|
||||||
|
use lldap_domain::types::AttributeName;
|
||||||
|
|
||||||
|
pub fn compare(
|
||||||
|
request: LdapCompareRequest,
|
||||||
|
search_results: Vec<LdapOp>,
|
||||||
|
base_dn_str: &str,
|
||||||
|
) -> LdapResult<Vec<LdapOp>> {
|
||||||
|
if search_results.len() > 2 {
|
||||||
|
// SearchResultEntry + SearchResultDone
|
||||||
|
return Err(LdapError {
|
||||||
|
code: LdapResultCode::OperationsError,
|
||||||
|
message: "Too many search results".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let requested_attribute = AttributeName::from(&request.atype);
|
||||||
|
match search_results.first() {
|
||||||
|
Some(LdapOp::SearchResultEntry(entry)) => {
|
||||||
|
let available = entry.attributes.iter().any(|attr| {
|
||||||
|
AttributeName::from(&attr.atype) == requested_attribute
|
||||||
|
&& attr.vals.contains(&request.val)
|
||||||
|
});
|
||||||
|
Ok(vec![LdapOp::CompareResult(LdapResultOp {
|
||||||
|
code: if available {
|
||||||
|
LdapResultCode::CompareTrue
|
||||||
|
} else {
|
||||||
|
LdapResultCode::CompareFalse
|
||||||
|
},
|
||||||
|
matcheddn: request.dn,
|
||||||
|
message: "".to_string(),
|
||||||
|
referral: vec![],
|
||||||
|
})])
|
||||||
|
}
|
||||||
|
Some(LdapOp::SearchResultDone(_)) => Ok(vec![LdapOp::CompareResult(LdapResultOp {
|
||||||
|
code: LdapResultCode::NoSuchObject,
|
||||||
|
matcheddn: base_dn_str.to_string(),
|
||||||
|
message: "".to_string(),
|
||||||
|
referral: vec![],
|
||||||
|
})]),
|
||||||
|
None => Err(LdapError {
|
||||||
|
code: LdapResultCode::OperationsError,
|
||||||
|
message: "Search request returned nothing".to_string(),
|
||||||
|
}),
|
||||||
|
_ => Err(LdapError {
|
||||||
|
code: LdapResultCode::OperationsError,
|
||||||
|
message: "Unexpected results from search".to_string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::handler::tests::setup_bound_admin_handler;
|
||||||
|
use chrono::TimeZone;
|
||||||
|
use lldap_domain::{
|
||||||
|
types::{Group, GroupId, User, UserAndGroups, UserId},
|
||||||
|
uuid,
|
||||||
|
};
|
||||||
|
use lldap_domain_handlers::handler::{GroupRequestFilter, UserRequestFilter};
|
||||||
|
use lldap_test_utils::MockTestBackendHandler;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_compare_user() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_list_users().returning(|f, g| {
|
||||||
|
assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob"))));
|
||||||
|
assert!(!g);
|
||||||
|
Ok(vec![UserAndGroups {
|
||||||
|
user: User {
|
||||||
|
user_id: UserId::new("bob"),
|
||||||
|
email: "bob@bobmail.bob".into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
groups: None,
|
||||||
|
}])
|
||||||
|
});
|
||||||
|
mock.expect_list_groups().returning(|_| Ok(vec![]));
|
||||||
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let dn = "uid=bob,ou=people,dc=example,dc=com";
|
||||||
|
let request = LdapCompareRequest {
|
||||||
|
dn: dn.to_string(),
|
||||||
|
atype: "uid".to_owned(),
|
||||||
|
val: b"bob".to_vec(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.do_compare(request).await,
|
||||||
|
Ok(vec![LdapOp::CompareResult(LdapResultOp {
|
||||||
|
code: LdapResultCode::CompareTrue,
|
||||||
|
matcheddn: dn.to_string(),
|
||||||
|
message: "".to_string(),
|
||||||
|
referral: vec![],
|
||||||
|
})])
|
||||||
|
);
|
||||||
|
// Non-canonical attribute.
|
||||||
|
let request = LdapCompareRequest {
|
||||||
|
dn: dn.to_string(),
|
||||||
|
atype: "eMail".to_owned(),
|
||||||
|
val: b"bob@bobmail.bob".to_vec(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.do_compare(request).await,
|
||||||
|
Ok(vec![LdapOp::CompareResult(LdapResultOp {
|
||||||
|
code: LdapResultCode::CompareTrue,
|
||||||
|
matcheddn: dn.to_string(),
|
||||||
|
message: "".to_string(),
|
||||||
|
referral: vec![],
|
||||||
|
})])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_compare_group() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_list_users().returning(|_, _| Ok(vec![]));
|
||||||
|
mock.expect_list_groups().returning(|f| {
|
||||||
|
assert_eq!(f, Some(GroupRequestFilter::DisplayName("group".into())));
|
||||||
|
Ok(vec![Group {
|
||||||
|
id: GroupId(1),
|
||||||
|
display_name: "group".into(),
|
||||||
|
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
|
||||||
|
users: vec![UserId::new("bob")],
|
||||||
|
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
|
||||||
|
attributes: Vec::new(),
|
||||||
|
}])
|
||||||
|
});
|
||||||
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let dn = "uid=group,ou=groups,dc=example,dc=com";
|
||||||
|
let request = LdapCompareRequest {
|
||||||
|
dn: dn.to_string(),
|
||||||
|
atype: "uid".to_owned(),
|
||||||
|
val: b"group".to_vec(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.do_compare(request).await,
|
||||||
|
Ok(vec![LdapOp::CompareResult(LdapResultOp {
|
||||||
|
code: LdapResultCode::CompareTrue,
|
||||||
|
matcheddn: dn.to_string(),
|
||||||
|
message: "".to_string(),
|
||||||
|
referral: vec![],
|
||||||
|
})])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_compare_not_found() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_list_users().returning(|f, g| {
|
||||||
|
assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob"))));
|
||||||
|
assert!(!g);
|
||||||
|
Ok(vec![])
|
||||||
|
});
|
||||||
|
mock.expect_list_groups().returning(|_| Ok(vec![]));
|
||||||
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let dn = "uid=bob,ou=people,dc=example,dc=com";
|
||||||
|
let request = LdapCompareRequest {
|
||||||
|
dn: dn.to_string(),
|
||||||
|
atype: "uid".to_owned(),
|
||||||
|
val: b"bob".to_vec(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.do_compare(request).await,
|
||||||
|
Ok(vec![LdapOp::CompareResult(LdapResultOp {
|
||||||
|
code: LdapResultCode::NoSuchObject,
|
||||||
|
matcheddn: "dc=example,dc=com".to_owned(),
|
||||||
|
message: "".to_string(),
|
||||||
|
referral: vec![],
|
||||||
|
})])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_compare_no_match() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_list_users().returning(|f, g| {
|
||||||
|
assert_eq!(f, Some(UserRequestFilter::UserId(UserId::new("bob"))));
|
||||||
|
assert!(!g);
|
||||||
|
Ok(vec![UserAndGroups {
|
||||||
|
user: User {
|
||||||
|
user_id: UserId::new("bob"),
|
||||||
|
email: "bob@bobmail.bob".into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
groups: None,
|
||||||
|
}])
|
||||||
|
});
|
||||||
|
mock.expect_list_groups().returning(|_| Ok(vec![]));
|
||||||
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let dn = "uid=bob,ou=people,dc=example,dc=com";
|
||||||
|
let request = LdapCompareRequest {
|
||||||
|
dn: dn.to_string(),
|
||||||
|
atype: "mail".to_owned(),
|
||||||
|
val: b"bob@bob".to_vec(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.do_compare(request).await,
|
||||||
|
Ok(vec![LdapOp::CompareResult(LdapResultOp {
|
||||||
|
code: LdapResultCode::CompareFalse,
|
||||||
|
matcheddn: dn.to_string(),
|
||||||
|
message: "".to_string(),
|
||||||
|
referral: vec![],
|
||||||
|
})])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_compare_group_member() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_list_users().returning(|_, _| Ok(vec![]));
|
||||||
|
mock.expect_list_groups().returning(|f| {
|
||||||
|
assert_eq!(f, Some(GroupRequestFilter::DisplayName("group".into())));
|
||||||
|
Ok(vec![Group {
|
||||||
|
id: GroupId(1),
|
||||||
|
display_name: "group".into(),
|
||||||
|
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
|
||||||
|
users: vec![UserId::new("bob")],
|
||||||
|
uuid: uuid!("04ac75e0-2900-3e21-926c-2f732c26b3fc"),
|
||||||
|
attributes: Vec::new(),
|
||||||
|
}])
|
||||||
|
});
|
||||||
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let dn = "uid=group,ou=groups,dc=example,dc=com";
|
||||||
|
let request = LdapCompareRequest {
|
||||||
|
dn: dn.to_string(),
|
||||||
|
atype: "uniqueMember".to_owned(),
|
||||||
|
val: b"uid=bob,ou=people,dc=example,dc=com".to_vec(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.do_compare(request).await,
|
||||||
|
Ok(vec![LdapOp::CompareResult(LdapResultOp {
|
||||||
|
code: LdapResultCode::CompareTrue,
|
||||||
|
matcheddn: dn.to_owned(),
|
||||||
|
message: "".to_string(),
|
||||||
|
referral: vec![],
|
||||||
|
})])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,40 @@
|
|||||||
|
use crate::core::{
|
||||||
|
error::{LdapError, LdapResult},
|
||||||
|
utils::{
|
||||||
|
ExpandedAttributes, GroupFieldType, LdapInfo, expand_attribute_wildcards,
|
||||||
|
get_custom_attribute, get_group_id_from_distinguished_name_or_plain_name,
|
||||||
|
get_user_id_from_distinguished_name_or_plain_name, map_group_field,
|
||||||
|
},
|
||||||
|
};
|
||||||
use chrono::TimeZone;
|
use chrono::TimeZone;
|
||||||
use ldap3_proto::{
|
use ldap3_proto::{
|
||||||
proto::LdapOp, LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry,
|
LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry, proto::LdapOp,
|
||||||
};
|
};
|
||||||
|
use lldap_domain::{
|
||||||
|
deserialize::deserialize_attribute_value,
|
||||||
|
public_schema::PublicSchema,
|
||||||
|
types::{AttributeName, AttributeType, Group, GroupId, LdapObjectClass, UserId, Uuid},
|
||||||
|
};
|
||||||
|
use lldap_domain_handlers::handler::{GroupListerBackendHandler, GroupRequestFilter};
|
||||||
use tracing::{debug, instrument, warn};
|
use tracing::{debug, instrument, warn};
|
||||||
|
|
||||||
use crate::domain::{
|
pub const REQUIRED_GROUP_ATTRIBUTES: &[&str] = &["display_name"];
|
||||||
deserialize::deserialize_attribute_value,
|
|
||||||
handler::{GroupListerBackendHandler, GroupRequestFilter},
|
const DEFAULT_GROUP_OBJECT_CLASSES: &[&str] = &["groupOfUniqueNames", "groupOfNames"];
|
||||||
ldap::{
|
|
||||||
error::{LdapError, LdapResult},
|
fn get_default_group_object_classes_as_bytes() -> Vec<Vec<u8>> {
|
||||||
utils::{
|
DEFAULT_GROUP_OBJECT_CLASSES
|
||||||
expand_attribute_wildcards, get_custom_attribute,
|
.iter()
|
||||||
get_group_id_from_distinguished_name_or_plain_name,
|
.map(|c| c.as_bytes().to_vec())
|
||||||
get_user_id_from_distinguished_name_or_plain_name, map_group_field, ExpandedAttributes,
|
.collect()
|
||||||
GroupFieldType, LdapInfo,
|
}
|
||||||
},
|
|
||||||
},
|
pub fn get_default_group_object_classes() -> Vec<LdapObjectClass> {
|
||||||
schema::{PublicSchema, SchemaGroupAttributeExtractor},
|
DEFAULT_GROUP_OBJECT_CLASSES
|
||||||
types::{AttributeName, AttributeType, Group, LdapObjectClass, UserId, Uuid},
|
.iter()
|
||||||
};
|
.map(|&c| LdapObjectClass::from(c))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_group_attribute(
|
pub fn get_group_attribute(
|
||||||
group: &Group,
|
group: &Group,
|
||||||
@@ -30,7 +46,8 @@ pub fn get_group_attribute(
|
|||||||
) -> Option<Vec<Vec<u8>>> {
|
) -> Option<Vec<Vec<u8>>> {
|
||||||
let attribute_values = match map_group_field(attribute, schema) {
|
let attribute_values = match map_group_field(attribute, schema) {
|
||||||
GroupFieldType::ObjectClass => {
|
GroupFieldType::ObjectClass => {
|
||||||
let mut classes = vec![b"groupOfUniqueNames".to_vec()];
|
let mut classes: Vec<Vec<u8>> = get_default_group_object_classes_as_bytes();
|
||||||
|
|
||||||
classes.extend(
|
classes.extend(
|
||||||
schema
|
schema
|
||||||
.get_schema()
|
.get_schema()
|
||||||
@@ -45,42 +62,42 @@ pub fn get_group_attribute(
|
|||||||
GroupFieldType::EntryDn => {
|
GroupFieldType::EntryDn => {
|
||||||
vec![format!("uid={},ou=groups,{}", group.display_name, base_dn_str).into_bytes()]
|
vec![format!("uid={},ou=groups,{}", group.display_name, base_dn_str).into_bytes()]
|
||||||
}
|
}
|
||||||
|
GroupFieldType::GroupId => {
|
||||||
|
vec![group.id.0.to_string().into_bytes()]
|
||||||
|
}
|
||||||
GroupFieldType::DisplayName => vec![group.display_name.to_string().into_bytes()],
|
GroupFieldType::DisplayName => vec![group.display_name.to_string().into_bytes()],
|
||||||
GroupFieldType::CreationDate => vec![chrono::Utc
|
GroupFieldType::CreationDate => vec![
|
||||||
.from_utc_datetime(&group.creation_date)
|
chrono::Utc
|
||||||
.to_rfc3339()
|
.from_utc_datetime(&group.creation_date)
|
||||||
.into_bytes()],
|
.to_rfc3339()
|
||||||
|
.into_bytes(),
|
||||||
|
],
|
||||||
GroupFieldType::Member => group
|
GroupFieldType::Member => group
|
||||||
.users
|
.users
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|u| user_filter.as_ref().map(|f| *u == f).unwrap_or(true))
|
.filter(|u| user_filter.as_ref().map(|f| *u == f).unwrap_or(true))
|
||||||
.map(|u| format!("uid={},ou=people,{}", u, base_dn_str).into_bytes())
|
.map(|u| format!("uid={u},ou=people,{base_dn_str}").into_bytes())
|
||||||
.collect(),
|
.collect(),
|
||||||
GroupFieldType::Uuid => vec![group.uuid.to_string().into_bytes()],
|
GroupFieldType::Uuid => vec![group.uuid.to_string().into_bytes()],
|
||||||
GroupFieldType::Attribute(attr, _, _) => {
|
GroupFieldType::Attribute(attr, _, _) => get_custom_attribute(&group.attributes, &attr)?,
|
||||||
get_custom_attribute::<SchemaGroupAttributeExtractor>(&group.attributes, &attr, schema)?
|
|
||||||
}
|
|
||||||
GroupFieldType::NoMatch => match attribute.as_str() {
|
GroupFieldType::NoMatch => match attribute.as_str() {
|
||||||
"1.1" => return None,
|
"1.1" => return None,
|
||||||
// We ignore the operational attribute wildcard
|
// We ignore the operational attribute wildcard
|
||||||
"+" => return None,
|
"+" => return None,
|
||||||
"*" => {
|
"*" => {
|
||||||
panic!(
|
panic!(
|
||||||
"Matched {}, * should have been expanded into attribute list and * removed",
|
"Matched {attribute}, * should have been expanded into attribute list and * removed"
|
||||||
attribute
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if ignored_group_attributes.contains(attribute) {
|
if ignored_group_attributes.contains(attribute) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
get_custom_attribute::<SchemaGroupAttributeExtractor>(
|
get_custom_attribute(
|
||||||
&group.attributes,
|
&group.attributes,
|
||||||
attribute,
|
attribute,
|
||||||
schema,
|
|
||||||
).or_else(||{warn!(
|
).or_else(||{warn!(
|
||||||
r#"Ignoring unrecognized group attribute: {}\n\
|
r#"Ignoring unrecognized group attribute: {}. To disable this warning, add it to "ignored_group_attributes" in the config."#,
|
||||||
To disable this warning, add it to "ignored_group_attributes" in the config."#,
|
|
||||||
attribute
|
attribute
|
||||||
);None})?
|
);None})?
|
||||||
}
|
}
|
||||||
@@ -151,12 +168,23 @@ fn get_group_attribute_equality_filter(
|
|||||||
is_list: bool,
|
is_list: bool,
|
||||||
value: &str,
|
value: &str,
|
||||||
) -> GroupRequestFilter {
|
) -> GroupRequestFilter {
|
||||||
deserialize_attribute_value(&[value.to_owned()], typ, is_list)
|
let value_lc = value.to_ascii_lowercase();
|
||||||
.map(|v| GroupRequestFilter::AttributeEquality(field.clone(), v))
|
let serialized_value = deserialize_attribute_value(&[value.to_owned()], typ, is_list);
|
||||||
.unwrap_or_else(|e| {
|
let serialized_value_lc = deserialize_attribute_value(&[value_lc.to_owned()], typ, is_list);
|
||||||
|
match (serialized_value, serialized_value_lc) {
|
||||||
|
(Ok(v), Ok(v_lc)) => GroupRequestFilter::Or(vec![
|
||||||
|
GroupRequestFilter::AttributeEquality(field.clone(), v),
|
||||||
|
GroupRequestFilter::AttributeEquality(field.clone(), v_lc),
|
||||||
|
]),
|
||||||
|
(Ok(_), Err(e)) => {
|
||||||
|
warn!("Invalid value for attribute {} (lowercased): {}", field, e);
|
||||||
|
GroupRequestFilter::from(false)
|
||||||
|
}
|
||||||
|
(Err(e), _) => {
|
||||||
warn!("Invalid value for attribute {}: {}", field, e);
|
warn!("Invalid value for attribute {}: {}", field, e);
|
||||||
GroupRequestFilter::from(false)
|
GroupRequestFilter::from(false)
|
||||||
})
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_group_filter(
|
fn convert_group_filter(
|
||||||
@@ -168,17 +196,24 @@ fn convert_group_filter(
|
|||||||
match filter {
|
match filter {
|
||||||
LdapFilter::Equality(field, value) => {
|
LdapFilter::Equality(field, value) => {
|
||||||
let field = AttributeName::from(field.as_str());
|
let field = AttributeName::from(field.as_str());
|
||||||
let value = value.to_ascii_lowercase();
|
let value_lc = value.to_ascii_lowercase();
|
||||||
match map_group_field(&field, schema) {
|
match map_group_field(&field, schema) {
|
||||||
GroupFieldType::DisplayName => Ok(GroupRequestFilter::DisplayName(value.into())),
|
GroupFieldType::GroupId => Ok(value_lc
|
||||||
GroupFieldType::Uuid => Uuid::try_from(value.as_str())
|
.parse::<i32>()
|
||||||
|
.map(|id| GroupRequestFilter::GroupId(GroupId(id)))
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
warn!("Given group id is not a valid integer: {}", value_lc);
|
||||||
|
GroupRequestFilter::from(false)
|
||||||
|
})),
|
||||||
|
GroupFieldType::DisplayName => Ok(GroupRequestFilter::DisplayName(value_lc.into())),
|
||||||
|
GroupFieldType::Uuid => Uuid::try_from(value_lc.as_str())
|
||||||
.map(GroupRequestFilter::Uuid)
|
.map(GroupRequestFilter::Uuid)
|
||||||
.map_err(|e| LdapError {
|
.map_err(|e| LdapError {
|
||||||
code: LdapResultCode::Other,
|
code: LdapResultCode::Other,
|
||||||
message: format!("Invalid UUID: {:#}", e),
|
message: format!("Invalid UUID: {e:#}"),
|
||||||
}),
|
}),
|
||||||
GroupFieldType::Member => Ok(get_user_id_from_distinguished_name_or_plain_name(
|
GroupFieldType::Member => Ok(get_user_id_from_distinguished_name_or_plain_name(
|
||||||
&value,
|
&value_lc,
|
||||||
&ldap_info.base_dn,
|
&ldap_info.base_dn,
|
||||||
&ldap_info.base_dn_str,
|
&ldap_info.base_dn_str,
|
||||||
)
|
)
|
||||||
@@ -188,21 +223,23 @@ fn convert_group_filter(
|
|||||||
GroupRequestFilter::from(false)
|
GroupRequestFilter::from(false)
|
||||||
})),
|
})),
|
||||||
GroupFieldType::ObjectClass => Ok(GroupRequestFilter::from(
|
GroupFieldType::ObjectClass => Ok(GroupRequestFilter::from(
|
||||||
matches!(value.as_str(), "groupofuniquenames" | "groupofnames")
|
get_default_group_object_classes()
|
||||||
|
.iter()
|
||||||
|
.any(|class| class.as_str().eq_ignore_ascii_case(value_lc.as_str()))
|
||||||
|| schema
|
|| schema
|
||||||
.get_schema()
|
.get_schema()
|
||||||
.extra_group_object_classes
|
.extra_group_object_classes
|
||||||
.contains(&LdapObjectClass::from(value)),
|
.contains(&LdapObjectClass::from(value_lc)),
|
||||||
)),
|
)),
|
||||||
GroupFieldType::Dn | GroupFieldType::EntryDn => {
|
GroupFieldType::Dn | GroupFieldType::EntryDn => {
|
||||||
Ok(get_group_id_from_distinguished_name_or_plain_name(
|
Ok(get_group_id_from_distinguished_name_or_plain_name(
|
||||||
value.as_str(),
|
value_lc.as_str(),
|
||||||
&ldap_info.base_dn,
|
&ldap_info.base_dn,
|
||||||
&ldap_info.base_dn_str,
|
&ldap_info.base_dn_str,
|
||||||
)
|
)
|
||||||
.map(GroupRequestFilter::DisplayName)
|
.map(GroupRequestFilter::DisplayName)
|
||||||
.unwrap_or_else(|_| {
|
.unwrap_or_else(|_| {
|
||||||
warn!("Invalid dn filter on group: {}", value);
|
warn!("Invalid dn filter on group: {}", value_lc);
|
||||||
GroupRequestFilter::from(false)
|
GroupRequestFilter::from(false)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -217,7 +254,7 @@ fn convert_group_filter(
|
|||||||
Ok(GroupRequestFilter::from(false))
|
Ok(GroupRequestFilter::from(false))
|
||||||
}
|
}
|
||||||
GroupFieldType::Attribute(field, typ, is_list) => Ok(
|
GroupFieldType::Attribute(field, typ, is_list) => Ok(
|
||||||
get_group_attribute_equality_filter(&field, typ, is_list, &value),
|
get_group_attribute_equality_filter(&field, typ, is_list, value),
|
||||||
),
|
),
|
||||||
GroupFieldType::CreationDate => Err(LdapError {
|
GroupFieldType::CreationDate => Err(LdapError {
|
||||||
code: LdapResultCode::UnwillingToPerform,
|
code: LdapResultCode::UnwillingToPerform,
|
||||||
@@ -252,15 +289,14 @@ fn convert_group_filter(
|
|||||||
_ => Err(LdapError {
|
_ => Err(LdapError {
|
||||||
code: LdapResultCode::UnwillingToPerform,
|
code: LdapResultCode::UnwillingToPerform,
|
||||||
message: format!(
|
message: format!(
|
||||||
"Unsupported group attribute for substring filter: \"{}\"",
|
"Unsupported group attribute for substring filter: \"{field}\""
|
||||||
field
|
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => Err(LdapError {
|
_ => Err(LdapError {
|
||||||
code: LdapResultCode::UnwillingToPerform,
|
code: LdapResultCode::UnwillingToPerform,
|
||||||
message: format!("Unsupported group filter: {:?}", filter),
|
message: format!("Unsupported group filter: {filter:?}"),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -280,7 +316,7 @@ pub async fn get_groups_list<Backend: GroupListerBackendHandler>(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| LdapError {
|
.map_err(|e| LdapError {
|
||||||
code: LdapResultCode::Other,
|
code: LdapResultCode::Other,
|
||||||
message: format!(r#"Error while listing groups "{}": {:#}"#, base, e),
|
message: format!(r#"Error while listing groups "{base}": {e:#}"#),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,27 +1,44 @@
|
|||||||
|
use crate::core::{
|
||||||
|
error::{LdapError, LdapResult},
|
||||||
|
utils::{
|
||||||
|
ExpandedAttributes, LdapInfo, UserFieldType, expand_attribute_wildcards,
|
||||||
|
get_custom_attribute, get_group_id_from_distinguished_name_or_plain_name,
|
||||||
|
get_user_id_from_distinguished_name_or_plain_name, map_user_field,
|
||||||
|
},
|
||||||
|
};
|
||||||
use chrono::TimeZone;
|
use chrono::TimeZone;
|
||||||
use ldap3_proto::{
|
use ldap3_proto::{
|
||||||
proto::LdapOp, LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry,
|
LdapFilter, LdapPartialAttribute, LdapResultCode, LdapSearchResultEntry, proto::LdapOp,
|
||||||
};
|
};
|
||||||
|
use lldap_domain::{
|
||||||
|
deserialize::deserialize_attribute_value,
|
||||||
|
public_schema::PublicSchema,
|
||||||
|
types::{
|
||||||
|
AttributeName, AttributeType, GroupDetails, LdapObjectClass, User, UserAndGroups, UserId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use lldap_domain_handlers::handler::{UserListerBackendHandler, UserRequestFilter};
|
||||||
|
use lldap_domain_model::model::UserColumn;
|
||||||
use tracing::{debug, instrument, warn};
|
use tracing::{debug, instrument, warn};
|
||||||
|
|
||||||
use crate::domain::{
|
pub const REQUIRED_USER_ATTRIBUTES: &[&str] = &["user_id", "mail"];
|
||||||
deserialize::deserialize_attribute_value,
|
|
||||||
handler::{UserListerBackendHandler, UserRequestFilter},
|
const DEFAULT_USER_OBJECT_CLASSES: &[&str] =
|
||||||
ldap::{
|
&["inetOrgPerson", "posixAccount", "mailAccount", "person"];
|
||||||
error::{LdapError, LdapResult},
|
|
||||||
utils::{
|
fn get_default_user_object_classes_vec_u8() -> Vec<Vec<u8>> {
|
||||||
expand_attribute_wildcards, get_custom_attribute,
|
DEFAULT_USER_OBJECT_CLASSES
|
||||||
get_group_id_from_distinguished_name_or_plain_name,
|
.iter()
|
||||||
get_user_id_from_distinguished_name_or_plain_name, map_user_field, ExpandedAttributes,
|
.map(|c| c.as_bytes().to_vec())
|
||||||
LdapInfo, UserFieldType,
|
.collect()
|
||||||
},
|
}
|
||||||
},
|
|
||||||
schema::{PublicSchema, SchemaUserAttributeExtractor},
|
pub fn get_default_user_object_classes() -> Vec<LdapObjectClass> {
|
||||||
types::{
|
DEFAULT_USER_OBJECT_CLASSES
|
||||||
AttributeName, AttributeType, GroupDetails, LdapObjectClass, User, UserAndGroups,
|
.iter()
|
||||||
UserColumn, UserId,
|
.map(|&c| LdapObjectClass::from(c))
|
||||||
},
|
.collect()
|
||||||
};
|
}
|
||||||
|
|
||||||
pub fn get_user_attribute(
|
pub fn get_user_attribute(
|
||||||
user: &User,
|
user: &User,
|
||||||
@@ -33,12 +50,8 @@ pub fn get_user_attribute(
|
|||||||
) -> Option<Vec<Vec<u8>>> {
|
) -> Option<Vec<Vec<u8>>> {
|
||||||
let attribute_values = match map_user_field(attribute, schema) {
|
let attribute_values = match map_user_field(attribute, schema) {
|
||||||
UserFieldType::ObjectClass => {
|
UserFieldType::ObjectClass => {
|
||||||
let mut classes = vec![
|
let mut classes: Vec<Vec<u8>> = get_default_user_object_classes_vec_u8();
|
||||||
b"inetOrgPerson".to_vec(),
|
|
||||||
b"posixAccount".to_vec(),
|
|
||||||
b"mailAccount".to_vec(),
|
|
||||||
b"person".to_vec(),
|
|
||||||
];
|
|
||||||
classes.extend(
|
classes.extend(
|
||||||
schema
|
schema
|
||||||
.get_schema()
|
.get_schema()
|
||||||
@@ -74,36 +87,29 @@ pub fn get_user_attribute(
|
|||||||
UserFieldType::PrimaryField(UserColumn::DisplayName) => {
|
UserFieldType::PrimaryField(UserColumn::DisplayName) => {
|
||||||
vec![user.display_name.clone()?.into_bytes()]
|
vec![user.display_name.clone()?.into_bytes()]
|
||||||
}
|
}
|
||||||
UserFieldType::PrimaryField(UserColumn::CreationDate) => vec![chrono::Utc
|
UserFieldType::PrimaryField(UserColumn::CreationDate) => vec![
|
||||||
.from_utc_datetime(&user.creation_date)
|
chrono::Utc
|
||||||
.to_rfc3339()
|
.from_utc_datetime(&user.creation_date)
|
||||||
.into_bytes()],
|
.to_rfc3339()
|
||||||
UserFieldType::Attribute(attr, _, _) => {
|
.into_bytes(),
|
||||||
get_custom_attribute::<SchemaUserAttributeExtractor>(&user.attributes, &attr, schema)?
|
],
|
||||||
}
|
UserFieldType::Attribute(attr, _, _) => get_custom_attribute(&user.attributes, &attr)?,
|
||||||
UserFieldType::NoMatch => match attribute.as_str() {
|
UserFieldType::NoMatch => match attribute.as_str() {
|
||||||
"1.1" => return None,
|
"1.1" => return None,
|
||||||
// We ignore the operational attribute wildcard.
|
// We ignore the operational attribute wildcard.
|
||||||
"+" => return None,
|
"+" => return None,
|
||||||
"*" => {
|
"*" => {
|
||||||
panic!(
|
panic!(
|
||||||
"Matched {}, * should have been expanded into attribute list and * removed",
|
"Matched {attribute}, * should have been expanded into attribute list and * removed"
|
||||||
attribute
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if ignored_user_attributes.contains(attribute) {
|
if ignored_user_attributes.contains(attribute) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
get_custom_attribute::<SchemaUserAttributeExtractor>(
|
get_custom_attribute(&user.attributes, attribute).or_else(|| {
|
||||||
&user.attributes,
|
|
||||||
attribute,
|
|
||||||
schema,
|
|
||||||
)
|
|
||||||
.or_else(|| {
|
|
||||||
warn!(
|
warn!(
|
||||||
r#"Ignoring unrecognized group attribute: {}\n\
|
r#"Ignoring unrecognized user attribute: {}. To disable this warning, add it to "ignored_user_attributes" in the config."#,
|
||||||
To disable this warning, add it to "ignored_user_attributes" in the config."#,
|
|
||||||
attribute
|
attribute
|
||||||
);
|
);
|
||||||
None
|
None
|
||||||
@@ -174,12 +180,23 @@ fn get_user_attribute_equality_filter(
|
|||||||
is_list: bool,
|
is_list: bool,
|
||||||
value: &str,
|
value: &str,
|
||||||
) -> UserRequestFilter {
|
) -> UserRequestFilter {
|
||||||
deserialize_attribute_value(&[value.to_owned()], typ, is_list)
|
let value_lc = value.to_ascii_lowercase();
|
||||||
.map(|v| UserRequestFilter::AttributeEquality(field.clone(), v))
|
let serialized_value = deserialize_attribute_value(&[value.to_owned()], typ, is_list);
|
||||||
.unwrap_or_else(|e| {
|
let serialized_value_lc = deserialize_attribute_value(&[value_lc.to_owned()], typ, is_list);
|
||||||
|
match (serialized_value, serialized_value_lc) {
|
||||||
|
(Ok(v), Ok(v_lc)) => UserRequestFilter::Or(vec![
|
||||||
|
UserRequestFilter::AttributeEquality(field.clone(), v),
|
||||||
|
UserRequestFilter::AttributeEquality(field.clone(), v_lc),
|
||||||
|
]),
|
||||||
|
(Ok(_), Err(e)) => {
|
||||||
|
warn!("Invalid value for attribute {} (lowercased): {}", field, e);
|
||||||
|
UserRequestFilter::from(false)
|
||||||
|
}
|
||||||
|
(Err(e), _) => {
|
||||||
warn!("Invalid value for attribute {}: {}", field, e);
|
warn!("Invalid value for attribute {}: {}", field, e);
|
||||||
UserRequestFilter::from(false)
|
UserRequestFilter::from(false)
|
||||||
})
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_user_filter(
|
fn convert_user_filter(
|
||||||
@@ -198,18 +215,20 @@ fn convert_user_filter(
|
|||||||
LdapFilter::Not(filter) => Ok(UserRequestFilter::Not(Box::new(rec(filter)?))),
|
LdapFilter::Not(filter) => Ok(UserRequestFilter::Not(Box::new(rec(filter)?))),
|
||||||
LdapFilter::Equality(field, value) => {
|
LdapFilter::Equality(field, value) => {
|
||||||
let field = AttributeName::from(field.as_str());
|
let field = AttributeName::from(field.as_str());
|
||||||
let value = value.to_ascii_lowercase();
|
let value_lc = value.to_ascii_lowercase();
|
||||||
match map_user_field(&field, schema) {
|
match map_user_field(&field, schema) {
|
||||||
UserFieldType::PrimaryField(UserColumn::UserId) => {
|
UserFieldType::PrimaryField(UserColumn::UserId) => {
|
||||||
Ok(UserRequestFilter::UserId(UserId::new(&value)))
|
Ok(UserRequestFilter::UserId(UserId::new(&value_lc)))
|
||||||
}
|
}
|
||||||
UserFieldType::PrimaryField(UserColumn::Email) => Ok(UserRequestFilter::Equality(
|
UserFieldType::PrimaryField(UserColumn::Email) => Ok(UserRequestFilter::Equality(
|
||||||
UserColumn::LowercaseEmail,
|
UserColumn::LowercaseEmail,
|
||||||
value,
|
value_lc,
|
||||||
)),
|
)),
|
||||||
UserFieldType::PrimaryField(field) => Ok(UserRequestFilter::Equality(field, value)),
|
UserFieldType::PrimaryField(field) => {
|
||||||
|
Ok(UserRequestFilter::Equality(field, value_lc))
|
||||||
|
}
|
||||||
UserFieldType::Attribute(field, typ, is_list) => Ok(
|
UserFieldType::Attribute(field, typ, is_list) => Ok(
|
||||||
get_user_attribute_equality_filter(&field, typ, is_list, &value),
|
get_user_attribute_equality_filter(&field, typ, is_list, value),
|
||||||
),
|
),
|
||||||
UserFieldType::NoMatch => {
|
UserFieldType::NoMatch => {
|
||||||
if !ldap_info.ignored_user_attributes.contains(&field) {
|
if !ldap_info.ignored_user_attributes.contains(&field) {
|
||||||
@@ -222,16 +241,16 @@ fn convert_user_filter(
|
|||||||
Ok(UserRequestFilter::from(false))
|
Ok(UserRequestFilter::from(false))
|
||||||
}
|
}
|
||||||
UserFieldType::ObjectClass => Ok(UserRequestFilter::from(
|
UserFieldType::ObjectClass => Ok(UserRequestFilter::from(
|
||||||
matches!(
|
get_default_user_object_classes()
|
||||||
value.as_str(),
|
.iter()
|
||||||
"person" | "inetorgperson" | "posixaccount" | "mailaccount"
|
.any(|class| class.as_str().eq_ignore_ascii_case(value_lc.as_str()))
|
||||||
) || schema
|
|| schema
|
||||||
.get_schema()
|
.get_schema()
|
||||||
.extra_user_object_classes
|
.extra_user_object_classes
|
||||||
.contains(&LdapObjectClass::from(value)),
|
.contains(&LdapObjectClass::from(value_lc)),
|
||||||
)),
|
)),
|
||||||
UserFieldType::MemberOf => Ok(get_group_id_from_distinguished_name_or_plain_name(
|
UserFieldType::MemberOf => Ok(get_group_id_from_distinguished_name_or_plain_name(
|
||||||
&value,
|
&value_lc,
|
||||||
&ldap_info.base_dn,
|
&ldap_info.base_dn,
|
||||||
&ldap_info.base_dn_str,
|
&ldap_info.base_dn_str,
|
||||||
)
|
)
|
||||||
@@ -242,13 +261,13 @@ fn convert_user_filter(
|
|||||||
})),
|
})),
|
||||||
UserFieldType::EntryDn | UserFieldType::Dn => {
|
UserFieldType::EntryDn | UserFieldType::Dn => {
|
||||||
Ok(get_user_id_from_distinguished_name_or_plain_name(
|
Ok(get_user_id_from_distinguished_name_or_plain_name(
|
||||||
value.as_str(),
|
value_lc.as_str(),
|
||||||
&ldap_info.base_dn,
|
&ldap_info.base_dn,
|
||||||
&ldap_info.base_dn_str,
|
&ldap_info.base_dn_str,
|
||||||
)
|
)
|
||||||
.map(UserRequestFilter::UserId)
|
.map(UserRequestFilter::UserId)
|
||||||
.unwrap_or_else(|_| {
|
.unwrap_or_else(|_| {
|
||||||
warn!("Invalid dn filter on user: {}", value);
|
warn!("Invalid dn filter on user: {}", value_lc);
|
||||||
UserRequestFilter::from(false)
|
UserRequestFilter::from(false)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -278,10 +297,7 @@ fn convert_user_filter(
|
|||||||
| UserFieldType::PrimaryField(UserColumn::CreationDate)
|
| UserFieldType::PrimaryField(UserColumn::CreationDate)
|
||||||
| UserFieldType::PrimaryField(UserColumn::Uuid) => Err(LdapError {
|
| UserFieldType::PrimaryField(UserColumn::Uuid) => Err(LdapError {
|
||||||
code: LdapResultCode::UnwillingToPerform,
|
code: LdapResultCode::UnwillingToPerform,
|
||||||
message: format!(
|
message: format!("Unsupported user attribute for substring filter: {field:?}"),
|
||||||
"Unsupported user attribute for substring filter: {:?}",
|
|
||||||
field
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
UserFieldType::NoMatch => Ok(UserRequestFilter::from(false)),
|
UserFieldType::NoMatch => Ok(UserRequestFilter::from(false)),
|
||||||
UserFieldType::PrimaryField(UserColumn::Email) => Ok(UserRequestFilter::SubString(
|
UserFieldType::PrimaryField(UserColumn::Email) => Ok(UserRequestFilter::SubString(
|
||||||
@@ -296,7 +312,7 @@ fn convert_user_filter(
|
|||||||
}
|
}
|
||||||
_ => Err(LdapError {
|
_ => Err(LdapError {
|
||||||
code: LdapResultCode::UnwillingToPerform,
|
code: LdapResultCode::UnwillingToPerform,
|
||||||
message: format!("Unsupported user filter: {:?}", filter),
|
message: format!("Unsupported user filter: {filter:?}"),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,7 +337,7 @@ pub async fn get_user_list<Backend: UserListerBackendHandler>(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| LdapError {
|
.map_err(|e| LdapError {
|
||||||
code: LdapResultCode::Other,
|
code: LdapResultCode::Other,
|
||||||
message: format!(r#"Error while searching user "{}": {:#}"#, base, e),
|
message: format!(r#"Error while searching user "{base}": {e:#}"#),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,518 @@
|
|||||||
|
use crate::core::{
|
||||||
|
error::{LdapError, LdapResult},
|
||||||
|
group::{REQUIRED_GROUP_ATTRIBUTES, get_default_group_object_classes},
|
||||||
|
user::{REQUIRED_USER_ATTRIBUTES, get_default_user_object_classes},
|
||||||
|
};
|
||||||
|
use chrono::TimeZone;
|
||||||
|
use itertools::join;
|
||||||
|
use ldap3_proto::LdapResultCode;
|
||||||
|
use lldap_domain::{
|
||||||
|
public_schema::PublicSchema,
|
||||||
|
schema::{AttributeList, Schema},
|
||||||
|
types::{
|
||||||
|
Attribute, AttributeName, AttributeType, AttributeValue, Cardinality, GroupName,
|
||||||
|
LdapObjectClass, UserId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use lldap_domain_model::model::UserColumn;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use tracing::{debug, instrument, warn};
|
||||||
|
|
||||||
|
fn make_dn_pair<I>(mut iter: I) -> LdapResult<(String, String)>
|
||||||
|
where
|
||||||
|
I: Iterator<Item = String>,
|
||||||
|
{
|
||||||
|
(|| {
|
||||||
|
let pair = (
|
||||||
|
iter.next().ok_or_else(|| "Empty DN element".to_string())?,
|
||||||
|
iter.next().ok_or_else(|| "Missing DN value".to_string())?,
|
||||||
|
);
|
||||||
|
if let Some(e) = iter.next() {
|
||||||
|
Err(format!(
|
||||||
|
r#"Too many elements in distinguished name: "{}", "{}", "{}""#,
|
||||||
|
pair.0, pair.1, e
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(pair)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
.map_err(|e| LdapError {
|
||||||
|
code: LdapResultCode::InvalidDNSyntax,
|
||||||
|
message: e,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_distinguished_name(dn: &str) -> LdapResult<Vec<(String, String)>> {
|
||||||
|
assert!(dn == dn.to_ascii_lowercase());
|
||||||
|
dn.split(',')
|
||||||
|
.map(|s| make_dn_pair(s.split('=').map(str::trim).map(String::from)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum UserOrGroupName {
|
||||||
|
User(UserId),
|
||||||
|
Group(GroupName),
|
||||||
|
BadSubStree,
|
||||||
|
UnexpectedFormat,
|
||||||
|
InvalidSyntax(LdapError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserOrGroupName {
|
||||||
|
pub fn into_ldap_error(self, input: &str, expected_format: String) -> LdapError {
|
||||||
|
LdapError {
|
||||||
|
code: LdapResultCode::InvalidDNSyntax,
|
||||||
|
message: match self {
|
||||||
|
UserOrGroupName::BadSubStree => "Not a subtree of the base tree".to_string(),
|
||||||
|
UserOrGroupName::InvalidSyntax(err) => return err,
|
||||||
|
UserOrGroupName::UnexpectedFormat
|
||||||
|
| UserOrGroupName::User(_)
|
||||||
|
| UserOrGroupName::Group(_) => {
|
||||||
|
format!(r#"Unexpected DN format. Got "{input}", expected: {expected_format}"#)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_user_or_group_id_from_distinguished_name(
|
||||||
|
dn: &str,
|
||||||
|
base_tree: &[(String, String)],
|
||||||
|
) -> UserOrGroupName {
|
||||||
|
let parts = match parse_distinguished_name(dn) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => return UserOrGroupName::InvalidSyntax(e),
|
||||||
|
};
|
||||||
|
if !is_subtree(&parts, base_tree) {
|
||||||
|
return UserOrGroupName::BadSubStree;
|
||||||
|
} else if parts.len() == base_tree.len() + 2
|
||||||
|
&& parts[1].0 == "ou"
|
||||||
|
&& (parts[0].0 == "cn" || parts[0].0 == "uid")
|
||||||
|
{
|
||||||
|
if parts[1].1 == "groups" {
|
||||||
|
return UserOrGroupName::Group(GroupName::from(parts[0].1.clone()));
|
||||||
|
} else if parts[1].1 == "people" {
|
||||||
|
return UserOrGroupName::User(UserId::from(parts[0].1.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UserOrGroupName::UnexpectedFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_user_id_from_distinguished_name(
|
||||||
|
dn: &str,
|
||||||
|
base_tree: &[(String, String)],
|
||||||
|
base_dn_str: &str,
|
||||||
|
) -> LdapResult<UserId> {
|
||||||
|
match get_user_or_group_id_from_distinguished_name(dn, base_tree) {
|
||||||
|
UserOrGroupName::User(user_id) => Ok(user_id),
|
||||||
|
err => Err(err.into_ldap_error(dn, format!(r#""uid=id,ou=people,{base_dn_str}""#))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_group_id_from_distinguished_name(
|
||||||
|
dn: &str,
|
||||||
|
base_tree: &[(String, String)],
|
||||||
|
base_dn_str: &str,
|
||||||
|
) -> LdapResult<GroupName> {
|
||||||
|
match get_user_or_group_id_from_distinguished_name(dn, base_tree) {
|
||||||
|
UserOrGroupName::Group(group_name) => Ok(group_name),
|
||||||
|
err => Err(err.into_ldap_error(dn, format!(r#""uid=id,ou=groups,{base_dn_str}""#))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn looks_like_distinguished_name(dn: &str) -> bool {
|
||||||
|
dn.contains('=') || dn.contains(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_user_id_from_distinguished_name_or_plain_name(
|
||||||
|
dn: &str,
|
||||||
|
base_tree: &[(String, String)],
|
||||||
|
base_dn_str: &str,
|
||||||
|
) -> LdapResult<UserId> {
|
||||||
|
if !looks_like_distinguished_name(dn) {
|
||||||
|
Ok(UserId::from(dn))
|
||||||
|
} else {
|
||||||
|
get_user_id_from_distinguished_name(dn, base_tree, base_dn_str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_group_id_from_distinguished_name_or_plain_name(
|
||||||
|
dn: &str,
|
||||||
|
base_tree: &[(String, String)],
|
||||||
|
base_dn_str: &str,
|
||||||
|
) -> LdapResult<GroupName> {
|
||||||
|
if !looks_like_distinguished_name(dn) {
|
||||||
|
Ok(GroupName::from(dn))
|
||||||
|
} else {
|
||||||
|
get_group_id_from_distinguished_name(dn, base_tree, base_dn_str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ExpandedAttributes {
|
||||||
|
// Lowercase name to original name.
|
||||||
|
pub attribute_keys: BTreeMap<AttributeName, String>,
|
||||||
|
pub include_custom_attributes: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(all_attribute_keys), level = "debug")]
|
||||||
|
pub fn expand_attribute_wildcards(
|
||||||
|
ldap_attributes: &[String],
|
||||||
|
all_attribute_keys: &[&'static str],
|
||||||
|
) -> ExpandedAttributes {
|
||||||
|
let mut include_custom_attributes = false;
|
||||||
|
let mut attributes_out: BTreeMap<_, _> = ldap_attributes
|
||||||
|
.iter()
|
||||||
|
.filter(|&s| s != "*" && s != "+" && s != "1.1")
|
||||||
|
.map(|s| (AttributeName::from(s), s.to_string()))
|
||||||
|
.collect();
|
||||||
|
attributes_out.extend(
|
||||||
|
if ldap_attributes.iter().any(|x| x == "*") || ldap_attributes.is_empty() {
|
||||||
|
include_custom_attributes = true;
|
||||||
|
all_attribute_keys
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
}
|
||||||
|
.iter()
|
||||||
|
.map(|&s| (AttributeName::from(s), s.to_string())),
|
||||||
|
);
|
||||||
|
debug!(?attributes_out);
|
||||||
|
ExpandedAttributes {
|
||||||
|
attribute_keys: attributes_out,
|
||||||
|
include_custom_attributes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_subtree(subtree: &[(String, String)], base_tree: &[(String, String)]) -> bool {
|
||||||
|
for (k, v) in subtree {
|
||||||
|
assert!(k == &k.to_ascii_lowercase());
|
||||||
|
assert!(v == &v.to_ascii_lowercase());
|
||||||
|
}
|
||||||
|
for (k, v) in base_tree {
|
||||||
|
assert!(k == &k.to_ascii_lowercase());
|
||||||
|
assert!(v == &v.to_ascii_lowercase());
|
||||||
|
}
|
||||||
|
if subtree.len() < base_tree.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let size_diff = subtree.len() - base_tree.len();
|
||||||
|
for i in 0..base_tree.len() {
|
||||||
|
if subtree[size_diff + i] != base_tree[i] {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum UserFieldType {
|
||||||
|
NoMatch,
|
||||||
|
ObjectClass,
|
||||||
|
MemberOf,
|
||||||
|
Dn,
|
||||||
|
EntryDn,
|
||||||
|
PrimaryField(UserColumn),
|
||||||
|
Attribute(AttributeName, AttributeType, bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map_user_field(field: &AttributeName, schema: &PublicSchema) -> UserFieldType {
|
||||||
|
match field.as_str() {
|
||||||
|
"memberof" | "ismemberof" => UserFieldType::MemberOf,
|
||||||
|
"objectclass" => UserFieldType::ObjectClass,
|
||||||
|
"dn" | "distinguishedname" => UserFieldType::Dn,
|
||||||
|
"entrydn" => UserFieldType::EntryDn,
|
||||||
|
"uid" | "user_id" | "id" => UserFieldType::PrimaryField(UserColumn::UserId),
|
||||||
|
"mail" | "email" => UserFieldType::PrimaryField(UserColumn::Email),
|
||||||
|
"cn" | "displayname" | "display_name" => {
|
||||||
|
UserFieldType::PrimaryField(UserColumn::DisplayName)
|
||||||
|
}
|
||||||
|
"givenname" | "first_name" | "firstname" => UserFieldType::Attribute(
|
||||||
|
AttributeName::from("first_name"),
|
||||||
|
AttributeType::String,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
"sn" | "last_name" | "lastname" => UserFieldType::Attribute(
|
||||||
|
AttributeName::from("last_name"),
|
||||||
|
AttributeType::String,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
"avatar" | "jpegphoto" => UserFieldType::Attribute(
|
||||||
|
AttributeName::from("avatar"),
|
||||||
|
AttributeType::JpegPhoto,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
"creationdate" | "createtimestamp" | "modifytimestamp" | "creation_date" => {
|
||||||
|
UserFieldType::PrimaryField(UserColumn::CreationDate)
|
||||||
|
}
|
||||||
|
"entryuuid" | "uuid" => UserFieldType::PrimaryField(UserColumn::Uuid),
|
||||||
|
_ => schema
|
||||||
|
.get_schema()
|
||||||
|
.user_attributes
|
||||||
|
.get_attribute_type(field)
|
||||||
|
.map(|(t, is_list)| UserFieldType::Attribute(field.clone(), t, is_list))
|
||||||
|
.unwrap_or(UserFieldType::NoMatch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum GroupFieldType {
|
||||||
|
NoMatch,
|
||||||
|
GroupId,
|
||||||
|
DisplayName,
|
||||||
|
CreationDate,
|
||||||
|
ObjectClass,
|
||||||
|
Dn,
|
||||||
|
// Like Dn, but returned as part of the attributes.
|
||||||
|
EntryDn,
|
||||||
|
Member,
|
||||||
|
Uuid,
|
||||||
|
Attribute(AttributeName, AttributeType, bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map_group_field(field: &AttributeName, schema: &PublicSchema) -> GroupFieldType {
|
||||||
|
match field.as_str() {
|
||||||
|
"dn" | "distinguishedname" => GroupFieldType::Dn,
|
||||||
|
"entrydn" => GroupFieldType::EntryDn,
|
||||||
|
"objectclass" => GroupFieldType::ObjectClass,
|
||||||
|
"cn" | "displayname" | "uid" | "display_name" | "id" => GroupFieldType::DisplayName,
|
||||||
|
"creationdate" | "createtimestamp" | "modifytimestamp" | "creation_date" => {
|
||||||
|
GroupFieldType::CreationDate
|
||||||
|
}
|
||||||
|
"member" | "uniquemember" => GroupFieldType::Member,
|
||||||
|
"entryuuid" | "uuid" => GroupFieldType::Uuid,
|
||||||
|
"group_id" | "groupid" => GroupFieldType::GroupId,
|
||||||
|
_ => schema
|
||||||
|
.get_schema()
|
||||||
|
.group_attributes
|
||||||
|
.get_attribute_type(field)
|
||||||
|
.map(|(t, is_list)| GroupFieldType::Attribute(field.clone(), t, is_list))
|
||||||
|
.unwrap_or(GroupFieldType::NoMatch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LdapInfo {
|
||||||
|
pub base_dn: Vec<(String, String)>,
|
||||||
|
pub base_dn_str: String,
|
||||||
|
pub ignored_user_attributes: Vec<AttributeName>,
|
||||||
|
pub ignored_group_attributes: Vec<AttributeName>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_custom_attribute(
|
||||||
|
attributes: &[Attribute],
|
||||||
|
attribute_name: &AttributeName,
|
||||||
|
) -> Option<Vec<Vec<u8>>> {
|
||||||
|
let convert_date = |date| {
|
||||||
|
chrono::Utc
|
||||||
|
.from_utc_datetime(date)
|
||||||
|
.to_rfc3339()
|
||||||
|
.into_bytes()
|
||||||
|
};
|
||||||
|
attributes
|
||||||
|
.iter()
|
||||||
|
.find(|a| &a.name == attribute_name)
|
||||||
|
.map(|attribute| match &attribute.value {
|
||||||
|
AttributeValue::String(Cardinality::Singleton(s)) => {
|
||||||
|
vec![s.clone().into_bytes()]
|
||||||
|
}
|
||||||
|
AttributeValue::String(Cardinality::Unbounded(l)) => {
|
||||||
|
l.iter().map(|s| s.clone().into_bytes()).collect()
|
||||||
|
}
|
||||||
|
AttributeValue::Integer(Cardinality::Singleton(i)) => {
|
||||||
|
// LDAP integers are encoded as strings.
|
||||||
|
vec![i.to_string().into_bytes()]
|
||||||
|
}
|
||||||
|
AttributeValue::Integer(Cardinality::Unbounded(l)) => l
|
||||||
|
.iter()
|
||||||
|
// LDAP integers are encoded as strings.
|
||||||
|
.map(|i| i.to_string().into_bytes())
|
||||||
|
.collect(),
|
||||||
|
AttributeValue::JpegPhoto(Cardinality::Singleton(p)) => {
|
||||||
|
vec![p.clone().into_bytes()]
|
||||||
|
}
|
||||||
|
AttributeValue::JpegPhoto(Cardinality::Unbounded(l)) => {
|
||||||
|
l.iter().map(|p| p.clone().into_bytes()).collect()
|
||||||
|
}
|
||||||
|
AttributeValue::DateTime(Cardinality::Singleton(dt)) => vec![convert_date(dt)],
|
||||||
|
AttributeValue::DateTime(Cardinality::Unbounded(l)) => {
|
||||||
|
l.iter().map(convert_date).collect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(derive_more::From)]
|
||||||
|
pub struct ObjectClassList(Vec<LdapObjectClass>);
|
||||||
|
|
||||||
|
// See RFC4512 section 4.2.1 "objectClasses"
|
||||||
|
impl ObjectClassList {
|
||||||
|
pub fn format_for_ldap_schema_description(&self) -> String {
|
||||||
|
join(self.0.iter().map(|c| format!("'{c}'")), " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// See RFC4512 section 4.2 "Subschema Subentries"
|
||||||
|
// This struct holds all information on what attributes and objectclasses are present on the server.
|
||||||
|
// It can be used to 'index' a server using a LDAP subschema call.
|
||||||
|
pub struct LdapSchemaDescription {
|
||||||
|
base: PublicSchema,
|
||||||
|
user_object_classes: ObjectClassList,
|
||||||
|
group_object_classes: ObjectClassList,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LdapSchemaDescription {
|
||||||
|
pub fn from(schema: PublicSchema) -> Self {
|
||||||
|
let mut user_object_classes = get_default_user_object_classes();
|
||||||
|
user_object_classes.extend(schema.get_schema().extra_user_object_classes.clone());
|
||||||
|
let mut group_object_classes = get_default_group_object_classes();
|
||||||
|
group_object_classes.extend(schema.get_schema().extra_group_object_classes.clone());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
base: schema,
|
||||||
|
user_object_classes: ObjectClassList(user_object_classes),
|
||||||
|
group_object_classes: ObjectClassList(group_object_classes),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schema(&self) -> &Schema {
|
||||||
|
self.base.get_schema()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn user_object_classes(&self) -> &ObjectClassList {
|
||||||
|
&self.user_object_classes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn group_object_classes(&self) -> &ObjectClassList {
|
||||||
|
&self.group_object_classes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn required_user_attributes(&self) -> AttributeList {
|
||||||
|
let attributes = self
|
||||||
|
.schema()
|
||||||
|
.user_attributes
|
||||||
|
.attributes
|
||||||
|
.iter()
|
||||||
|
.filter(|a| REQUIRED_USER_ATTRIBUTES.contains(&a.name.as_str()))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
AttributeList { attributes }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn optional_user_attributes(&self) -> AttributeList {
|
||||||
|
let attributes = self
|
||||||
|
.schema()
|
||||||
|
.user_attributes
|
||||||
|
.attributes
|
||||||
|
.iter()
|
||||||
|
.filter(|a| !REQUIRED_USER_ATTRIBUTES.contains(&a.name.as_str()))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
AttributeList { attributes }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn required_group_attributes(&self) -> AttributeList {
|
||||||
|
let attributes = self
|
||||||
|
.schema()
|
||||||
|
.group_attributes
|
||||||
|
.attributes
|
||||||
|
.iter()
|
||||||
|
.filter(|a| REQUIRED_GROUP_ATTRIBUTES.contains(&a.name.as_str()))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
AttributeList { attributes }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn optional_group_attributes(&self) -> AttributeList {
|
||||||
|
let attributes = self
|
||||||
|
.schema()
|
||||||
|
.group_attributes
|
||||||
|
.attributes
|
||||||
|
.iter()
|
||||||
|
.filter(|a| !REQUIRED_GROUP_ATTRIBUTES.contains(&a.name.as_str()))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
AttributeList { attributes }
|
||||||
|
}
|
||||||
|
|
||||||
|
// See RFC4512 section 4.2.2 "attributeTypes"
|
||||||
|
// Parameter 'index_offset' is an offset for the enumeration of this list of attributes,
|
||||||
|
// it has been preceeded by the list of hardcoded attributes.
|
||||||
|
pub fn formatted_attribute_list(
|
||||||
|
&self,
|
||||||
|
index_offset: usize,
|
||||||
|
exclude_attributes: Vec<&str>,
|
||||||
|
) -> Vec<Vec<u8>> {
|
||||||
|
let mut formatted_list: Vec<Vec<u8>> = Vec::new();
|
||||||
|
|
||||||
|
for (index, attribute) in self
|
||||||
|
.all_attributes()
|
||||||
|
.attributes
|
||||||
|
.into_iter()
|
||||||
|
.filter(|attr| !exclude_attributes.contains(&attr.name.as_str()))
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
formatted_list.push(
|
||||||
|
format!(
|
||||||
|
"( 10.{} NAME '{}' DESC 'LLDAP: {}' SUP {:?} )",
|
||||||
|
(index + index_offset),
|
||||||
|
attribute.name,
|
||||||
|
if attribute.is_hardcoded {
|
||||||
|
"builtin attribute"
|
||||||
|
} else {
|
||||||
|
"custom attribute"
|
||||||
|
},
|
||||||
|
attribute.attribute_type
|
||||||
|
)
|
||||||
|
.into_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted_list
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn all_attributes(&self) -> AttributeList {
|
||||||
|
let mut combined_attributes = self.schema().user_attributes.attributes.clone();
|
||||||
|
combined_attributes.extend_from_slice(&self.schema().group_attributes.attributes);
|
||||||
|
AttributeList {
|
||||||
|
attributes: combined_attributes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_subtree() {
|
||||||
|
let subtree1 = &[
|
||||||
|
("ou".to_string(), "people".to_string()),
|
||||||
|
("dc".to_string(), "example".to_string()),
|
||||||
|
("dc".to_string(), "com".to_string()),
|
||||||
|
];
|
||||||
|
let root = &[
|
||||||
|
("dc".to_string(), "example".to_string()),
|
||||||
|
("dc".to_string(), "com".to_string()),
|
||||||
|
];
|
||||||
|
assert!(is_subtree(subtree1, root));
|
||||||
|
assert!(!is_subtree(&[], root));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_distinguished_name() {
|
||||||
|
let parsed_dn = &[
|
||||||
|
("ou".to_string(), "people".to_string()),
|
||||||
|
("dc".to_string(), "example".to_string()),
|
||||||
|
("dc".to_string(), "com".to_string()),
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
parse_distinguished_name("ou=people,dc=example,dc=com").expect("parsing failed"),
|
||||||
|
parsed_dn
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_distinguished_name(" ou = people , dc = example , dc = com ")
|
||||||
|
.expect("parsing failed"),
|
||||||
|
parsed_dn
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
use crate::{
|
||||||
|
core::{
|
||||||
|
error::{LdapError, LdapResult},
|
||||||
|
utils::{LdapInfo, UserOrGroupName, get_user_or_group_id_from_distinguished_name},
|
||||||
|
},
|
||||||
|
handler::make_add_response,
|
||||||
|
};
|
||||||
|
use ldap3_proto::proto::{
|
||||||
|
LdapAddRequest, LdapAttribute, LdapOp, LdapPartialAttribute, LdapResultCode,
|
||||||
|
};
|
||||||
|
use lldap_access_control::AdminBackendHandler;
|
||||||
|
use lldap_domain::{
|
||||||
|
deserialize,
|
||||||
|
requests::{CreateGroupRequest, CreateUserRequest},
|
||||||
|
types::{Attribute, AttributeName, AttributeType, Email, GroupName, UserId},
|
||||||
|
};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
#[instrument(skip_all, level = "debug")]
|
||||||
|
pub(crate) async fn create_user_or_group(
|
||||||
|
backend_handler: &impl AdminBackendHandler,
|
||||||
|
ldap_info: &LdapInfo,
|
||||||
|
request: LdapAddRequest,
|
||||||
|
) -> LdapResult<Vec<LdapOp>> {
|
||||||
|
let base_dn_str = &ldap_info.base_dn_str;
|
||||||
|
match get_user_or_group_id_from_distinguished_name(&request.dn, &ldap_info.base_dn) {
|
||||||
|
UserOrGroupName::User(user_id) => {
|
||||||
|
create_user(backend_handler, user_id, request.attributes).await
|
||||||
|
}
|
||||||
|
UserOrGroupName::Group(group_name) => {
|
||||||
|
create_group(backend_handler, group_name, request.attributes).await
|
||||||
|
}
|
||||||
|
err => Err(err.into_ldap_error(
|
||||||
|
&request.dn,
|
||||||
|
format!(r#""uid=id,ou=people,{base_dn_str}" or "uid=id,ou=groups,{base_dn_str}""#),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, level = "debug")]
|
||||||
|
async fn create_user(
|
||||||
|
backend_handler: &impl AdminBackendHandler,
|
||||||
|
user_id: UserId,
|
||||||
|
attributes: Vec<LdapAttribute>,
|
||||||
|
) -> LdapResult<Vec<LdapOp>> {
|
||||||
|
fn parse_attribute(mut attr: LdapPartialAttribute) -> LdapResult<(String, Vec<u8>)> {
|
||||||
|
if attr.vals.len() > 1 {
|
||||||
|
Err(LdapError {
|
||||||
|
code: LdapResultCode::ConstraintViolation,
|
||||||
|
message: format!("Expected a single value for attribute {}", attr.atype),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
attr.atype.make_ascii_lowercase();
|
||||||
|
match attr.vals.pop() {
|
||||||
|
Some(val) => Ok((attr.atype, val)),
|
||||||
|
None => Err(LdapError {
|
||||||
|
code: LdapResultCode::ConstraintViolation,
|
||||||
|
message: format!("Missing value for attribute {}", attr.atype),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let attributes: HashMap<String, Vec<u8>> = attributes
|
||||||
|
.into_iter()
|
||||||
|
.filter(|a| !a.atype.eq_ignore_ascii_case("objectclass"))
|
||||||
|
.map(parse_attribute)
|
||||||
|
.collect::<LdapResult<_>>()?;
|
||||||
|
fn decode_attribute_value(val: &[u8]) -> LdapResult<String> {
|
||||||
|
std::str::from_utf8(val)
|
||||||
|
.map_err(|e| LdapError {
|
||||||
|
code: LdapResultCode::ConstraintViolation,
|
||||||
|
message: format!("Attribute value is invalid UTF-8: {e:#?} (value {val:?})"),
|
||||||
|
})
|
||||||
|
.map(str::to_owned)
|
||||||
|
}
|
||||||
|
let get_attribute = |name| {
|
||||||
|
attributes
|
||||||
|
.get(name)
|
||||||
|
.map(Vec::as_slice)
|
||||||
|
.map(decode_attribute_value)
|
||||||
|
};
|
||||||
|
let make_encoded_attribute = |name: &str, typ: AttributeType, value: String| {
|
||||||
|
Ok(Attribute {
|
||||||
|
name: AttributeName::from(name),
|
||||||
|
value: deserialize::deserialize_attribute_value(&[value], typ, false).map_err(|e| {
|
||||||
|
LdapError {
|
||||||
|
code: LdapResultCode::ConstraintViolation,
|
||||||
|
message: format!("Invalid attribute value: {e}"),
|
||||||
|
}
|
||||||
|
})?,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let mut new_user_attributes: Vec<Attribute> = Vec::new();
|
||||||
|
if let Some(first_name) = get_attribute("givenname").transpose()? {
|
||||||
|
new_user_attributes.push(make_encoded_attribute(
|
||||||
|
"first_name",
|
||||||
|
AttributeType::String,
|
||||||
|
first_name,
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
if let Some(last_name) = get_attribute("sn").transpose()? {
|
||||||
|
new_user_attributes.push(make_encoded_attribute(
|
||||||
|
"last_name",
|
||||||
|
AttributeType::String,
|
||||||
|
last_name,
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
if let Some(avatar) = get_attribute("avatar").transpose()? {
|
||||||
|
new_user_attributes.push(make_encoded_attribute(
|
||||||
|
"avatar",
|
||||||
|
AttributeType::JpegPhoto,
|
||||||
|
avatar,
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
backend_handler
|
||||||
|
.create_user(CreateUserRequest {
|
||||||
|
user_id,
|
||||||
|
email: Email::from(
|
||||||
|
get_attribute("mail")
|
||||||
|
.or_else(|| get_attribute("email"))
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or_default(),
|
||||||
|
),
|
||||||
|
display_name: get_attribute("cn").transpose()?,
|
||||||
|
attributes: new_user_attributes,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| LdapError {
|
||||||
|
code: LdapResultCode::OperationsError,
|
||||||
|
message: format!("Could not create user: {e:#?}"),
|
||||||
|
})?;
|
||||||
|
Ok(vec![make_add_response(
|
||||||
|
LdapResultCode::Success,
|
||||||
|
String::new(),
|
||||||
|
)])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, level = "debug")]
|
||||||
|
async fn create_group(
|
||||||
|
backend_handler: &impl AdminBackendHandler,
|
||||||
|
group_name: GroupName,
|
||||||
|
_attributes: Vec<LdapAttribute>,
|
||||||
|
) -> LdapResult<Vec<LdapOp>> {
|
||||||
|
backend_handler
|
||||||
|
.create_group(CreateGroupRequest {
|
||||||
|
display_name: group_name,
|
||||||
|
attributes: Vec::new(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| LdapError {
|
||||||
|
code: LdapResultCode::OperationsError,
|
||||||
|
message: format!("Could not create group: {e:#?}"),
|
||||||
|
})?;
|
||||||
|
Ok(vec![make_add_response(
|
||||||
|
LdapResultCode::Success,
|
||||||
|
String::new(),
|
||||||
|
)])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::handler::tests::setup_bound_admin_handler;
|
||||||
|
use lldap_domain::types::*;
|
||||||
|
use lldap_test_utils::MockTestBackendHandler;
|
||||||
|
use mockall::predicate::eq;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_user() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_create_user()
|
||||||
|
.with(eq(CreateUserRequest {
|
||||||
|
user_id: UserId::new("bob"),
|
||||||
|
email: "".into(),
|
||||||
|
display_name: Some("Bob".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.times(1)
|
||||||
|
.return_once(|_| Ok(()));
|
||||||
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let request = LdapAddRequest {
|
||||||
|
dn: "uid=bob,ou=people,dc=example,dc=com".to_owned(),
|
||||||
|
attributes: vec![LdapPartialAttribute {
|
||||||
|
atype: "cn".to_owned(),
|
||||||
|
vals: vec![b"Bob".to_vec()],
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.create_user_or_group(request).await,
|
||||||
|
Ok(vec![make_add_response(
|
||||||
|
LdapResultCode::Success,
|
||||||
|
String::new()
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_group() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_create_group()
|
||||||
|
.with(eq(CreateGroupRequest {
|
||||||
|
display_name: GroupName::new("bob"),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.times(1)
|
||||||
|
.return_once(|_| Ok(GroupId(5)));
|
||||||
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let request = LdapAddRequest {
|
||||||
|
dn: "uid=bob,ou=groups,dc=example,dc=com".to_owned(),
|
||||||
|
attributes: vec![LdapPartialAttribute {
|
||||||
|
atype: "cn".to_owned(),
|
||||||
|
vals: vec![b"Bobby".to_vec()],
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.create_user_or_group(request).await,
|
||||||
|
Ok(vec![make_add_response(
|
||||||
|
LdapResultCode::Success,
|
||||||
|
String::new()
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_user_multiple_object_class() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_create_user()
|
||||||
|
.with(eq(CreateUserRequest {
|
||||||
|
user_id: UserId::new("bob"),
|
||||||
|
email: "".into(),
|
||||||
|
display_name: Some("Bob".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.times(1)
|
||||||
|
.return_once(|_| Ok(()));
|
||||||
|
let ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let request = LdapAddRequest {
|
||||||
|
dn: "uid=bob,ou=people,dc=example,dc=com".to_owned(),
|
||||||
|
attributes: vec![
|
||||||
|
LdapPartialAttribute {
|
||||||
|
atype: "cn".to_owned(),
|
||||||
|
vals: vec![b"Bob".to_vec()],
|
||||||
|
},
|
||||||
|
LdapPartialAttribute {
|
||||||
|
atype: "objectClass".to_owned(),
|
||||||
|
vals: vec![
|
||||||
|
b"top".to_vec(),
|
||||||
|
b"person".to_vec(),
|
||||||
|
b"inetOrgPerson".to_vec(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.create_user_or_group(request).await,
|
||||||
|
Ok(vec![make_add_response(
|
||||||
|
LdapResultCode::Success,
|
||||||
|
String::new()
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
use crate::core::{
|
||||||
|
error::{LdapError, LdapResult},
|
||||||
|
utils::{LdapInfo, UserOrGroupName, get_user_or_group_id_from_distinguished_name},
|
||||||
|
};
|
||||||
|
use ldap3_proto::proto::{LdapOp, LdapResult as LdapResultOp, LdapResultCode};
|
||||||
|
use lldap_access_control::AdminBackendHandler;
|
||||||
|
use lldap_domain::types::{GroupName, UserId};
|
||||||
|
use lldap_domain_handlers::handler::GroupRequestFilter;
|
||||||
|
use lldap_domain_model::error::DomainError;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
pub(crate) fn make_del_response(code: LdapResultCode, message: String) -> LdapOp {
|
||||||
|
LdapOp::DelResponse(LdapResultOp {
|
||||||
|
code,
|
||||||
|
matcheddn: "".to_string(),
|
||||||
|
message,
|
||||||
|
referral: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, level = "debug")]
|
||||||
|
pub(crate) async fn delete_user_or_group(
|
||||||
|
backend_handler: &impl AdminBackendHandler,
|
||||||
|
ldap_info: &LdapInfo,
|
||||||
|
request: String,
|
||||||
|
) -> LdapResult<Vec<LdapOp>> {
|
||||||
|
let base_dn_str = &ldap_info.base_dn_str;
|
||||||
|
match get_user_or_group_id_from_distinguished_name(&request, &ldap_info.base_dn) {
|
||||||
|
UserOrGroupName::User(user_id) => delete_user(backend_handler, user_id).await,
|
||||||
|
UserOrGroupName::Group(group_name) => delete_group(backend_handler, group_name).await,
|
||||||
|
err => Err(err.into_ldap_error(
|
||||||
|
&request,
|
||||||
|
format!(r#""uid=id,ou=people,{base_dn_str}" or "uid=id,ou=groups,{base_dn_str}""#),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, level = "debug")]
|
||||||
|
async fn delete_user(
|
||||||
|
backend_handler: &impl AdminBackendHandler,
|
||||||
|
user_id: UserId,
|
||||||
|
) -> LdapResult<Vec<LdapOp>> {
|
||||||
|
backend_handler
|
||||||
|
.get_user_details(&user_id)
|
||||||
|
.await
|
||||||
|
.map_err(|err| match err {
|
||||||
|
DomainError::EntityNotFound(_) => LdapError {
|
||||||
|
code: LdapResultCode::NoSuchObject,
|
||||||
|
message: "Could not find user".to_string(),
|
||||||
|
},
|
||||||
|
e => LdapError {
|
||||||
|
code: LdapResultCode::OperationsError,
|
||||||
|
message: format!("Error while finding user: {e:?}"),
|
||||||
|
},
|
||||||
|
})?;
|
||||||
|
backend_handler
|
||||||
|
.delete_user(&user_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| LdapError {
|
||||||
|
code: LdapResultCode::OperationsError,
|
||||||
|
message: format!("Error while deleting user: {e:?}"),
|
||||||
|
})?;
|
||||||
|
Ok(vec![make_del_response(
|
||||||
|
LdapResultCode::Success,
|
||||||
|
String::new(),
|
||||||
|
)])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, level = "debug")]
|
||||||
|
async fn delete_group(
|
||||||
|
backend_handler: &impl AdminBackendHandler,
|
||||||
|
group_name: GroupName,
|
||||||
|
) -> LdapResult<Vec<LdapOp>> {
|
||||||
|
let groups = backend_handler
|
||||||
|
.list_groups(Some(GroupRequestFilter::DisplayName(group_name.clone())))
|
||||||
|
.await
|
||||||
|
.map_err(|e| LdapError {
|
||||||
|
code: LdapResultCode::OperationsError,
|
||||||
|
message: format!("Error while finding group: {e:?}"),
|
||||||
|
})?;
|
||||||
|
let group_id = groups
|
||||||
|
.iter()
|
||||||
|
.find(|g| g.display_name == group_name)
|
||||||
|
.map(|g| g.id)
|
||||||
|
.ok_or_else(|| LdapError {
|
||||||
|
code: LdapResultCode::NoSuchObject,
|
||||||
|
message: "Could not find group".to_string(),
|
||||||
|
})?;
|
||||||
|
backend_handler
|
||||||
|
.delete_group(group_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| LdapError {
|
||||||
|
code: LdapResultCode::OperationsError,
|
||||||
|
message: format!("Error while deleting group: {e:?}"),
|
||||||
|
})?;
|
||||||
|
Ok(vec![make_del_response(
|
||||||
|
LdapResultCode::Success,
|
||||||
|
String::new(),
|
||||||
|
)])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::handler::tests::setup_bound_admin_handler;
|
||||||
|
use chrono::TimeZone;
|
||||||
|
use lldap_domain::{
|
||||||
|
types::{Group, GroupId, User},
|
||||||
|
uuid,
|
||||||
|
};
|
||||||
|
use lldap_domain_model::error::DomainError;
|
||||||
|
use lldap_test_utils::MockTestBackendHandler;
|
||||||
|
use mockall::predicate::eq;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_user() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_get_user_details()
|
||||||
|
.with(eq(UserId::new("bob")))
|
||||||
|
.return_once(|_| {
|
||||||
|
Ok(User {
|
||||||
|
user_id: UserId::new("bob"),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
mock.expect_delete_user()
|
||||||
|
.with(eq(UserId::new("bob")))
|
||||||
|
.times(1)
|
||||||
|
.return_once(|_| Ok(()));
|
||||||
|
let mut ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let request = LdapOp::DelRequest("uid=bob,ou=people,dc=example,dc=com".to_owned());
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.handle_ldap_message(request).await,
|
||||||
|
Some(vec![make_del_response(
|
||||||
|
LdapResultCode::Success,
|
||||||
|
String::new()
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_group() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_list_groups()
|
||||||
|
.with(eq(Some(GroupRequestFilter::DisplayName(GroupName::from(
|
||||||
|
"bob",
|
||||||
|
)))))
|
||||||
|
.return_once(|_| {
|
||||||
|
Ok(vec![Group {
|
||||||
|
id: GroupId(34),
|
||||||
|
display_name: GroupName::from("bob"),
|
||||||
|
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
|
||||||
|
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
|
||||||
|
users: Vec::new(),
|
||||||
|
attributes: Vec::new(),
|
||||||
|
}])
|
||||||
|
});
|
||||||
|
mock.expect_delete_group()
|
||||||
|
.with(eq(GroupId(34)))
|
||||||
|
.times(1)
|
||||||
|
.return_once(|_| Ok(()));
|
||||||
|
let mut ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let request = LdapOp::DelRequest("uid=bob,ou=groups,dc=example,dc=com".to_owned());
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.handle_ldap_message(request).await,
|
||||||
|
Some(vec![make_del_response(
|
||||||
|
LdapResultCode::Success,
|
||||||
|
String::new()
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_user_not_found() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_get_user_details()
|
||||||
|
.with(eq(UserId::new("bob")))
|
||||||
|
.return_once(|_| Err(DomainError::EntityNotFound("No such user".to_string())));
|
||||||
|
let mut ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let request = LdapOp::DelRequest("uid=bob,ou=people,dc=example,dc=com".to_owned());
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.handle_ldap_message(request).await,
|
||||||
|
Some(vec![make_del_response(
|
||||||
|
LdapResultCode::NoSuchObject,
|
||||||
|
"Could not find user".to_string()
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_user_lookup_error() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_get_user_details()
|
||||||
|
.with(eq(UserId::new("bob")))
|
||||||
|
.return_once(|_| Err(DomainError::InternalError("WTF?".to_string())));
|
||||||
|
let mut ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let request = LdapOp::DelRequest("uid=bob,ou=people,dc=example,dc=com".to_owned());
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.handle_ldap_message(request).await,
|
||||||
|
Some(vec![make_del_response(
|
||||||
|
LdapResultCode::OperationsError,
|
||||||
|
r#"Error while finding user: InternalError("WTF?")"#.to_string()
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_user_deletion_error() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_get_user_details()
|
||||||
|
.with(eq(UserId::new("bob")))
|
||||||
|
.return_once(|_| {
|
||||||
|
Ok(User {
|
||||||
|
user_id: UserId::new("bob"),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
mock.expect_delete_user()
|
||||||
|
.with(eq(UserId::new("bob")))
|
||||||
|
.times(1)
|
||||||
|
.return_once(|_| Err(DomainError::InternalError("WTF?".to_string())));
|
||||||
|
let mut ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let request = LdapOp::DelRequest("uid=bob,ou=people,dc=example,dc=com".to_owned());
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.handle_ldap_message(request).await,
|
||||||
|
Some(vec![make_del_response(
|
||||||
|
LdapResultCode::OperationsError,
|
||||||
|
r#"Error while deleting user: InternalError("WTF?")"#.to_string()
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_group_not_found() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_list_groups()
|
||||||
|
.with(eq(Some(GroupRequestFilter::DisplayName(GroupName::from(
|
||||||
|
"bob",
|
||||||
|
)))))
|
||||||
|
.return_once(|_| Ok(vec![]));
|
||||||
|
let mut ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let request = LdapOp::DelRequest("uid=bob,ou=groups,dc=example,dc=com".to_owned());
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.handle_ldap_message(request).await,
|
||||||
|
Some(vec![make_del_response(
|
||||||
|
LdapResultCode::NoSuchObject,
|
||||||
|
"Could not find group".to_string()
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_group_lookup_error() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_list_groups()
|
||||||
|
.with(eq(Some(GroupRequestFilter::DisplayName(GroupName::from(
|
||||||
|
"bob",
|
||||||
|
)))))
|
||||||
|
.return_once(|_| Err(DomainError::InternalError("WTF?".to_string())));
|
||||||
|
let mut ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let request = LdapOp::DelRequest("uid=bob,ou=groups,dc=example,dc=com".to_owned());
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.handle_ldap_message(request).await,
|
||||||
|
Some(vec![make_del_response(
|
||||||
|
LdapResultCode::OperationsError,
|
||||||
|
r#"Error while finding group: InternalError("WTF?")"#.to_string()
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_group_deletion_error() {
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_list_groups()
|
||||||
|
.with(eq(Some(GroupRequestFilter::DisplayName(GroupName::from(
|
||||||
|
"bob",
|
||||||
|
)))))
|
||||||
|
.return_once(|_| {
|
||||||
|
Ok(vec![Group {
|
||||||
|
id: GroupId(34),
|
||||||
|
display_name: GroupName::from("bob"),
|
||||||
|
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
|
||||||
|
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
|
||||||
|
users: Vec::new(),
|
||||||
|
attributes: Vec::new(),
|
||||||
|
}])
|
||||||
|
});
|
||||||
|
mock.expect_delete_group()
|
||||||
|
.with(eq(GroupId(34)))
|
||||||
|
.times(1)
|
||||||
|
.return_once(|_| Err(DomainError::InternalError("WTF?".to_string())));
|
||||||
|
let mut ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let request = LdapOp::DelRequest("uid=bob,ou=groups,dc=example,dc=com".to_owned());
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.handle_ldap_message(request).await,
|
||||||
|
Some(vec![make_del_response(
|
||||||
|
LdapResultCode::OperationsError,
|
||||||
|
r#"Error while deleting group: InternalError("WTF?")"#.to_string()
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,459 @@
|
|||||||
|
use crate::{
|
||||||
|
compare,
|
||||||
|
core::{
|
||||||
|
error::{LdapError, LdapResult},
|
||||||
|
utils::{LdapInfo, parse_distinguished_name},
|
||||||
|
},
|
||||||
|
create, delete, modify,
|
||||||
|
password::{self, do_password_modification},
|
||||||
|
search::{
|
||||||
|
self, is_root_dse_request, is_subschema_entry_request, make_ldap_subschema_entry,
|
||||||
|
make_search_error, make_search_request, make_search_success, root_dse_response,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use ldap3_proto::proto::{
|
||||||
|
LdapAddRequest, LdapBindRequest, LdapBindResponse, LdapCompareRequest, LdapExtendedRequest,
|
||||||
|
LdapExtendedResponse, LdapFilter, LdapModifyRequest, LdapOp, LdapPasswordModifyRequest,
|
||||||
|
LdapResult as LdapResultOp, LdapResultCode, LdapSearchRequest, OID_PASSWORD_MODIFY, OID_WHOAMI,
|
||||||
|
};
|
||||||
|
use lldap_access_control::AccessControlledBackendHandler;
|
||||||
|
use lldap_auth::access_control::ValidationResults;
|
||||||
|
use lldap_domain::{public_schema::PublicSchema, types::AttributeName};
|
||||||
|
use lldap_domain_handlers::handler::{BackendHandler, LoginHandler, ReadSchemaBackendHandler};
|
||||||
|
use lldap_opaque_handler::OpaqueHandler;
|
||||||
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
|
use super::delete::make_del_response;
|
||||||
|
|
||||||
|
pub(crate) fn make_add_response(code: LdapResultCode, message: String) -> LdapOp {
|
||||||
|
LdapOp::AddResponse(LdapResultOp {
|
||||||
|
code,
|
||||||
|
matcheddn: "".to_string(),
|
||||||
|
message,
|
||||||
|
referral: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn make_extended_response(code: LdapResultCode, message: String) -> LdapOp {
|
||||||
|
LdapOp::ExtendedResponse(LdapExtendedResponse {
|
||||||
|
res: LdapResultOp {
|
||||||
|
code,
|
||||||
|
matcheddn: "".to_string(),
|
||||||
|
message,
|
||||||
|
referral: vec![],
|
||||||
|
},
|
||||||
|
name: None,
|
||||||
|
value: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn make_modify_response(code: LdapResultCode, message: String) -> LdapOp {
|
||||||
|
LdapOp::ModifyResponse(LdapResultOp {
|
||||||
|
code,
|
||||||
|
matcheddn: "".to_string(),
|
||||||
|
message,
|
||||||
|
referral: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LdapHandler<Backend> {
|
||||||
|
user_info: Option<ValidationResults>,
|
||||||
|
backend_handler: AccessControlledBackendHandler<Backend>,
|
||||||
|
ldap_info: LdapInfo,
|
||||||
|
session_uuid: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Backend> LdapHandler<Backend> {
|
||||||
|
pub fn session_uuid(&self) -> &uuid::Uuid {
|
||||||
|
&self.session_uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Backend: LoginHandler> LdapHandler<Backend> {
|
||||||
|
pub fn get_login_handler(&self) -> &(impl LoginHandler + use<Backend>) {
|
||||||
|
self.backend_handler.unsafe_get_handler()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Backend: OpaqueHandler> LdapHandler<Backend> {
|
||||||
|
pub fn get_opaque_handler(&self) -> &(impl OpaqueHandler + use<Backend>) {
|
||||||
|
self.backend_handler.unsafe_get_handler()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Credentials<'s> {
|
||||||
|
Bound(&'s ValidationResults),
|
||||||
|
Unbound(Vec<LdapOp>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend> {
|
||||||
|
pub fn new(
|
||||||
|
backend_handler: AccessControlledBackendHandler<Backend>,
|
||||||
|
mut ldap_base_dn: String,
|
||||||
|
ignored_user_attributes: Vec<AttributeName>,
|
||||||
|
ignored_group_attributes: Vec<AttributeName>,
|
||||||
|
session_uuid: uuid::Uuid,
|
||||||
|
) -> Self {
|
||||||
|
ldap_base_dn.make_ascii_lowercase();
|
||||||
|
Self {
|
||||||
|
user_info: None,
|
||||||
|
backend_handler,
|
||||||
|
ldap_info: LdapInfo {
|
||||||
|
base_dn: parse_distinguished_name(&ldap_base_dn).unwrap_or_else(|_| {
|
||||||
|
panic!("Invalid value for ldap_base_dn in configuration: {ldap_base_dn}")
|
||||||
|
}),
|
||||||
|
base_dn_str: ldap_base_dn,
|
||||||
|
ignored_user_attributes,
|
||||||
|
ignored_group_attributes,
|
||||||
|
},
|
||||||
|
session_uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn new_for_tests(backend_handler: Backend, ldap_base_dn: &str) -> Self {
|
||||||
|
Self::new(
|
||||||
|
AccessControlledBackendHandler::new(backend_handler),
|
||||||
|
ldap_base_dn.to_string(),
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_credentials(&self) -> Credentials<'_> {
|
||||||
|
match self.user_info.as_ref() {
|
||||||
|
Some(user_info) => Credentials::Bound(user_info),
|
||||||
|
None => Credentials::Unbound(vec![make_extended_response(
|
||||||
|
LdapResultCode::InsufficentAccessRights,
|
||||||
|
"No user currently bound".to_string(),
|
||||||
|
)]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn do_search_or_dse(&self, request: &LdapSearchRequest) -> LdapResult<Vec<LdapOp>> {
|
||||||
|
if is_root_dse_request(request) {
|
||||||
|
debug!("rootDSE request");
|
||||||
|
return Ok(vec![
|
||||||
|
root_dse_response(&self.ldap_info.base_dn_str),
|
||||||
|
make_search_success(),
|
||||||
|
]);
|
||||||
|
} else if is_subschema_entry_request(request) {
|
||||||
|
// See RFC4512 section 4.4 "Subschema discovery"
|
||||||
|
debug!("Schema request");
|
||||||
|
let backend_handler = self
|
||||||
|
.user_info
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|u| self.backend_handler.get_schema_only_handler(u))
|
||||||
|
.ok_or_else(|| LdapError {
|
||||||
|
code: LdapResultCode::InsufficentAccessRights,
|
||||||
|
message: "No user currently bound".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let schema = backend_handler.get_schema().await.map_err(|e| LdapError {
|
||||||
|
code: LdapResultCode::OperationsError,
|
||||||
|
message: format!("Unable to get schema: {e:#}"),
|
||||||
|
})?;
|
||||||
|
return Ok(vec![
|
||||||
|
make_ldap_subschema_entry(PublicSchema::from(schema)),
|
||||||
|
make_search_success(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
self.do_search(request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, level = "debug")]
|
||||||
|
async fn do_search(&self, request: &LdapSearchRequest) -> LdapResult<Vec<LdapOp>> {
|
||||||
|
let user_info = self.user_info.as_ref().ok_or_else(|| LdapError {
|
||||||
|
code: LdapResultCode::InsufficentAccessRights,
|
||||||
|
message: "No user currently bound".to_string(),
|
||||||
|
})?;
|
||||||
|
let backend_handler = self
|
||||||
|
.backend_handler
|
||||||
|
.get_user_restricted_lister_handler(user_info);
|
||||||
|
search::do_search(&backend_handler, &self.ldap_info, request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, level = "debug", fields(dn = %request.dn))]
|
||||||
|
pub async fn do_bind(&mut self, request: &LdapBindRequest) -> Vec<LdapOp> {
|
||||||
|
let (code, message) =
|
||||||
|
match password::do_bind(&self.ldap_info, request, self.get_login_handler()).await {
|
||||||
|
Ok(user_id) => {
|
||||||
|
self.user_info = self
|
||||||
|
.backend_handler
|
||||||
|
.get_permissions_for_user(user_id)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
debug!("Success!");
|
||||||
|
(LdapResultCode::Success, "".to_string())
|
||||||
|
}
|
||||||
|
Err(err) => (err.code, err.message),
|
||||||
|
};
|
||||||
|
vec![LdapOp::BindResponse(LdapBindResponse {
|
||||||
|
res: LdapResultOp {
|
||||||
|
code,
|
||||||
|
matcheddn: "".to_string(),
|
||||||
|
message,
|
||||||
|
referral: vec![],
|
||||||
|
},
|
||||||
|
saslcreds: None,
|
||||||
|
})]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, level = "debug")]
|
||||||
|
async fn do_extended_request(&self, request: &LdapExtendedRequest) -> Vec<LdapOp> {
|
||||||
|
match request.name.as_str() {
|
||||||
|
OID_PASSWORD_MODIFY => match LdapPasswordModifyRequest::try_from(request) {
|
||||||
|
Ok(password_request) => {
|
||||||
|
let credentials = match self.get_credentials() {
|
||||||
|
Credentials::Bound(cred) => cred,
|
||||||
|
Credentials::Unbound(err) => return err,
|
||||||
|
};
|
||||||
|
do_password_modification(
|
||||||
|
credentials,
|
||||||
|
&self.ldap_info,
|
||||||
|
&self.backend_handler,
|
||||||
|
self.get_opaque_handler(),
|
||||||
|
&password_request,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e: LdapError| vec![make_extended_response(e.code, e.message)])
|
||||||
|
}
|
||||||
|
Err(e) => vec![make_extended_response(
|
||||||
|
LdapResultCode::ProtocolError,
|
||||||
|
format!("Error while parsing password modify request: {e:#?}"),
|
||||||
|
)],
|
||||||
|
},
|
||||||
|
OID_WHOAMI => {
|
||||||
|
let authz_id = self
|
||||||
|
.user_info
|
||||||
|
.as_ref()
|
||||||
|
.map(|user_info| {
|
||||||
|
format!(
|
||||||
|
"dn:uid={},ou=people,{}",
|
||||||
|
user_info.user.as_str(),
|
||||||
|
self.ldap_info.base_dn_str
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
vec![make_extended_response(LdapResultCode::Success, authz_id)]
|
||||||
|
}
|
||||||
|
_ => vec![make_extended_response(
|
||||||
|
LdapResultCode::UnwillingToPerform,
|
||||||
|
format!("Unsupported extended operation: {}", &request.name),
|
||||||
|
)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, level = "debug", fields(dn = %request.dn))]
|
||||||
|
pub async fn do_modify_request(&self, request: &LdapModifyRequest) -> Vec<LdapOp> {
|
||||||
|
let credentials = match self.get_credentials() {
|
||||||
|
Credentials::Bound(cred) => cred,
|
||||||
|
Credentials::Unbound(err) => return err,
|
||||||
|
};
|
||||||
|
modify::handle_modify_request(
|
||||||
|
self.get_opaque_handler(),
|
||||||
|
|credentials, user_id| {
|
||||||
|
self.backend_handler
|
||||||
|
.get_readable_handler(credentials, &user_id)
|
||||||
|
},
|
||||||
|
&self.ldap_info,
|
||||||
|
credentials,
|
||||||
|
request,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e: LdapError| vec![make_modify_response(e.code, e.message)])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, level = "debug")]
|
||||||
|
pub async fn create_user_or_group(&self, request: LdapAddRequest) -> LdapResult<Vec<LdapOp>> {
|
||||||
|
let backend_handler = self
|
||||||
|
.user_info
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|u| self.backend_handler.get_admin_handler(u))
|
||||||
|
.ok_or_else(|| LdapError {
|
||||||
|
code: LdapResultCode::InsufficentAccessRights,
|
||||||
|
message: "Unauthorized write".to_string(),
|
||||||
|
})?;
|
||||||
|
create::create_user_or_group(backend_handler, &self.ldap_info, request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, level = "debug")]
|
||||||
|
pub async fn delete_user_or_group(&self, request: String) -> LdapResult<Vec<LdapOp>> {
|
||||||
|
let backend_handler = self
|
||||||
|
.user_info
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|u| self.backend_handler.get_admin_handler(u))
|
||||||
|
.ok_or_else(|| LdapError {
|
||||||
|
code: LdapResultCode::InsufficentAccessRights,
|
||||||
|
message: "Unauthorized write".to_string(),
|
||||||
|
})?;
|
||||||
|
delete::delete_user_or_group(backend_handler, &self.ldap_info, request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, level = "debug")]
|
||||||
|
pub async fn do_compare(&self, request: LdapCompareRequest) -> LdapResult<Vec<LdapOp>> {
|
||||||
|
let req = make_search_request::<String>(
|
||||||
|
&self.ldap_info.base_dn_str,
|
||||||
|
LdapFilter::Equality("dn".to_string(), request.dn.to_string()),
|
||||||
|
vec![request.atype.clone()],
|
||||||
|
);
|
||||||
|
compare::compare(
|
||||||
|
request,
|
||||||
|
self.do_search(&req).await?,
|
||||||
|
&self.ldap_info.base_dn_str,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_ldap_message(&mut self, ldap_op: LdapOp) -> Option<Vec<LdapOp>> {
|
||||||
|
Some(match ldap_op {
|
||||||
|
LdapOp::BindRequest(request) => self.do_bind(&request).await,
|
||||||
|
LdapOp::SearchRequest(request) => self
|
||||||
|
.do_search_or_dse(&request)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e: LdapError| vec![make_search_error(e.code, e.message)]),
|
||||||
|
LdapOp::UnbindRequest => {
|
||||||
|
debug!(
|
||||||
|
"Unbind request for {}",
|
||||||
|
self.user_info
|
||||||
|
.as_ref()
|
||||||
|
.map(|u| u.user.as_str())
|
||||||
|
.unwrap_or("<not bound>"),
|
||||||
|
);
|
||||||
|
self.user_info = None;
|
||||||
|
// No need to notify on unbind (per rfc4511)
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
LdapOp::ModifyRequest(request) => self.do_modify_request(&request).await,
|
||||||
|
LdapOp::ExtendedRequest(request) => self.do_extended_request(&request).await,
|
||||||
|
LdapOp::AddRequest(request) => self
|
||||||
|
.create_user_or_group(request)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e: LdapError| vec![make_add_response(e.code, e.message)]),
|
||||||
|
LdapOp::DelRequest(request) => self
|
||||||
|
.delete_user_or_group(request)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e: LdapError| vec![make_del_response(e.code, e.message)]),
|
||||||
|
LdapOp::CompareRequest(request) => self
|
||||||
|
.do_compare(request)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e: LdapError| vec![make_search_error(e.code, e.message)]),
|
||||||
|
op => vec![make_extended_response(
|
||||||
|
LdapResultCode::UnwillingToPerform,
|
||||||
|
format!("Unsupported operation: {op:#?}"),
|
||||||
|
)],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::password::tests::make_bind_success;
|
||||||
|
use chrono::TimeZone;
|
||||||
|
use ldap3_proto::proto::{LdapBindCred, LdapWhoamiRequest};
|
||||||
|
use lldap_domain::{
|
||||||
|
types::{GroupDetails, GroupId, UserId},
|
||||||
|
uuid,
|
||||||
|
};
|
||||||
|
use lldap_domain_handlers::handler::*;
|
||||||
|
use lldap_test_utils::{MockTestBackendHandler, setup_default_schema};
|
||||||
|
use mockall::predicate::eq;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use tokio;
|
||||||
|
|
||||||
|
pub fn make_user_search_request<S: Into<String>>(
|
||||||
|
filter: LdapFilter,
|
||||||
|
attrs: Vec<S>,
|
||||||
|
) -> LdapSearchRequest {
|
||||||
|
make_search_request::<S>("ou=people,Dc=example,dc=com", filter, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_group_search_request<S: Into<String>>(
|
||||||
|
filter: LdapFilter,
|
||||||
|
attrs: Vec<S>,
|
||||||
|
) -> LdapSearchRequest {
|
||||||
|
make_search_request::<S>("ou=groups,dc=example,dc=com", filter, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn setup_bound_handler_with_group(
|
||||||
|
mut mock: MockTestBackendHandler,
|
||||||
|
group: &str,
|
||||||
|
) -> LdapHandler<MockTestBackendHandler> {
|
||||||
|
mock.expect_bind()
|
||||||
|
.with(eq(BindRequest {
|
||||||
|
name: UserId::new("test"),
|
||||||
|
password: "pass".to_string(),
|
||||||
|
}))
|
||||||
|
.return_once(|_| Ok(()));
|
||||||
|
let group = group.to_string();
|
||||||
|
mock.expect_get_user_groups()
|
||||||
|
.with(eq(UserId::new("test")))
|
||||||
|
.return_once(|_| {
|
||||||
|
let mut set = HashSet::new();
|
||||||
|
set.insert(GroupDetails {
|
||||||
|
group_id: GroupId(42),
|
||||||
|
display_name: group.into(),
|
||||||
|
creation_date: chrono::Utc.timestamp_opt(42, 42).unwrap().naive_utc(),
|
||||||
|
uuid: uuid!("a1a2a3a4b1b2c1c2d1d2d3d4d5d6d7d8"),
|
||||||
|
attributes: Vec::new(),
|
||||||
|
});
|
||||||
|
Ok(set)
|
||||||
|
});
|
||||||
|
setup_default_schema(&mut mock);
|
||||||
|
let mut ldap_handler = LdapHandler::new_for_tests(mock, "dc=Example,dc=com");
|
||||||
|
let request = LdapBindRequest {
|
||||||
|
dn: "uid=test,ou=people,dc=example,dc=coM".to_string(),
|
||||||
|
cred: LdapBindCred::Simple("pass".to_string()),
|
||||||
|
};
|
||||||
|
assert_eq!(ldap_handler.do_bind(&request).await, make_bind_success());
|
||||||
|
ldap_handler
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn setup_bound_readonly_handler(
|
||||||
|
mock: MockTestBackendHandler,
|
||||||
|
) -> LdapHandler<MockTestBackendHandler> {
|
||||||
|
setup_bound_handler_with_group(mock, "lldap_strict_readonly").await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn setup_bound_password_manager_handler(
|
||||||
|
mock: MockTestBackendHandler,
|
||||||
|
) -> LdapHandler<MockTestBackendHandler> {
|
||||||
|
setup_bound_handler_with_group(mock, "lldap_password_manager").await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn setup_bound_admin_handler(
|
||||||
|
mock: MockTestBackendHandler,
|
||||||
|
) -> LdapHandler<MockTestBackendHandler> {
|
||||||
|
setup_bound_handler_with_group(mock, "lldap_admin").await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_whoami_empty() {
|
||||||
|
let mut ldap_handler =
|
||||||
|
LdapHandler::new_for_tests(MockTestBackendHandler::new(), "dc=example,dc=com");
|
||||||
|
let request = LdapOp::ExtendedRequest(LdapWhoamiRequest {}.into());
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.handle_ldap_message(request).await,
|
||||||
|
Some(vec![make_extended_response(
|
||||||
|
LdapResultCode::Success,
|
||||||
|
"".to_string(),
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_whoami_bound() {
|
||||||
|
let mock = MockTestBackendHandler::new();
|
||||||
|
let mut ldap_handler = setup_bound_admin_handler(mock).await;
|
||||||
|
let request = LdapOp::ExtendedRequest(LdapWhoamiRequest {}.into());
|
||||||
|
assert_eq!(
|
||||||
|
ldap_handler.handle_ldap_message(request).await,
|
||||||
|
Some(vec![make_extended_response(
|
||||||
|
LdapResultCode::Success,
|
||||||
|
"dn:uid=test,ou=people,dc=example,dc=com".to_string(),
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user