4 Commits

Author SHA1 Message Date
Dedy Martadinata S
1b2dfbe52e Publish job using matrix
Change publish job with matrix, don't fail if another job fail.
2023-05-12 20:41:35 +07:00
Valentin Tolmer
e1aa2bfb18 cargo: depend on the published version of lldap_auth 2023-05-12 15:37:39 +02:00
Valentin Tolmer
1377d5aed9 github: create actions to publish crates 2023-05-12 15:25:59 +02:00
Valentin Tolmer
fa0185af5e cargo: set specific versions for each dependency 2023-05-12 15:25:59 +02:00
163 changed files with 2940 additions and 12124 deletions

View File

@@ -1,9 +1,7 @@
FROM rust:1.74
FROM rust:1.66
ARG USERNAME=lldapdev
# We need to keep the user as 1001 to match the GitHub runner's UID.
# See https://github.com/actions/checkout/issues/956.
ARG USER_UID=1001
ARG USER_UID=1000
ARG USER_GID=$USER_UID
# Create the user
@@ -23,4 +21,4 @@ RUN RUSTFLAGS=-Ctarget-feature=-crt-static cargo install wasm-pack \
USER $USERNAME
ENV CARGO_HOME=/home/$USERNAME/.cargo
ENV SHELL=/bin/bash
ENV SHELL=/bin/bash

2
.gitattributes vendored
View File

@@ -1,4 +1,4 @@
example_configs/** linguist-documentation
example-configs/** linguist-documentation
docs/** linguist-documentation
*.md linguist-documentation
lldap_config.docker_template.toml linguist-documentation

5
.github/FUNDING.yml vendored
View File

@@ -1,5 +0,0 @@
# These are supported funding model platforms
github: [lldap]
custom: ['https://bmc.link/nitnelave']

View File

@@ -1,29 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Logs**
If applicable, add logs to explain the problem.
LLDAP should be started in verbose mode (`LLDAP_VERBOSE=true` env variable, or `verbose = true` in the config). Include the logs in triple-backtick "```"
If integrating with another service, please add its configuration (paste it or screenshot it) as well as any useful logs or screenshots (showing the error, for instance).
**Additional context**
Add any other context about the problem here.

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE REQUEST]"
labels: enhancement
assignees: ''
---
**Motivation**
Why do you want the feature? What problem do you have, what use cases would it enable?
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered. You can include workarounds that are currently possible.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,25 +0,0 @@
---
name: Integration request
about: Request for integration with a service
title: "[INTEGRATION]"
labels: integration
assignees: ''
---
**Checklist**
- [ ] Check if there is already an [example config](https://github.com/lldap/lldap/tree/main/example_configs) for it.
- [ ] Try to figure out the configuration values for the new service yourself.
- You can use other example configs for inspiration.
- If you're having trouble, you can ask on [Discord](https://discord.gg/h5PEdRMNyP) or create an issue.
- If you succeed, make sure to contribute an example configuration, or a configuration guide.
- If you hit a block because of an unimplemented feature, create an issue.
**Description of the service**
Quick summary of what the service is and how it's using LDAP. Link to the service's documentation on configuring LDAP.
**What you've tried**
A sample configuration that you've tried.
**What's not working**
Error logs, error screenshots, features that are not working, missing features.

11
.github/codecov.yml vendored
View File

@@ -1,19 +1,10 @@
codecov:
require_ci_to_pass: yes
comment:
layout: "header,diff,files"
layout: "diff,flags"
require_changes: true
require_base: true
require_head: true
coverage:
status:
project:
default:
target: "75%"
threshold: "0.1%"
removed_code_behavior: adjust_base
github_checks:
annotations: true
ignore:
- "app"
- "docs"

View File

@@ -1,6 +1,72 @@
FROM localhost:5000/lldap/lldap:alpine-base
# Taken directly from https://github.com/tianon/gosu/blob/master/INSTALL.md
ENV GOSU_VERSION 1.17
FROM debian:bullseye AS lldap
ARG DEBIAN_FRONTEND=noninteractive
ARG TARGETPLATFORM
RUN apt update && apt install -y wget
WORKDIR /dim
COPY bin/ bin/
COPY web/ web/
RUN mkdir -p target/
RUN mkdir -p /lldap/app
RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \
mv bin/x86_64-unknown-linux-musl-lldap-bin/lldap target/lldap && \
mv bin/x86_64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/x86_64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
; fi
RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
mv bin/aarch64-unknown-linux-musl-lldap-bin/lldap target/lldap && \
mv bin/aarch64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/aarch64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
; fi
RUN if [ "${TARGETPLATFORM}" = "linux/arm/v7" ]; then \
mv bin/armv7-unknown-linux-gnueabihf-lldap-bin/lldap target/lldap && \
mv bin/armv7-unknown-linux-gnueabihf-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/armv7-unknown-linux-gnueabihf-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
; fi
# Web and App dir
COPY docker-entrypoint.sh /docker-entrypoint.sh
COPY lldap_config.docker_template.toml /lldap/
COPY web/index_local.html web/index.html
RUN cp target/lldap /lldap/ && \
cp target/lldap_migration_tool /lldap/ && \
cp target/lldap_set_password /lldap/ && \
cp -R web/index.html \
web/pkg \
web/static \
/lldap/app/
WORKDIR /lldap
RUN set -x \
&& for file in $(cat /lldap/app/static/libraries.txt); do wget -P app/static "$file"; done \
&& for file in $(cat /lldap/app/static/fonts/fonts.txt); do wget -P app/static/fonts "$file"; done \
&& chmod a+r -R .
FROM alpine:3.16
WORKDIR /app
ENV UID=1000
ENV GID=1000
ENV USER=lldap
ENV GOSU_VERSION 1.14
# Fetch gosu from git
RUN set -eux; \
\
apk add --no-cache --virtual .gosu-deps \
@@ -17,7 +83,7 @@ RUN set -eux; \
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
gpgconf --kill all; \
command -v gpgconf && gpgconf --kill all || :; \
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
\
# clean up fetch dependencies
@@ -27,4 +93,22 @@ RUN set -eux; \
# verify that the binary works
gosu --version; \
gosu nobody true
COPY --chown=$USER:$USER docker-entrypoint.sh /docker-entrypoint.sh
RUN apk add --no-cache tini ca-certificates bash tzdata && \
addgroup -g $GID $USER && \
adduser \
--disabled-password \
--gecos "" \
--home "$(pwd)" \
--ingroup "$USER" \
--no-create-home \
--uid "$UID" \
"$USER" && \
mkdir -p /data && \
chown $USER:$USER /data
COPY --from=lldap --chown=$USER:$USER /lldap /app
COPY --from=lldap --chown=$USER:$USER /docker-entrypoint.sh /docker-entrypoint.sh
VOLUME ["/data"]
WORKDIR /app
ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"]
CMD ["run", "--config-file", "/data/lldap_config.toml"]
HEALTHCHECK CMD ["/app/lldap", "healthcheck", "--config-file", "/data/lldap_config.toml"]

View File

@@ -1,84 +0,0 @@
FROM debian:bullseye AS lldap
ARG DEBIAN_FRONTEND=noninteractive
ARG TARGETPLATFORM
RUN apt update && apt install -y wget
WORKDIR /dim
COPY bin/ bin/
COPY web/ web/
RUN mkdir -p target/
RUN mkdir -p /lldap/app
RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \
mv bin/x86_64-unknown-linux-musl-lldap-bin/lldap target/lldap && \
mv bin/x86_64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/x86_64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
; fi
RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
mv bin/aarch64-unknown-linux-musl-lldap-bin/lldap target/lldap && \
mv bin/aarch64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/aarch64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
; fi
RUN if [ "${TARGETPLATFORM}" = "linux/arm/v7" ]; then \
mv bin/armv7-unknown-linux-musleabihf-lldap-bin/lldap target/lldap && \
mv bin/armv7-unknown-linux-musleabihf-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/armv7-unknown-linux-musleabihf-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
; fi
# Web and App dir
COPY lldap_config.docker_template.toml /lldap/
COPY web/index_local.html web/index.html
RUN cp target/lldap /lldap/ && \
cp target/lldap_migration_tool /lldap/ && \
cp target/lldap_set_password /lldap/ && \
cp -R web/index.html \
web/pkg \
web/static \
/lldap/app/
WORKDIR /lldap
RUN set -x \
&& for file in $(cat /lldap/app/static/libraries.txt); do wget -P app/static "$file"; done \
&& for file in $(cat /lldap/app/static/fonts/fonts.txt); do wget -P app/static/fonts "$file"; done \
&& chmod a+r -R .
FROM alpine:3.16
WORKDIR /app
ENV UID=1000
ENV GID=1000
ENV USER=lldap
RUN apk add --no-cache tini ca-certificates bash tzdata && \
addgroup -g $GID $USER && \
adduser \
--disabled-password \
--gecos "" \
--home "$(pwd)" \
--ingroup "$USER" \
--no-create-home \
--uid "$UID" \
"$USER" && \
mkdir -p /data && \
chown $USER:$USER /data
COPY --from=lldap --chown=$USER:$USER /lldap /app
VOLUME ["/data"]
HEALTHCHECK CMD ["/app/lldap", "healthcheck", "--config-file", "/data/lldap_config.toml"]
WORKDIR /app
ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"]
CMD ["run", "--config-file", "/data/lldap_config.toml"]

View File

@@ -1,3 +0,0 @@
FROM localhost:5000/lldap/lldap:alpine-base
COPY --chown=$USER:$USER docker-entrypoint-rootless.sh /docker-entrypoint.sh
USER $USER

View File

@@ -1,31 +1,79 @@
FROM localhost:5000/lldap/lldap:debian-base
# Taken directly from https://github.com/tianon/gosu/blob/master/INSTALL.md
ENV GOSU_VERSION 1.17
RUN set -eux; \
# save list of currently installed packages for later so we can clean up
savedAptMark="$(apt-mark showmanual)"; \
apt-get update; \
apt-get install -y --no-install-recommends ca-certificates gnupg wget; \
rm -rf /var/lib/apt/lists/*; \
\
dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
\
# verify the signature
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
gpgconf --kill all; \
rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
\
# clean up fetch dependencies
apt-mark auto '.*' > /dev/null; \
[ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; \
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
\
chmod +x /usr/local/bin/gosu; \
# verify that the binary works
gosu --version; \
gosu nobody true
COPY --chown=$USER:$USER docker-entrypoint.sh /docker-entrypoint.sh
FROM debian:bullseye AS lldap
ARG DEBIAN_FRONTEND=noninteractive
ARG TARGETPLATFORM
RUN apt update && apt install -y wget
WORKDIR /dim
COPY bin/ bin/
COPY web/ web/
RUN mkdir -p target/
RUN mkdir -p /lldap/app
RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \
mv bin/x86_64-unknown-linux-musl-lldap-bin/lldap target/lldap && \
mv bin/x86_64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/x86_64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
; fi
RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
mv bin/aarch64-unknown-linux-musl-lldap-bin/lldap target/lldap && \
mv bin/aarch64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/aarch64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
; fi
RUN if [ "${TARGETPLATFORM}" = "linux/arm/v7" ]; then \
mv bin/armv7-unknown-linux-gnueabihf-lldap-bin/lldap target/lldap && \
mv bin/armv7-unknown-linux-gnueabihf-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/armv7-unknown-linux-gnueabihf-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
; fi
# Web and App dir
COPY docker-entrypoint.sh /docker-entrypoint.sh
COPY lldap_config.docker_template.toml /lldap/
COPY web/index_local.html web/index.html
RUN cp target/lldap /lldap/ && \
cp target/lldap_migration_tool /lldap/ && \
cp target/lldap_set_password /lldap/ && \
cp -R web/index.html \
web/pkg \
web/static \
/lldap/app/
WORKDIR /lldap
RUN set -x \
&& for file in $(cat /lldap/app/static/libraries.txt); do wget -P app/static "$file"; done \
&& for file in $(cat /lldap/app/static/fonts/fonts.txt); do wget -P app/static/fonts "$file"; done \
&& chmod a+r -R .
FROM debian:bullseye-slim
ENV UID=1000
ENV GID=1000
ENV USER=lldap
RUN apt update && \
apt install -y --no-install-recommends tini openssl ca-certificates gosu tzdata && \
apt clean && \
rm -rf /var/lib/apt/lists/* && \
groupadd -g $GID $USER && useradd --system -m -g $USER --uid $UID $USER && \
mkdir -p /data && chown $USER:$USER /data
COPY --from=lldap --chown=$USER:$USER /lldap /app
COPY --from=lldap --chown=$USER:$USER /docker-entrypoint.sh /docker-entrypoint.sh
VOLUME ["/data"]
WORKDIR /app
ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"]
CMD ["run", "--config-file", "/data/lldap_config.toml"]
HEALTHCHECK CMD ["/app/lldap", "healthcheck", "--config-file", "/data/lldap_config.toml"]

View File

@@ -1,79 +0,0 @@
FROM debian:bullseye AS lldap
ARG DEBIAN_FRONTEND=noninteractive
ARG TARGETPLATFORM
RUN apt update && apt install -y wget
WORKDIR /dim
COPY bin/ bin/
COPY web/ web/
RUN mkdir -p target/
RUN mkdir -p /lldap/app
RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \
mv bin/x86_64-unknown-linux-musl-lldap-bin/lldap target/lldap && \
mv bin/x86_64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/x86_64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
; fi
RUN if [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \
mv bin/aarch64-unknown-linux-musl-lldap-bin/lldap target/lldap && \
mv bin/aarch64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/aarch64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
; fi
RUN if [ "${TARGETPLATFORM}" = "linux/arm/v7" ]; then \
mv bin/armv7-unknown-linux-musleabihf-lldap-bin/lldap target/lldap && \
mv bin/armv7-unknown-linux-musleabihf-lldap_migration_tool-bin/lldap_migration_tool target/lldap_migration_tool && \
mv bin/armv7-unknown-linux-musleabihf-lldap_set_password-bin/lldap_set_password target/lldap_set_password && \
chmod +x target/lldap && \
chmod +x target/lldap_migration_tool && \
chmod +x target/lldap_set_password && \
ls -la target/ . && \
pwd \
; fi
# Web and App dir
COPY docker-entrypoint.sh /docker-entrypoint.sh
COPY lldap_config.docker_template.toml /lldap/
COPY web/index_local.html web/index.html
RUN cp target/lldap /lldap/ && \
cp target/lldap_migration_tool /lldap/ && \
cp target/lldap_set_password /lldap/ && \
cp -R web/index.html \
web/pkg \
web/static \
/lldap/app/
WORKDIR /lldap
RUN set -x \
&& for file in $(cat /lldap/app/static/libraries.txt); do wget -P app/static "$file"; done \
&& for file in $(cat /lldap/app/static/fonts/fonts.txt); do wget -P app/static/fonts "$file"; done \
&& chmod a+r -R .
FROM debian:bullseye-slim
ENV UID=1000
ENV GID=1000
ENV USER=lldap
RUN apt update && \
apt install -y --no-install-recommends tini openssl ca-certificates tzdata && \
apt clean && \
rm -rf /var/lib/apt/lists/* && \
groupadd -g $GID $USER && useradd --system -m -g $USER --uid $UID $USER && \
mkdir -p /data && chown $USER:$USER /data
COPY --from=lldap --chown=$USER:$USER /lldap /app
COPY --from=lldap --chown=$USER:$USER /docker-entrypoint.sh /docker-entrypoint.sh
VOLUME ["/data"]
WORKDIR /app
ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"]
CMD ["run", "--config-file", "/data/lldap_config.toml"]
HEALTHCHECK CMD ["/app/lldap", "healthcheck", "--config-file", "/data/lldap_config.toml"]

View File

@@ -1,3 +0,0 @@
FROM localhost:5000/lldap/lldap:debian-base
COPY --chown=$USER:$USER docker-entrypoint-rootless.sh /docker-entrypoint.sh
USER $USER

View File

@@ -1,40 +1,45 @@
# Keep tracking base image
FROM rust:1.74-slim-bookworm
FROM rust:1.66-slim-bullseye
# 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/aarch64-linux-musl-cross/:/opt/aarch64-linux-musl-cross/bin/:/opt/x86_64-linux-musl-cross/:/opt/x86_64-linux-musl-cross/bin/:$PATH"
# Set building env
ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse \
CARGO_NET_GIT_FETCH_WITH_CLI=true \
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER=armv7l-linux-musleabihf-gcc \
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-musl-gcc \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=x86_64-linux-musl-gcc \
CC_armv7_unknown_linux_musleabihf=armv7l-linux-musleabihf-gcc \
CC_x86_64_unknown_linux_musl=x86_64-linux-musl-gcc \
CC_aarch64_unknown_linux_musl=aarch64-linux-musl-gcc
### Install Additional Build Tools
### Install build deps x86_64
RUN apt update && \
apt install -y --no-install-recommends curl git wget make perl pkg-config tar jq gzip && \
apt install -y --no-install-recommends curl git wget build-essential make perl pkg-config curl tar jq musl-tools gzip && \
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && \
apt update && \
apt install -y --no-install-recommends nodejs && \
apt clean && \
rm -rf /var/lib/apt/lists/*
### Add musl-gcc aarch64, x86_64 and armv7l
### Install build deps aarch64 build
RUN dpkg --add-architecture arm64 && \
apt update && \
apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libc6-arm64-cross libc6-dev-arm64-cross gzip && \
apt clean && \
rm -rf /var/lib/apt/lists/* && \
rustup target add aarch64-unknown-linux-gnu
### armhf deps
RUN dpkg --add-architecture armhf && \
apt update && \
apt install -y gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf libc6-armhf-cross libc6-dev-armhf-cross gzip && \
apt clean && \
rm -rf /var/lib/apt/lists/* && \
rustup target add armv7-unknown-linux-gnueabihf
### Add musl-gcc aarch64 and x86_64
RUN wget -c https://musl.cc/x86_64-linux-musl-cross.tgz && \
tar zxf ./x86_64-linux-musl-cross.tgz -C /opt && \
wget -c https://musl.cc/aarch64-linux-musl-cross.tgz && \
tar zxf ./aarch64-linux-musl-cross.tgz -C /opt && \
wget -c http://musl.cc/armv7l-linux-musleabihf-cross.tgz && \
tar zxf ./armv7l-linux-musleabihf-cross.tgz -C /opt && \
rm ./x86_64-linux-musl-cross.tgz && \
rm ./aarch64-linux-musl-cross.tgz && \
rm ./armv7l-linux-musleabihf-cross.tgz
rm ./aarch64-linux-musl-cross.tgz
### Add musl target
RUN rustup target add x86_64-unknown-linux-musl && \
rustup target add aarch64-unknown-linux-musl && \
rustup target add armv7-unknown-linux-musleabihf
rustup target add aarch64-unknown-linux-musl
CMD ["bash"]

View File

@@ -30,6 +30,7 @@ env:
# build-ui , create/compile the web
### install wasm
### install rollup
### run app/build.sh
### upload artifacts
@@ -39,10 +40,10 @@ env:
# GitHub actions randomly timeout when downloading musl-gcc, using custom dev image #
# Look into .github/workflows/Dockerfile.dev for development image details #
# Using lldap dev image based on https://hub.docker.com/_/rust and musl-gcc bundled #
# lldap/rust-dev:latest #
#######################################################################################
# Cargo build
### armv7, aarch64 and amd64 is musl based
### Cargo build
### aarch64 and amd64 is musl based
### armv7 is glibc based, musl had issue with time_t when cross compile https://github.com/rust-lang/libc/issues/1848
# build-ui,builds-armhf, build-aarch64, build-amd64 will upload artifacts will be used next job
@@ -50,11 +51,12 @@ env:
### will run lldap with postgres, mariadb and sqlite backend, do selfcheck command.
# Build docker image
### Triplet docker image arch with debian and alpine base
### Triplet docker image arch with debian base
### amd64 & aarch64 with alpine base
# build-docker-image job will fetch artifacts and run Dockerfile.ci then push the image.
### Look into .github/workflows/Dockerfile.ci.debian or .github/workflowds/Dockerfile.ci.alpine
# Create release artifacts
# create release artifacts
### Fetch artifacts
### Clean up web artifact
### Setup folder structure
@@ -84,11 +86,11 @@ jobs:
needs: pre_job
if: ${{ needs.pre_job.outputs.should_skip != 'true' || github.event_name == 'release' }}
container:
image: lldap/rust-dev:latest
image: nitnelave/rust-dev:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.1
- uses: actions/cache@v4
uses: actions/checkout@v3.5.2
- uses: actions/cache@v3
with:
path: |
/usr/local/cargo/bin
@@ -99,6 +101,8 @@ jobs:
key: lldap-ui-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
lldap-ui-
- name: Install rollup (nodejs)
run: npm install -g rollup
- name: Add wasm target (rust)
run: rustup target add wasm32-unknown-unknown
- name: Install wasm-pack with cargo
@@ -110,7 +114,7 @@ jobs:
- name: Check build path
run: ls -al app/
- name: Upload ui artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: ui
path: app/
@@ -121,19 +125,21 @@ jobs:
needs: pre_job
if: ${{ needs.pre_job.outputs.should_skip != 'true' || github.event_name == 'release' }}
strategy:
fail-fast: false
matrix:
target: [armv7-unknown-linux-musleabihf, aarch64-unknown-linux-musl, x86_64-unknown-linux-musl]
target: [armv7-unknown-linux-gnueabihf, aarch64-unknown-linux-musl, x86_64-unknown-linux-musl]
container:
image: lldap/rust-dev:latest
image: nitnelave/rust-dev:latest
env:
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER: arm-linux-gnueabihf-gcc
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: aarch64-linux-musl-gcc
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER: x86_64-linux-musl-gcc
CARGO_TERM_COLOR: always
RUSTFLAGS: -Ctarget-feature=+crt-static
CARGO_HOME: ${GITHUB_WORKSPACE}/.cargo
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.1
- uses: actions/cache@v4
uses: actions/checkout@v3.5.2
- uses: actions/cache@v3
with:
path: |
.cargo/bin
@@ -149,17 +155,17 @@ jobs:
- name: Check path
run: ls -al target/release
- name: Upload ${{ matrix.target}} lldap artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.target}}-lldap-bin
path: target/${{ matrix.target }}/release/lldap
- name: Upload ${{ matrix.target }} migration tool artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.target }}-lldap_migration_tool-bin
path: target/${{ matrix.target }}/release/lldap_migration_tool
- name: Upload ${{ matrix.target }} password tool artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.target }}-lldap_set_password-bin
path: target/${{ matrix.target }}/release/lldap_set_password
@@ -180,7 +186,7 @@ jobs:
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 1
options: >-
--name mariadb
--health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3
--health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
postgresql:
image: postgres:latest
@@ -199,7 +205,7 @@ jobs:
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: x86_64-unknown-linux-musl-lldap-bin
path: bin/
@@ -275,7 +281,7 @@ jobs:
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 1
options: >-
--name mariadb
--health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3
--health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
mysql:
@@ -293,19 +299,14 @@ jobs:
steps:
- name: Checkout scripts
uses: actions/checkout@v4.1.1
with:
sparse-checkout: 'scripts'
- name: Download LLDAP artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: x86_64-unknown-linux-musl-lldap-bin
path: bin/
- name: Download LLDAP set password
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: x86_64-unknown-linux-musl-lldap_set_password-bin
path: bin/
@@ -346,8 +347,10 @@ jobs:
- name: Export and Converting to Postgress
run: |
bash ./scripts/sqlite_dump_commands.sh | sqlite3 ./users.db > ./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
curl -L https://raw.githubusercontent.com/lldap/lldap/main/scripts/sqlite_dump_commands.sh -o helper.sh
chmod +x ./helper.sh
./helper.sh | sqlite3 ./users.db > ./dump.sql
sed -i -r -e "s/X'([[:xdigit:]]+'[^'])/'\\\x\\1/g" -e '1s/^/BEGIN;\n/' -e '$aCOMMIT;' ./dump.sql
- name: Create schema on postgres
run: |
@@ -355,14 +358,16 @@ jobs:
- name: Copy converted db to postgress and import
run: |
docker ps -a
docker cp ./dump.sql postgresql:/tmp/dump.sql
docker exec postgresql bash -c "psql -U lldapuser -d lldap < /tmp/dump.sql" | tee import.log
docker exec postgresql bash -c "psql -U lldapuser -d lldap < /tmp/dump.sql"
rm ./dump.sql
! grep ERROR import.log > /dev/null
- name: Export and Converting to mariadb
run: |
bash ./scripts/sqlite_dump_commands.sh | sqlite3 ./users.db > ./dump.sql
curl -L https://raw.githubusercontent.com/lldap/lldap/main/scripts/sqlite_dump_commands.sh -o helper.sh
chmod +x ./helper.sh
./helper.sh | sqlite3 ./users.db > ./dump.sql
cp ./dump.sql ./dump-no-sed.sql
sed -i -r -e "s/([^']'[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{9})\+00:00'([^'])/\1'\2/g" \-e 's/^INSERT INTO "?([a-zA-Z0-9_]+)"?/INSERT INTO `\1`/' -e '1s/^/START TRANSACTION;\n/' -e '$aCOMMIT;' ./dump.sql
sed -i '1 i\SET FOREIGN_KEY_CHECKS = 0;' ./dump.sql
@@ -372,14 +377,16 @@ jobs:
- name: Copy converted db to mariadb and import
run: |
docker ps -a
docker cp ./dump.sql mariadb:/tmp/dump.sql
docker exec mariadb bash -c "mariadb -ulldapuser -plldappass -f lldap < /tmp/dump.sql" | tee import.log
docker exec mariadb bash -c "mariadb -ulldapuser -plldappass -f lldap < /tmp/dump.sql"
rm ./dump.sql
! grep ERROR import.log > /dev/null
- name: Export and Converting to mysql
run: |
bash ./scripts/sqlite_dump_commands.sh | sqlite3 ./users.db > ./dump.sql
curl -L https://raw.githubusercontent.com/lldap/lldap/main/scripts/sqlite_dump_commands.sh -o helper.sh
chmod +x ./helper.sh
./helper.sh | sqlite3 ./users.db > ./dump.sql
sed -i -r -e 's/^INSERT INTO "?([a-zA-Z0-9_]+)"?/INSERT INTO `\1`/' -e '1s/^/START TRANSACTION;\n/' -e '$aCOMMIT;' ./dump.sql
sed -i '1 i\SET FOREIGN_KEY_CHECKS = 0;' ./dump.sql
@@ -388,10 +395,10 @@ jobs:
- name: Copy converted db to mysql and import
run: |
docker ps -a
docker cp ./dump.sql mysql:/tmp/dump.sql
docker exec mysql bash -c "mysql -ulldapuser -plldappass -f lldap < /tmp/dump.sql" | tee import.log
docker exec mysql bash -c "mysql -ulldapuser -plldappass -f lldap < /tmp/dump.sql"
rm ./dump.sql
! grep ERROR import.log > /dev/null
- name: Run lldap with postgres DB and healthcheck again
run: |
@@ -427,16 +434,12 @@ jobs:
LLDAP_http_port: 17173
LLDAP_JWT_SECRET: somejwtsecret
- name: Test Dummy User Postgres
run: ldapsearch -H ldap://localhost:3891 -LLL -D "uid=dummyuser,ou=people,dc=example,dc=com" -w 'dummypassword' -s "One" -b "ou=people,dc=example,dc=com"
- name: Test Dummy User MariaDB
run: ldapsearch -H ldap://localhost:3892 -LLL -D "uid=dummyuser,ou=people,dc=example,dc=com" -w 'dummypassword' -s "One" -b "ou=people,dc=example,dc=com"
- name: Test Dummy User MySQL
run: ldapsearch -H ldap://localhost:3893 -LLL -D "uid=dummyuser,ou=people,dc=example,dc=com" -w 'dummypassword' -s "One" -b "ou=people,dc=example,dc=com"
- name: Test Dummy User
run: |
ldapsearch -H ldap://localhost:3891 -LLL -D "uid=dummyuser,ou=people,dc=example,dc=com" -w 'dummypassword' -s "One" -b "ou=people,dc=example,dc=com"
ldapsearch -H ldap://localhost:3892 -LLL -D "uid=dummyuser,ou=people,dc=example,dc=com" -w 'dummypassword' -s "One" -b "ou=people,dc=example,dc=com"
ldapsearch -H ldap://localhost:3893 -LLL -D "uid=dummyuser,ou=people,dc=example,dc=com" -w 'dummypassword' -s "One" -b "ou=people,dc=example,dc=com"
########################################
#### BUILD BASE IMAGE ##################
########################################
build-docker-image:
needs: [build-ui, build-bin]
name: Build Docker image
@@ -446,7 +449,7 @@ jobs:
container: ["debian","alpine"]
include:
- container: alpine
platforms: linux/amd64,linux/arm64,linux/arm/v7
platforms: linux/amd64,linux/arm64
tags: |
type=ref,event=pr
type=semver,pattern=v{{version}}
@@ -459,8 +462,6 @@ jobs:
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }},suffix=
type=raw,value=latest,enable={{ is_default_branch }},suffix=
type=raw,value={{ date 'YYYY-MM-DD' }},enable={{ is_default_branch }}
type=raw,value={{ date 'YYYY-MM-DD' }},enable={{ is_default_branch }},suffix=
- container: debian
platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: |
@@ -470,102 +471,31 @@ jobs:
type=semver,pattern=v{{major}}.{{minor}}
type=raw,value=latest,enable={{ is_default_branch }}
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=raw,value={{ date 'YYYY-MM-DD' }},enable={{ is_default_branch }}
services:
registry:
image: registry:2
ports:
- 5000:5000
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.1
uses: actions/checkout@v3.5.2
- name: Download all artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
path: bin
- name: Download llap ui artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: ui
path: web
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
- name: Setup buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: network=host
- name: Docker ${{ matrix.container }} Base meta
id: meta-base
uses: docker/metadata-action@v5
with:
# list of Docker images to use as base name for tags
images: |
localhost:5000/lldap/lldap
tags: ${{ matrix.container }}-base
- name: Build ${{ matrix.container }} Base Docker Image
uses: docker/build-push-action@v5
with:
context: .
# On PR will fail, force fully uncomment push: true, or docker image will fail for next steps
#push: ${{ github.event_name != 'pull_request' }}
push: true
platforms: ${{ matrix.platforms }}
file: ./.github/workflows/Dockerfile.ci.${{ matrix.container }}-base
tags: |
${{ steps.meta-base.outputs.tags }}
labels: ${{ steps.meta-base.outputs.labels }}
cache-from: type=gha,mode=max
cache-to: type=gha,mode=max
#####################################
#### build variants docker image ####
#####################################
- name: Docker ${{ matrix.container }}-rootless meta
id: meta-rootless
uses: docker/metadata-action@v5
with:
# list of Docker images to use as base name for tags
images: |
nitnelave/lldap
lldap/lldap
ghcr.io/lldap/lldap
# Wanted Docker tags
# vX-alpine
# vX.Y-alpine
# vX.Y.Z-alpine
# latest
# latest-alpine
# stable
# stable-alpine
# YYYY-MM-DD
# YYYY-MM-DD-alpine
#################
# vX-debian
# vX.Y-debian
# vX.Y.Z-debian
# latest-debian
# stable-debian
# YYYY-MM-DD-debian
#################
# Check matrix for tag list definition
flavor: |
latest=false
suffix=-${{ matrix.container }}-rootless
tags: ${{ matrix.tags }}
uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
- name: Docker ${{ matrix.container }} meta
id: meta-standard
uses: docker/metadata-action@v5
id: meta
uses: docker/metadata-action@v4
with:
# list of Docker images to use as base name for tags
images: |
@@ -580,15 +510,12 @@ jobs:
# latest-alpine
# stable
# stable-alpine
# YYYY-MM-DD
# YYYY-MM-DD-alpine
#################
# vX-debian
# vX.Y-debian
# vX.Y.Z-debian
# latest-debian
# stable-debian
# YYYY-MM-DD-debian
#################
# Check matrix for tag list definition
flavor: |
@@ -599,49 +526,39 @@ jobs:
# Docker login to nitnelave/lldap and lldap/lldap
- name: Login to Nitnelave/LLDAP Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
uses: docker/login-action@v2
with:
registry: ghcr.io
username: nitnelave
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build ${{ matrix.container }}-rootless Docker Image
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: ${{ matrix.platforms }}
file: ./.github/workflows/Dockerfile.ci.${{ matrix.container }}-rootless
tags: |
${{ steps.meta-rootless.outputs.tags }}
labels: ${{ steps.meta-rootless.outputs.labels }}
cache-from: type=gha,mode=max
cache-to: type=gha,mode=max
### This docker build always the last, due :latest tag pushed multiple times, for whatever variants may added in future add docker build above this
########################################
#### docker image build ####
########################################
- name: Build ${{ matrix.container }} Docker Image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v4
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: ${{ matrix.platforms }}
file: ./.github/workflows/Dockerfile.ci.${{ matrix.container }}
tags: |
${{ steps.meta-standard.outputs.tags }}
labels: ${{ steps.meta-standard.outputs.labels }}
${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,mode=max
cache-to: type=gha,mode=max
- name: Update repo description
if: github.event_name != 'pull_request'
uses: peter-evans/dockerhub-description@v4
uses: peter-evans/dockerhub-description@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
@@ -649,7 +566,7 @@ jobs:
- name: Update lldap repo description
if: github.event_name != 'pull_request'
uses: peter-evans/dockerhub-description@v4
uses: peter-evans/dockerhub-description@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
@@ -667,7 +584,7 @@ jobs:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
path: bin/
- name: Check file
@@ -676,19 +593,19 @@ jobs:
run: |
mv bin/aarch64-unknown-linux-musl-lldap-bin/lldap bin/aarch64-lldap
mv bin/x86_64-unknown-linux-musl-lldap-bin/lldap bin/amd64-lldap
mv bin/armv7-unknown-linux-musleabihf-lldap-bin/lldap bin/armhf-lldap
mv bin/armv7-unknown-linux-gnueabihf-lldap-bin/lldap bin/armhf-lldap
mv bin/aarch64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool bin/aarch64-lldap_migration_tool
mv bin/x86_64-unknown-linux-musl-lldap_migration_tool-bin/lldap_migration_tool bin/amd64-lldap_migration_tool
mv bin/armv7-unknown-linux-musleabihf-lldap_migration_tool-bin/lldap_migration_tool bin/armhf-lldap_migration_tool
mv bin/armv7-unknown-linux-gnueabihf-lldap_migration_tool-bin/lldap_migration_tool bin/armhf-lldap_migration_tool
mv bin/aarch64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password bin/aarch64-lldap_set_password
mv bin/x86_64-unknown-linux-musl-lldap_set_password-bin/lldap_set_password bin/amd64-lldap_set_password
mv bin/armv7-unknown-linux-musleabihf-lldap_set_password-bin/lldap_set_password bin/armhf-lldap_set_password
mv bin/armv7-unknown-linux-gnueabihf-lldap_set_password-bin/lldap_set_password bin/armhf-lldap_set_password
chmod +x bin/*-lldap
chmod +x bin/*-lldap_migration_tool
chmod +x bin/*-lldap_set_password
- name: Download llap ui artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v3
with:
name: ui
path: web

View File

@@ -1,20 +0,0 @@
name: Release Bot
on:
release:
types: [published]
jobs:
comment:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: nflaig/release-comment-on-pr@master
with:
token: ${{ secrets.RELEASE_BOT_TOKEN }}
message: |
Thank you everyone for the contribution!
This feature is now available in the latest release, [${releaseTag}](${releaseUrl}).
You can support LLDAP by starring our repo, contributing some configuration examples and becoming a sponsor.

View File

@@ -33,7 +33,7 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@v4.1.1
uses: actions/checkout@v3.5.2
- uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --verbose --workspace
@@ -52,7 +52,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4.1.1
uses: actions/checkout@v3.5.2
- uses: Swatinem/rust-cache@v2
@@ -69,7 +69,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4.1.1
uses: actions/checkout@v3.5.2
- uses: Swatinem/rust-cache@v2
@@ -81,14 +81,12 @@ jobs:
coverage:
name: Code coverage
needs:
- pre_job
- test
needs: pre_job
if: ${{ needs.pre_job.outputs.should_skip != 'true' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4.1.1
uses: actions/checkout@v3.5.2
- 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
@@ -114,3 +112,28 @@ jobs:
files: lcov.info
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
publish-crates:
name: Publish on crates.io
if: ${{ needs.pre_job.outputs.should_skip != 'true' || (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'release' }}
needs: pre_job
strategy:
fail-fast: false
matrix:
target: [lldap_auth, lldap, lldap_app, lldap_set_password, lldap_migration_tool]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.5.2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Publish ${{ matrix.target }} crate
uses: katyo/publish-crates@v2
with:
args: -p ${{ matrix.target }}
dry-run: ${{ github.event_name != 'release' }}
check-repo: ${{ github.event_name != 'pull_request' }}
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
ignore-unpublished-changes: ${{ github.event_name != 'release' }}

View File

@@ -5,69 +5,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.5.0] 2023-09-14
### Breaking
- Emails and UUIDs are now enforced to be unique.
- If you have several users with the same email, you'll have to disambiguate
them. You can do that by either issuing SQL commands directly
(`UPDATE users SET email = 'x@x' WHERE user_id = 'bob';`), or by reverting
to a 0.4.x version of LLDAP and editing the user through the web UI.
An error will prevent LLDAP 0.5+ from starting otherwise.
- This was done to prevent account takeover for systems that allow to
login via email.
### Added
- The server private key can be set as a seed from an env variable (#504).
- This is especially useful when you have multiple containers, they don't
need to share a writeable folder.
- Added support for changing the password through a plain LDAP Modify
operation (as opposed to an extended operation), to allow Jellyfin
to change password (#620).
- Allow creating a user with multiple objectClass (#612).
- Emails now have a message ID (#608).
- Added a warning for browsers that have WASM/JS disabled (#639).
- Added support for querying OUs in LDAP (#669).
- Added a button to clear the avatar in the UI (#358).
### Changed
- Groups are now sorted by name in the web UI (#623).
- ARM build now uses musl (#584).
- Improved logging.
- Default admin user is only created if there are no admins (#563).
- That allows you to remove the default admin, making it harder to
bruteforce.
### Fixed
- Fixed URL parsing with a trailing slash in the password setting utility
(#597).
In addition to all that, there was significant progress towards #67,
user-defined attributes. That complex feature will unblock integration with many
systems, including PAM authentication.
### New services
- Ejabberd
- Ergo
- LibreNMS
- Mealie
- MinIO
- OpnSense
- PfSense
- PowerDnsAdmin
- Proxmox
- Squid
- Tandoor recipes
- TheLounge
- Zabbix-web
- Zulip
## [0.4.3] 2023-04-11
The repository has changed from `nitnelave/lldap` to `lldap/lldap`, both on GitHub

View File

@@ -1,97 +0,0 @@
# How to contribute to LLDAP
## Did you find a bug?
- Make sure there isn't already an [issue](https://github.com/lldap/lldap/issues?q=is%3Aissue+is%3Aopen) for it.
- Check if the bug still happens with the `latest` docker image, or the `main` branch if you compile it yourself.
- [Create an issue](https://github.com/lldap/lldap/issues/new) on GitHub. What makes a great issue:
- A quick summary of the bug.
- Steps to reproduce.
- LLDAP _verbose_ logs when reproducing the bug. Verbose mode can be set through environment variables (`LLDAP_VERBOSE=true`) or in the config (`verbose = true`).
- What you expected to happen.
- What actually happened.
- Other notes (what you tried, why you think it's happening, ...).
## Are you requesting integration with a new service?
- Check if there is already an [example config](https://github.com/lldap/lldap/tree/main/example_configs) for it.
- Try to figure out the configuration values for the new service yourself.
- You can use other example configs for inspiration.
- If you're having trouble, you can ask on [Discord](https://discord.gg/h5PEdRMNyP)
- If you succeed, make sure to contribute an example configuration, or a configuration guide.
- If you hit a block because of an unimplemented feature, go to the next section.
## Are you asking for a new feature?
- Make sure there isn't already an [issue](https://github.com/lldap/lldap/issues?q=is%3Aissue+is%3Aopen) for it.
- [Create an issue](https://github.com/lldap/lldap/issues/new) on GitHub. What makes a great feature request:
- A quick summary of the feature.
- Motivation: what problem does the feature solve?
- Workarounds: what are the currently possible solutions to the problem, however bad?
## Do you want to work on a PR?
That's great! There are 2 main ways to contribute to the project: documentation and code.
### Documentation
The simplest way to contribute is to submit a configuration guide for a new
service: it can be an example configuration file, or a markdown guide
explaining the steps necessary to configure the service.
We also have some
[documentation](https://github.com/lldap/lldap/tree/main/docs) with more
advanced guides (scripting, migrations, ...) you can contribute to.
### Code
If you don't know what to start with, check out the
[good first issues](https://github.com/lldap/lldap/labels/good%20first%20issue).
Otherwise, if you want to fix a specific bug or implement a feature, make sure
to start by creating an issue for it (if it doesn't already exist). There, we
can discuss whether it would be likely to be accepted and consider design
issues. That will save you from going down a wrong path, creating an entire PR
before getting told that it doesn't align with the project or the design is
flawed!
Once we agree on what to do in the issue, you can start working on the PR. A good quality PR has:
- A description of the change.
- The format we use for both commit titles and PRs is:
`tag: Do the thing`
The tag can be: server, app, docker, example_configs, ... It's a broad category.
The rest of the title should be an imperative sentence (see for instance [Commit Message
Guidelines](https://gist.github.com/robertpainsi/b632364184e70900af4ab688decf6f53)).
- The PR should refer to the issue it's addressing (e.g. "Fix #123").
- Explain the _why_ of the change.
- But also the _how_.
- Highlight any potential flaw or limitation.
- The code change should be as small as possible while solving the problem.
- Don't try to code-golf to change fewer characters, but keep logically separate changes in
different PRs.
- Add tests if possible.
- The tests should highlight the original issue in case of a bug.
- Ideally, we can apply the tests without the rest of the change and they would fail. With the
change, they pass.
- In some areas, there is no test infrastructure in place (e.g. for frontend changes). In that
case, do some manual testing and include the results (logs for backend changes, screenshot of a
successful service integration, screenshot of the frontend change).
- For backend changes, the tests should cover a significant portion of the new code paths, or
everything if possible. You can also add more tests to cover existing code.
- Of course, make sure all the existing tests pass. This will be checked anyway in the GitHub CI.
### Workflow
We use [GitHub Flow](https://docs.github.com/en/get-started/quickstart/github-flow):
- Fork the repository.
- (Optional) Create a new branch, or just use `main` in your fork.
- Make your change.
- Create a PR.
- Address the comments by adding more commits to your branch (or to `main`).
- The PR gets merged (the commits get squashed to a single one).
- (Optional) You can delete your branch/fork.
## Reminder
We're all volunteers, so be kind to each other! And since we're doing that in our free time, some
things can take a longer than expected.

966
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,6 @@ members = [
default-members = ["server"]
resolver = "2"
[profile.release]
lto = true

169
README.md
View File

@@ -5,9 +5,9 @@
</p>
<p align="center">
<a href="https://github.com/lldap/lldap/actions/workflows/rust.yml?query=branch%3Amain">
<a href="https://github.com/nitnelave/lldap/actions/workflows/rust.yml?query=branch%3Amain">
<img
src="https://github.com/lldap/lldap/actions/workflows/rust.yml/badge.svg"
src="https://github.com/nitnelave/lldap/actions/workflows/rust.yml/badge.svg"
alt="Build"/>
</a>
<a href="https://discord.gg/h5PEdRMNyP">
@@ -36,24 +36,15 @@
- [About](#about)
- [Installation](#installation)
- [With Docker](#with-docker)
- [With Kubernetes](#with-kubernetes)
- [From a package repository](#from-a-package-repository)
- [From source](#from-source)
- [Backend](#backend)
- [Frontend](#frontend)
- [Cross-compilation](#cross-compilation)
- [Usage](#usage)
- [Recommended architecture](#recommended-architecture)
- [Client configuration](#client-configuration)
- [Compatible services](#compatible-services)
- [General configuration guide](#general-configuration-guide)
- [Sample client configurations](#sample-client-configurations)
- [Incompatible services](#incompatible-services)
- [Migrating from SQLite](#migrating-from-sqlite)
- [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)
@@ -65,7 +56,7 @@ many backends, from KeyCloak to Authelia to Nextcloud and
[more](#compatible-services)!
<img
src="https://raw.githubusercontent.com/lldap/lldap/master/screenshot.png"
src="https://raw.githubusercontent.com/nitnelave/lldap/master/screenshot.png"
alt="Screenshot of the user list page"
width="50%"
align="right"
@@ -98,10 +89,9 @@ MySQL/MariaDB or PostgreSQL.
### With Docker
The image is available at `lldap/lldap`. You should persist the `/data`
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).
The image is available at `nitnelave/lldap`. You should persist the `/data`
folder, which contains your configuration, the database and the private key
file.
Configure the server by copying the `lldap_config.docker_template.toml` to
`/data/lldap_config.toml` and updating the configuration values (especially the
@@ -109,12 +99,10 @@ Configure the server by copying the `lldap_config.docker_template.toml` to
Environment variables should be prefixed with `LLDAP_` to override the
configuration.
If the `lldap_config.toml` doesn't exist when starting up, LLDAP will use
default one. The default admin password is `password`, you can change the
password later using the web interface.
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
variables `LLDAP_JWT_SECRET_FILE` or `LLDAP_LDAP_USER_PASS_FILE`, and the file
contents are loaded into the respective configuration parameters. Note that
`_FILE` variables take precedence.
@@ -124,7 +112,6 @@ Example for docker compose:
- `: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"
@@ -135,12 +122,10 @@ volumes:
services:
lldap:
image: lldap/lldap:stable
image: nitnelave/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 LDAP
- "3890:3890"
# For the web front-end
- "17170:17170"
volumes:
@@ -152,12 +137,8 @@ services:
- GID=####
- TZ=####/####
- LLDAP_JWT_SECRET=REPLACE_WITH_RANDOM
- LLDAP_KEY_SEED=REPLACE_WITH_RANDOM
- LLDAP_LDAP_USER_PASS=REPLACE_WITH_PASSWORD
- 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
@@ -170,44 +151,6 @@ front-end.
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.
#### Debian, CentOS Fedora, OpenSUSE, Ubuntu
The package for these distributions can be found at [LLDAP OBS](https://software.opensuse.org//download.html?project=home%3AMasgalor%3ALLDAP&package=lldap).
- When using the distributed package, the default login is `admin/password`. You can change that from the web UI after starting the service.
#### Arch Linux
Arch Linux offers unofficial support through the [Arch User Repository
(AUR)](https://wiki.archlinux.org/title/Arch_User_Repository).
Available package descriptions in AUR are:
- [lldap](https://aur.archlinux.org/packages/lldap) - Builds the latest stable version.
- [lldap-bin](https://aur.archlinux.org/packages/lldap-bin) - Uses the latest
pre-compiled binaries from the [releases in this repository](https://github.com/lldap/lldap/releases).
This package is recommended if you want to run lldap on a system with
limited resources.
- [lldap-git](https://aur.archlinux.org/packages/lldap-git) - Builds the
latest main branch code.
The package descriptions can be used
[to create and install packages](https://wiki.archlinux.org/title/Arch_User_Repository#Getting_started).
Each package places lldap's configuration file at `/etc/lldap.toml` and offers
[systemd service](https://wiki.archlinux.org/title/systemd#Using_units)
`lldap.service` to (auto-)start and stop lldap.
### From source
#### Backend
@@ -229,13 +172,15 @@ 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`.
`cargo`, you'll need:
- WASM-pack: `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).
@@ -270,47 +215,6 @@ You can then get the compiled server binary in
Raspberry Pi (or other target), with the folder structure maintained (`app`
files in an `app` folder next to the binary).
## Usage
The simplest way to use LLDAP is through the web front-end. There you can
create users, set passwords, add them to groups and so on. Users can also
connect to the web UI and change their information, or request a password reset
link (if you configured the SMTP client).
Creating and managing custom attributes is currently in Beta. It's not
supported in the Web UI. The recommended way is to use
[Zepmann/lldap-cli](https://github.com/Zepmann/lldap-cli), a
community-contributed CLI frontend.
LLDAP is also very scriptable, through its GraphQL API. See the
[Scripting](docs/scripting.md) docs for more info.
### Recommended architecture
If you are using containers, a sample architecture could look like this:
- A reverse proxy (e.g. nginx or Traefik)
- An authentication service (e.g. Authelia, Authentik or KeyCloak) connected to
LLDAP to provide authentication for non-authenticated services, or to provide
SSO with compatible ones.
- The LLDAP service, with the web port exposed to Traefik.
- The LDAP port doesn't need to be exposed, since only the other containers
will access it.
- You can also set up LDAPS if you want to expose the LDAP port to the
internet (not recommended) or for an extra layer of security in the
inter-container communication (though it's very much optional).
- The default LLDAP container starts up as root to fix up some files'
permissions before downgrading the privilege to the given user. However,
you can (should?) use the `*-rootless` version of the images to be able to
start directly as that user, once you got the permissions right. Just don't
forget to change from the `UID/GID` env vars to the `uid` docker-compose
field.
- Any other service that needs to connect to LLDAP for authentication (e.g.
NextCloud) can be added to a shared network with LLDAP. The finest
granularity is a network for each pair of LLDAP-service, but there are often
coarser granularities that make sense (e.g. a network for the \*arr stack and
LLDAP).
## Client configuration
### Compatible services
@@ -350,7 +254,6 @@ 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)
@@ -361,67 +264,27 @@ folder for help with:
- [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)
- [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)
- [MinIO](example_configs/minio.md)
- [Nextcloud](example_configs/nextcloud.md)
- [Nexus](example_configs/nexus.md)
- [Organizr](example_configs/Organizr.md)
- [Portainer](example_configs/portainer.md)
- [PowerDNS Admin](example_configs/powerdns_admin.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)
- [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
Though we try to be maximally compatible, not every feature is supported; LLDAP
is not a fully-featured LDAP server, intentionally so.
LDAP browsing tools are generally not supported, though they could be. If you
need to use one but it behaves weirdly, please file a bug.
Some services use features that are not implemented, or require specific
attributes. You can try to create those attributes (see custom attributes in
the [Usage](#usage) section).
Finally, some services require password hashes so they can validate themselves
the user's password without contacting LLDAP. This is not and will not be
supported, it's incompatible with our password hashing scheme (a zero-knowledge
proof). Furthermore, it's generally not recommended in terms of security, since
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
service that seems definitely incompatible with LLDAP.
## Migrating from SQLite

View File

@@ -6,7 +6,7 @@ homepage = "https://github.com/lldap/lldap"
license = "GPL-3.0-only"
name = "lldap_app"
repository = "https://github.com/lldap/lldap"
version = "0.5.1-alpha"
version = "0.5.0-alpha"
include = ["src/**/*", "queries/**/*", "Cargo.toml", "../schema.graphql"]
[dependencies]
@@ -14,7 +14,7 @@ anyhow = "1"
base64 = "0.13"
gloo-console = "0.2.3"
gloo-file = "0.2.3"
gloo-net = "*"
gloo-net = "0.2"
graphql_client = "0.10"
http = "0.2"
jwt = "0.13"
@@ -23,9 +23,9 @@ serde = "1"
serde_json = "1"
url-escape = "0.1.1"
validator = "=0.14"
validator_derive = "*"
validator_derive = "0.16"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "*"
wasm-bindgen-futures = "0.4"
yew = "0.19.3"
yew-router = "0.16"
@@ -38,24 +38,22 @@ features = [
"Document",
"Element",
"FileReader",
"FormData",
"HtmlDocument",
"HtmlInputElement",
"HtmlOptionElement",
"HtmlOptionsCollection",
"HtmlSelectElement",
"SubmitEvent",
"console",
]
[dependencies.chrono]
version = "*"
version = "0.4"
features = [
"wasmbind"
]
[dependencies.lldap_auth]
path = "../auth"
version = "0.3"
features = [ "opaque_client" ]
[dependencies.image]

View File

@@ -4,8 +4,7 @@
<head>
<meta charset="utf-8" />
<title>LLDAP Administration</title>
<base href="/">
<script src="static/main.js" type="module" defer></script>
<script src="/static/main.js" type="module" defer></script>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-nightshade.min.css"
rel="preload stylesheet"
@@ -16,8 +15,8 @@
src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-/bQdsTh/da6pkI1MST/rWKFNjaCP5gBSY4sEBT38Q/9RBh9AH40zEOg7Hlq2THRZ"
crossorigin="anonymous"></script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/js/darkmode.min.js"
<script
src="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/js/darkmode.min.js"
integrity="sha384-A4SLs39X/aUfwRclRaXvNeXNBTLZdnZdHhhteqbYFS2jZTRD79tKeFeBn7SGXNpi"
crossorigin="anonymous"></script>
<link
@@ -34,7 +33,7 @@
href="https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap" />
<link
rel="stylesheet"
href="static/style.css" />
href="/static/style.css" />
<script>
function inDarkMode(){
return darkmode.inDarkMode;
@@ -44,23 +43,6 @@
</head>
<body>
<noscript>
<!-- This will be displayed if the user doesn't have JavaScript enabled. -->
LLDAP requires JavaScript, please switch to a compatible browser or
enable it.
</noscript>
<script>
/* Detect if the user has WASM support. */
if (typeof WebAssembly === 'undefined') {
const pWASMMsg = document.createElement("p")
pWASMMsg.innerHTML = `
LLDAP requires WASM and JIT for JavaScript, please switch to a
compatible browser or enable it.
`
document.body.appendChild(pWASMMsg)
}
</script>
</body>
</html>

View File

@@ -13,8 +13,8 @@
<script
src="/static/bootstrap.bundle.min.js"
integrity="sha384-/bQdsTh/da6pkI1MST/rWKFNjaCP5gBSY4sEBT38Q/9RBh9AH40zEOg7Hlq2THRZ"></script>
<script
src="/static/darkmode.min.js"
<script
src="/static/darkmode.min.js"
integrity="sha384-A4SLs39X/aUfwRclRaXvNeXNBTLZdnZdHhhteqbYFS2jZTRD79tKeFeBn7SGXNpi"></script>
<link
rel="stylesheet"
@@ -40,23 +40,6 @@
</head>
<body>
<noscript>
<!-- This will be displayed if the user doesn't have JavaScript enabled. -->
LLDAP requires JavaScript, please switch to a compatible browser or
enable it.
</noscript>
<script>
/* Detect if the user has WASM support. */
if (typeof WebAssembly === 'undefined') {
const pWASMMsg = document.createElement("p")
pWASMMsg.innerHTML = `
LLDAP requires WASM and JIT for JavaScript, please switch to a
compatible browser or enable it.
`
document.body.appendChild(pWASMMsg)
}
</script>
</body>
</html>

View File

@@ -1,5 +0,0 @@
mutation CreateGroupAttribute($name: String!, $attributeType: AttributeType!, $isList: Boolean!, $isVisible: Boolean!) {
addGroupAttribute(name: $name, attributeType: $attributeType, isList: $isList, isVisible: $isVisible, isEditable: false) {
ok
}
}

View File

@@ -1,5 +0,0 @@
mutation CreateUserAttribute($name: String!, $attributeType: AttributeType!, $isList: Boolean!, $isVisible: Boolean!, $isEditable: Boolean!) {
addUserAttribute(name: $name, attributeType: $attributeType, isList: $isList, isVisible: $isVisible, isEditable: $isEditable) {
ok
}
}

View File

@@ -1,5 +0,0 @@
mutation DeleteGroupAttributeQuery($name: String!) {
deleteGroupAttribute(name: $name) {
ok
}
}

View File

@@ -1,5 +0,0 @@
mutation DeleteUserAttributeQuery($name: String!) {
deleteUserAttribute(name: $name) {
ok
}
}

View File

@@ -1,13 +0,0 @@
query GetGroupAttributesSchema {
schema {
groupSchema {
attributes {
name
attributeType
isList
isVisible
isHardcoded
}
}
}
}

View File

@@ -1,14 +0,0 @@
query GetUserAttributesSchema {
schema {
userSchema {
attributes {
name
attributeType
isList
isVisible
isEditable
isHardcoded
}
}
}
}

View File

@@ -11,20 +11,6 @@ query GetUserDetails($id: String!) {
groups {
id
displayName
}
attributes {
name
value
}
}
schema {
user_attrubutes {
attributes {
name
attributeType
isEditable
isHardcoded
}
}
}
}

View File

@@ -1,26 +1,23 @@
use crate::{
components::{
banner::Banner,
change_password::ChangePasswordForm,
create_group::CreateGroupForm,
create_group_attribute::CreateGroupAttributeForm,
create_user::CreateUserForm,
create_user_attribute::CreateUserAttributeForm,
group_details::GroupDetails,
group_schema_table::ListGroupSchema,
group_table::GroupTable,
login::LoginForm,
logout::LogoutButton,
reset_password_step1::ResetPasswordStep1Form,
reset_password_step2::ResetPasswordStep2Form,
router::{AppRoute, Link, Redirect},
user_details::UserDetails,
user_schema_table::ListUserSchema,
user_table::UserTable,
},
infra::{api::HostService, cookies::get_cookie},
};
use gloo_console::error;
use wasm_bindgen::prelude::*;
use yew::{
function_component,
html::Scope,
@@ -33,6 +30,25 @@ use yew_router::{
BrowserRouter, Switch,
};
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = darkmode)]
fn toggleDarkMode(doSave: bool);
#[wasm_bindgen]
fn inDarkMode() -> bool;
}
#[function_component(DarkModeToggle)]
pub fn dark_mode_toggle() -> Html {
html! {
<div class="form-check form-switch">
<input class="form-check-input" onclick={|_| toggleDarkMode(true)} type="checkbox" id="darkModeToggle" checked={inDarkMode()}/>
<label class="form-check-label" for="darkModeToggle">{"Dark mode"}</label>
</div>
}
}
#[function_component(AppContainer)]
pub fn app_container() -> Html {
html! {
@@ -119,11 +135,10 @@ impl Component for App {
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link().clone();
let is_admin = self.is_admin();
let username = self.user_info.clone().map(|(username, _)| username);
let password_reset_enabled = self.password_reset_enabled;
html! {
<div>
<Banner is_admin={is_admin} username={username} on_logged_out={link.callback(|_| Msg::Logout)} />
{self.view_banner(ctx)}
<div class="container py-3 bg-kug">
<div class="row justify-content-center" style="padding-bottom: 80px;">
<main class="py-3" style="max-width: 1000px">
@@ -212,12 +227,6 @@ impl App {
AppRoute::CreateGroup => html! {
<CreateGroupForm/>
},
AppRoute::CreateUserAttribute => html! {
<CreateUserAttributeForm/>
},
AppRoute::CreateGroupAttribute => html! {
<CreateGroupAttributeForm/>
},
AppRoute::ListGroups => html! {
<div>
<GroupTable />
@@ -227,12 +236,6 @@ impl App {
</Link>
</div>
},
AppRoute::ListUserSchema => html! {
<ListUserSchema />
},
AppRoute::ListGroupSchema => html! {
<ListGroupSchema />
},
AppRoute::GroupDetails { group_id } => html! {
<GroupDetails group_id={*group_id} />
},
@@ -260,6 +263,91 @@ impl App {
}
}
fn view_banner(&self, ctx: &Context<Self>) -> Html {
html! {
<header class="p-2 mb-3 border-bottom">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<a href="/" class="d-flex align-items-center mt-2 mb-lg-0 me-md-5 text-decoration-none">
<h2>{"LLDAP"}</h2>
</a>
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
{if self.is_admin() { html! {
<>
<li>
<Link
classes="nav-link px-2 h6"
to={AppRoute::ListUsers}>
<i class="bi-people me-2"></i>
{"Users"}
</Link>
</li>
<li>
<Link
classes="nav-link px-2 h6"
to={AppRoute::ListGroups}>
<i class="bi-collection me-2"></i>
{"Groups"}
</Link>
</li>
</>
} } else { html!{} } }
</ul>
{ self.view_user_menu(ctx) }
<DarkModeToggle />
</div>
</div>
</header>
}
}
fn view_user_menu(&self, ctx: &Context<Self>) -> Html {
if let Some((user_id, _)) = &self.user_info {
let link = ctx.link();
html! {
<div class="dropdown text-end">
<a href="#"
class="d-block nav-link text-decoration-none dropdown-toggle"
id="dropdownUser"
data-bs-toggle="dropdown"
aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
fill="currentColor"
class="bi bi-person-circle"
viewBox="0 0 16 16">
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
</svg>
<span class="ms-2">
{user_id}
</span>
</a>
<ul
class="dropdown-menu text-small dropdown-menu-lg-end"
aria-labelledby="dropdownUser1"
style="">
<li>
<Link
classes="dropdown-item"
to={AppRoute::UserDetails{ user_id: user_id.clone() }}>
{"View details"}
</Link>
</li>
<li><hr class="dropdown-divider" /></li>
<li>
<LogoutButton on_logged_out={link.callback(|_| Msg::Logout)} />
</li>
</ul>
</div>
}
} else {
html! {}
}
}
fn view_footer(&self) -> Html {
html! {
<footer class="text-center fixed-bottom text-muted bg-light py-2">
@@ -267,7 +355,7 @@ impl App {
<span>{format!("LLDAP version {}", env!("CARGO_PKG_VERSION"))}</span>
</div>
<div>
<a href="https://github.com/lldap/lldap" class="me-4 text-reset">
<a href="https://github.com/nitnelave/lldap" class="me-4 text-reset">
<i class="bi-github"></i>
</a>
<a href="https://discord.gg/h5PEdRMNyP" class="me-4 text-reset">
@@ -278,7 +366,7 @@ impl App {
</a>
</div>
<div>
<span>{"License "}<a href="https://github.com/lldap/lldap/blob/main/LICENSE" class="link-secondary">{"GNU GPL"}</a></span>
<span>{"License "}<a href="https://github.com/nitnelave/lldap/blob/main/LICENSE" class="link-secondary">{"GNU GPL"}</a></span>
</div>
</footer>
}

View File

@@ -1,87 +0,0 @@
use crate::infra::functional::{use_graphql_call, LoadableResult};
use graphql_client::GraphQLQuery;
use yew::{function_component, html, virtual_dom::AttrValue, Properties};
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "queries/get_user_details.graphql",
response_derives = "Debug, Hash, PartialEq, Eq, Clone",
custom_scalars_module = "crate::infra::graphql"
)]
pub struct GetUserDetails;
#[derive(Properties, PartialEq)]
pub struct Props {
pub user: AttrValue,
#[prop_or(32)]
pub width: i32,
#[prop_or(32)]
pub height: i32,
}
#[function_component(Avatar)]
pub fn avatar(props: &Props) -> Html {
let user_details = use_graphql_call::<GetUserDetails>(get_user_details::Variables {
id: props.user.to_string(),
});
match &(*user_details) {
LoadableResult::Loaded(Ok(response)) => {
let avatar = response.user.avatar.clone();
match &avatar {
Some(data) => html! {
<img
id="avatarDisplay"
src={format!("data:image/jpeg;base64, {}", data)}
style={format!("max-height:{}px;max-width:{}px;height:auto;width:auto;", props.height, props.width)}
alt="Avatar" />
},
None => html! {
<BlankAvatarDisplay
width={props.width}
height={props.height} />
},
}
}
LoadableResult::Loaded(Err(error)) => html! {
<BlankAvatarDisplay
error={error.to_string()}
width={props.width}
height={props.height} />
},
LoadableResult::Loading => html! {
<BlankAvatarDisplay
width={props.width}
height={props.height} />
},
}
}
#[derive(Properties, PartialEq)]
struct BlankAvatarDisplayProps {
#[prop_or(None)]
pub error: Option<AttrValue>,
pub width: i32,
pub height: i32,
}
#[function_component(BlankAvatarDisplay)]
fn blank_avatar_display(props: &BlankAvatarDisplayProps) -> Html {
let fill = match &props.error {
Some(_) => "red",
None => "currentColor",
};
html! {
<svg xmlns="http://www.w3.org/2000/svg"
width={props.width.to_string()}
height={props.height.to_string()}
fill={fill}
class="bi bi-person-circle"
viewBox="0 0 16 16">
<title>{props.error.clone().unwrap_or(AttrValue::Static("Avatar"))}</title>
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
</svg>
}
}

View File

@@ -1,132 +0,0 @@
use crate::components::{
avatar::Avatar,
logout::LogoutButton,
router::{AppRoute, Link},
};
use wasm_bindgen::prelude::wasm_bindgen;
use yew::{function_component, html, Callback, Properties};
#[derive(Properties, PartialEq)]
pub struct Props {
pub is_admin: bool,
pub username: Option<String>,
pub on_logged_out: Callback<()>,
}
#[function_component(Banner)]
pub fn banner(props: &Props) -> Html {
html! {
<header class="p-2 mb-3 border-bottom">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<a href={yew_router::utils::base_url().unwrap_or("/".to_string())} class="d-flex align-items-center mt-2 mb-lg-0 me-md-5 text-decoration-none">
<h2>{"LLDAP"}</h2>
</a>
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
{if props.is_admin { html! {
<>
<li>
<Link
classes="nav-link px-2 h6"
to={AppRoute::ListUsers}>
<i class="bi-people me-2"></i>
{"Users"}
</Link>
</li>
<li>
<Link
classes="nav-link px-2 h6"
to={AppRoute::ListGroups}>
<i class="bi-collection me-2"></i>
{"Groups"}
</Link>
</li>
<li>
<Link
classes="nav-link px-2 h6"
to={AppRoute::ListUserSchema}>
<i class="bi-list-ul me-2"></i>
{"User schema"}
</Link>
</li>
<li>
<Link
classes="nav-link px-2 h6"
to={AppRoute::ListGroupSchema}>
<i class="bi-list-ul me-2"></i>
{"Group schema"}
</Link>
</li>
</>
} } else { html!{} } }
</ul>
<UserMenu username={props.username.clone()} on_logged_out={props.on_logged_out.clone()}/>
<DarkModeToggle />
</div>
</div>
</header>
}
}
#[derive(Properties, PartialEq)]
struct UserMenuProps {
pub username: Option<String>,
pub on_logged_out: Callback<()>,
}
#[function_component(UserMenu)]
fn user_menu(props: &UserMenuProps) -> Html {
match &props.username {
Some(username) => html! {
<div class="dropdown text-end">
<a href="#"
class="d-block nav-link text-decoration-none dropdown-toggle"
id="dropdownUser"
data-bs-toggle="dropdown"
aria-expanded="false">
<Avatar user={username.clone()} />
<span class="ms-2">
{username}
</span>
</a>
<ul
class="dropdown-menu text-small dropdown-menu-lg-end"
aria-labelledby="dropdownUser1"
style="">
<li>
<Link
classes="dropdown-item"
to={AppRoute::UserDetails{ user_id: username.to_string() }}>
{"View details"}
</Link>
</li>
<li><hr class="dropdown-divider" /></li>
<li>
<LogoutButton on_logged_out={props.on_logged_out.clone()} />
</li>
</ul>
</div>
},
_ => html! {},
}
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = darkmode)]
fn toggleDarkMode(doSave: bool);
#[wasm_bindgen]
fn inDarkMode() -> bool;
}
#[function_component(DarkModeToggle)]
fn dark_mode_toggle() -> Html {
html! {
<div class="form-check form-switch">
<input class="form-check-input" onclick={|_| toggleDarkMode(true)} type="checkbox" id="darkModeToggle" checked={inDarkMode()}/>
<label class="form-check-label" for="darkModeToggle">{"Dark mode"}</label>
</div>
}
}

View File

@@ -1,8 +1,5 @@
use crate::{
components::{
form::{field::Field, submit::Submit},
router::{AppRoute, Link},
},
components::router::{AppRoute, Link},
infra::{
api::HostService,
common_component::{CommonComponent, CommonComponentParts},
@@ -100,7 +97,7 @@ impl CommonComponent<ChangePasswordForm> for ChangePasswordForm {
.context("Could not initialize login")?;
self.opaque_data = OpaqueData::Login(login_start_request.state);
let req = login::ClientLoginStartRequest {
username: ctx.props().username.clone().into(),
username: ctx.props().username.clone(),
login_start_request: login_start_request.message,
};
self.common.call_backend(
@@ -131,13 +128,11 @@ impl CommonComponent<ChangePasswordForm> for ChangePasswordForm {
Msg::SubmitNewPassword => {
let mut rng = rand::rngs::OsRng;
let new_password = self.form.model().password;
let registration_start_request = opaque::client::registration::start_registration(
new_password.as_bytes(),
&mut rng,
)
.context("Could not initiate password change")?;
let registration_start_request =
opaque::client::registration::start_registration(&new_password, &mut rng)
.context("Could not initiate password change")?;
let req = registration::ClientRegistrationStartRequest {
username: ctx.props().username.clone().into(),
username: ctx.props().username.clone(),
registration_start_request: registration_start_request.message,
};
self.opaque_data = OpaqueData::Registration(registration_start_request.state);
@@ -210,6 +205,7 @@ impl Component for ChangePasswordForm {
fn view(&self, ctx: &Context<Self>) -> Html {
let is_admin = ctx.props().is_admin;
let link = ctx.link();
type Field = yew_form::Field<FormModel>;
html! {
<>
<div class="mb-2 mt-2">
@@ -226,44 +222,90 @@ impl Component for ChangePasswordForm {
}
} else { html! {} }
}
<form class="form">
<form
class="form">
{if !is_admin { html! {
<Field<FormModel>
form={&self.form}
required=true
label="Current password"
field_name="old_password"
input_type="password"
autocomplete="current-password"
oninput={link.callback(|_| Msg::FormUpdate)} />
<div class="form-group row">
<label for="old_password"
class="form-label col-sm-2 col-form-label">
{"Current password*:"}
</label>
<div class="col-sm-10">
<Field
form={&self.form}
field_name="old_password"
input_type="password"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="current-password"
oninput={link.callback(|_| Msg::FormUpdate)} />
<div class="invalid-feedback">
{&self.form.field_message("old_password")}
</div>
</div>
</div>
}} else { html! {} }}
<Field<FormModel>
form={&self.form}
required=true
label="New password"
field_name="password"
input_type="password"
autocomplete="new-password"
oninput={link.callback(|_| Msg::FormUpdate)} />
<Field<FormModel>
form={&self.form}
required=true
label="Confirm password"
field_name="confirm_password"
input_type="password"
autocomplete="new-password"
oninput={link.callback(|_| Msg::FormUpdate)} />
<Submit
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})}
text="Save changes" >
<div class="form-group row mb-3">
<label for="new_password"
class="form-label col-sm-2 col-form-label">
{"New Password"}
<span class="text-danger">{"*"}</span>
{":"}
</label>
<div class="col-sm-10">
<Field
form={&self.form}
field_name="password"
input_type="password"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="new-password"
oninput={link.callback(|_| Msg::FormUpdate)} />
<div class="invalid-feedback">
{&self.form.field_message("password")}
</div>
</div>
</div>
<div class="form-group row mb-3">
<label for="confirm_password"
class="form-label col-sm-2 col-form-label">
{"Confirm Password"}
<span class="text-danger">{"*"}</span>
{":"}
</label>
<div class="col-sm-10">
<Field
form={&self.form}
field_name="confirm_password"
input_type="password"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="new-password"
oninput={link.callback(|_| Msg::FormUpdate)} />
<div class="invalid-feedback">
{&self.form.field_message("confirm_password")}
</div>
</div>
</div>
<div class="form-group row justify-content-center">
<button
class="btn btn-primary col-auto col-form-label"
type="submit"
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})}>
<i class="bi-save me-2"></i>
{"Save changes"}
</button>
<Link
classes="btn btn-secondary ms-2 col-auto col-form-label"
to={AppRoute::UserDetails{user_id: ctx.props().username.clone()}}>
<i class="bi-arrow-return-left me-2"></i>
{"Back"}
</Link>
</Submit>
</div>
</form>
</>
}

View File

@@ -1,8 +1,5 @@
use crate::{
components::{
form::{field::Field, submit::Submit},
router::AppRoute,
},
components::router::AppRoute,
infra::common_component::{CommonComponent, CommonComponentParts},
};
use anyhow::{bail, Result};
@@ -96,21 +93,44 @@ impl Component for CreateGroupForm {
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
type Field = yew_form::Field<CreateGroupModel>;
html! {
<div class="row justify-content-center">
<form class="form py-3" style="max-width: 636px">
<div class="row mb-3">
<h5 class="fw-bold">{"Create a group"}</h5>
</div>
<Field<CreateGroupModel>
form={&self.form}
required=true
label="Group name"
field_name="groupname"
oninput={link.callback(|_| Msg::Update)} />
<Submit
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})} />
<div class="form-group row mb-3">
<label for="groupname"
class="form-label col-4 col-form-label">
{"Group name"}
<span class="text-danger">{"*"}</span>
{":"}
</label>
<div class="col-8">
<Field
form={&self.form}
field_name="groupname"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="groupname"
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("groupname")}
</div>
</div>
</div>
<div class="form-group row justify-content-center">
<button
class="btn btn-primary col-auto col-form-label"
type="submit"
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})}>
<i class="bi-save me-2"></i>
{"Submit"}
</button>
</div>
</form>
{ if let Some(e) = &self.common.error {
html! {

View File

@@ -1,168 +0,0 @@
use crate::{
components::{
form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit},
router::AppRoute,
},
convert_attribute_type,
infra::{
common_component::{CommonComponent, CommonComponentParts},
schema::{validate_attribute_type, AttributeType},
},
};
use anyhow::{bail, Result};
use gloo_console::log;
use graphql_client::GraphQLQuery;
use validator_derive::Validate;
use yew::prelude::*;
use yew_form_derive::Model;
use yew_router::{prelude::History, scope_ext::RouterScopeExt};
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "queries/create_group_attribute.graphql",
response_derives = "Debug",
custom_scalars_module = "crate::infra::graphql"
)]
pub struct CreateGroupAttribute;
convert_attribute_type!(create_group_attribute::AttributeType);
pub struct CreateGroupAttributeForm {
common: CommonComponentParts<Self>,
form: yew_form::Form<CreateGroupAttributeModel>,
}
#[derive(Model, Validate, PartialEq, Eq, Clone, Default, Debug)]
pub struct CreateGroupAttributeModel {
#[validate(length(min = 1, message = "attribute_name is required"))]
attribute_name: String,
#[validate(custom = "validate_attribute_type")]
attribute_type: String,
is_list: bool,
is_visible: bool, // remove when backend doesn't return group attributes for normal users
}
pub enum Msg {
Update,
SubmitForm,
CreateGroupAttributeResponse(Result<create_group_attribute::ResponseData>),
}
impl CommonComponent<CreateGroupAttributeForm> for CreateGroupAttributeForm {
fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg {
Msg::Update => Ok(true),
Msg::SubmitForm => {
if !self.form.validate() {
bail!("Check the form for errors");
}
let model = self.form.model();
let attribute_type = model.attribute_type.parse::<AttributeType>().unwrap();
let req = create_group_attribute::Variables {
name: model.attribute_name,
attribute_type: create_group_attribute::AttributeType::from(attribute_type),
is_list: model.is_list,
is_visible: model.is_visible,
};
self.common.call_graphql::<CreateGroupAttribute, _>(
ctx,
req,
Msg::CreateGroupAttributeResponse,
"Error trying to create group attribute",
);
Ok(true)
}
Msg::CreateGroupAttributeResponse(response) => {
response?;
let model = self.form.model();
log!(&format!(
"Created group attribute '{}'",
model.attribute_name
));
ctx.link()
.history()
.unwrap()
.push(AppRoute::ListGroupSchema);
Ok(true)
}
}
}
fn mut_common(&mut self) -> &mut CommonComponentParts<Self> {
&mut self.common
}
}
impl Component for CreateGroupAttributeForm {
type Message = Msg;
type Properties = ();
fn create(_: &Context<Self>) -> Self {
let model = CreateGroupAttributeModel {
attribute_type: AttributeType::String.to_string(),
..Default::default()
};
Self {
common: CommonComponentParts::<Self>::create(),
form: yew_form::Form::<CreateGroupAttributeModel>::new(model),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, ctx, msg)
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="row justify-content-center">
<form class="form py-3" style="max-width: 636px">
<h5 class="fw-bold">{"Create a group attribute"}</h5>
<Field<CreateGroupAttributeModel>
label="Name"
required={true}
form={&self.form}
field_name="attribute_name"
oninput={link.callback(|_| Msg::Update)} />
<Select<CreateGroupAttributeModel>
label="Type"
required={true}
form={&self.form}
field_name="attribute_type"
oninput={link.callback(|_| Msg::Update)}>
<option selected=true value="String">{"String"}</option>
<option value="Integer">{"Integer"}</option>
<option value="Jpeg">{"Jpeg"}</option>
<option value="DateTime">{"DateTime"}</option>
</Select<CreateGroupAttributeModel>>
<CheckBox<CreateGroupAttributeModel>
label="Multiple values"
form={&self.form}
field_name="is_list"
ontoggle={link.callback(|_| Msg::Update)} />
<CheckBox<CreateGroupAttributeModel>
label="Visible to users"
form={&self.form}
field_name="is_visible"
ontoggle={link.callback(|_| Msg::Update)} />
<Submit
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})}/>
</form>
{ if let Some(e) = &self.common.error {
html! {
<div class="alert alert-danger">
{e.to_string() }
</div>
}
} else { html! {} }
}
</div>
}
}
}

View File

@@ -1,8 +1,5 @@
use crate::{
components::{
form::{field::Field, submit::Submit},
router::AppRoute,
},
components::router::AppRoute,
infra::{
api::HostService,
common_component::{CommonComponent, CommonComponentParts},
@@ -93,7 +90,6 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
firstName: to_option(model.first_name),
lastName: to_option(model.last_name),
avatar: None,
attributes: None,
},
};
self.common.call_graphql::<CreateUser, _>(
@@ -121,12 +117,9 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
let opaque::client::registration::ClientRegistrationStartResult {
state,
message,
} = opaque::client::registration::start_registration(
password.as_bytes(),
&mut rng,
)?;
} = opaque::client::registration::start_registration(&password, &mut rng)?;
let req = registration::ClientRegistrationStartRequest {
username: user_id.into(),
username: user_id,
registration_start_request: message,
};
self.common
@@ -190,57 +183,163 @@ impl Component for CreateUserForm {
fn view(&self, ctx: &Context<Self>) -> Html {
let link = &ctx.link();
type Field = yew_form::Field<CreateUserModel>;
html! {
<div class="row justify-content-center">
<form class="form py-3" style="max-width: 636px">
<Field<CreateUserModel>
form={&self.form}
required=true
label="User name"
field_name="username"
oninput={link.callback(|_| Msg::Update)} />
<Field<CreateUserModel>
form={&self.form}
required=true
label="Email"
field_name="email"
input_type="email"
oninput={link.callback(|_| Msg::Update)} />
<Field<CreateUserModel>
form={&self.form}
label="Display name"
field_name="display_name"
autocomplete="name"
oninput={link.callback(|_| Msg::Update)} />
<Field<CreateUserModel>
form={&self.form}
label="First name"
field_name="first_name"
autocomplete="given-name"
oninput={link.callback(|_| Msg::Update)} />
<Field<CreateUserModel>
form={&self.form}
label="Last name"
field_name="last_name"
autocomplete="family-name"
oninput={link.callback(|_| Msg::Update)} />
<Field<CreateUserModel>
form={&self.form}
label="Password"
field_name="password"
input_type="password"
autocomplete="new-password"
oninput={link.callback(|_| Msg::Update)} />
<Field<CreateUserModel>
form={&self.form}
label="Confirm password"
field_name="confirm_password"
input_type="password"
autocomplete="new-password"
oninput={link.callback(|_| Msg::Update)} />
<Submit
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})} />
<div class="row mb-3">
<h5 class="fw-bold">{"Create a user"}</h5>
</div>
<div class="form-group row mb-3">
<label for="username"
class="form-label col-4 col-form-label">
{"User name"}
<span class="text-danger">{"*"}</span>
{":"}
</label>
<div class="col-8">
<Field
form={&self.form}
field_name="username"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="username"
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("username")}
</div>
</div>
</div>
<div class="form-group row mb-3">
<label for="email"
class="form-label col-4 col-form-label">
{"Email"}
<span class="text-danger">{"*"}</span>
{":"}
</label>
<div class="col-8">
<Field
form={&self.form}
input_type="email"
field_name="email"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="email"
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("email")}
</div>
</div>
</div>
<div class="form-group row mb-3">
<label for="display-name"
class="form-label col-4 col-form-label">
{"Display name:"}
</label>
<div class="col-8">
<Field
form={&self.form}
autocomplete="name"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
field_name="display_name"
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("display_name")}
</div>
</div>
</div>
<div class="form-group row mb-3">
<label for="first-name"
class="form-label col-4 col-form-label">
{"First name:"}
</label>
<div class="col-8">
<Field
form={&self.form}
autocomplete="given-name"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
field_name="first_name"
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("first_name")}
</div>
</div>
</div>
<div class="form-group row mb-3">
<label for="last-name"
class="form-label col-4 col-form-label">
{"Last name:"}
</label>
<div class="col-8">
<Field
form={&self.form}
autocomplete="family-name"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
field_name="last_name"
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("last_name")}
</div>
</div>
</div>
<div class="form-group row mb-3">
<label for="password"
class="form-label col-4 col-form-label">
{"Password:"}
</label>
<div class="col-8">
<Field
form={&self.form}
input_type="password"
field_name="password"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="new-password"
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("password")}
</div>
</div>
</div>
<div class="form-group row mb-3">
<label for="confirm_password"
class="form-label col-4 col-form-label">
{"Confirm password:"}
</label>
<div class="col-8">
<Field
form={&self.form}
input_type="password"
field_name="confirm_password"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="new-password"
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("confirm_password")}
</div>
</div>
</div>
<div class="form-group row justify-content-center">
<button
class="btn btn-primary col-auto col-form-label mt-4"
disabled={self.common.is_task_running()}
type="submit"
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})}>
<i class="bi-save me-2"></i>
{"Submit"}
</button>
</div>
</form>
{
if let Some(e) = &self.common.error {

View File

@@ -1,175 +0,0 @@
use crate::{
components::{
form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit},
router::AppRoute,
},
convert_attribute_type,
infra::{
common_component::{CommonComponent, CommonComponentParts},
schema::{validate_attribute_type, AttributeType},
},
};
use anyhow::{bail, Result};
use gloo_console::log;
use graphql_client::GraphQLQuery;
use validator_derive::Validate;
use yew::prelude::*;
use yew_form_derive::Model;
use yew_router::{prelude::History, scope_ext::RouterScopeExt};
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "queries/create_user_attribute.graphql",
response_derives = "Debug",
custom_scalars_module = "crate::infra::graphql"
)]
pub struct CreateUserAttribute;
convert_attribute_type!(create_user_attribute::AttributeType);
pub struct CreateUserAttributeForm {
common: CommonComponentParts<Self>,
form: yew_form::Form<CreateUserAttributeModel>,
}
#[derive(Model, Validate, PartialEq, Eq, Clone, Default, Debug)]
pub struct CreateUserAttributeModel {
#[validate(length(min = 1, message = "attribute_name is required"))]
attribute_name: String,
#[validate(custom = "validate_attribute_type")]
attribute_type: String,
is_editable: bool,
is_list: bool,
is_visible: bool,
}
pub enum Msg {
Update,
SubmitForm,
CreateUserAttributeResponse(Result<create_user_attribute::ResponseData>),
}
impl CommonComponent<CreateUserAttributeForm> for CreateUserAttributeForm {
fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg {
Msg::Update => Ok(true),
Msg::SubmitForm => {
if !self.form.validate() {
bail!("Check the form for errors");
}
let model = self.form.model();
if model.is_editable && !model.is_visible {
bail!("Editable attributes must also be visible");
}
let attribute_type = model.attribute_type.parse::<AttributeType>().unwrap();
let req = create_user_attribute::Variables {
name: model.attribute_name,
attribute_type: create_user_attribute::AttributeType::from(attribute_type),
is_editable: model.is_editable,
is_list: model.is_list,
is_visible: model.is_visible,
};
self.common.call_graphql::<CreateUserAttribute, _>(
ctx,
req,
Msg::CreateUserAttributeResponse,
"Error trying to create user attribute",
);
Ok(true)
}
Msg::CreateUserAttributeResponse(response) => {
response?;
let model = self.form.model();
log!(&format!(
"Created user attribute '{}'",
model.attribute_name
));
ctx.link().history().unwrap().push(AppRoute::ListUserSchema);
Ok(true)
}
}
}
fn mut_common(&mut self) -> &mut CommonComponentParts<Self> {
&mut self.common
}
}
impl Component for CreateUserAttributeForm {
type Message = Msg;
type Properties = ();
fn create(_: &Context<Self>) -> Self {
let model = CreateUserAttributeModel {
attribute_type: AttributeType::String.to_string(),
..Default::default()
};
Self {
common: CommonComponentParts::<Self>::create(),
form: yew_form::Form::<CreateUserAttributeModel>::new(model),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, ctx, msg)
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="row justify-content-center">
<form class="form py-3" style="max-width: 636px">
<h5 class="fw-bold">{"Create a user attribute"}</h5>
<Field<CreateUserAttributeModel>
label="Name"
required={true}
form={&self.form}
field_name="attribute_name"
oninput={link.callback(|_| Msg::Update)} />
<Select<CreateUserAttributeModel>
label="Type"
required={true}
form={&self.form}
field_name="attribute_type"
oninput={link.callback(|_| Msg::Update)}>
<option selected=true value="String">{"String"}</option>
<option value="Integer">{"Integer"}</option>
<option value="Jpeg">{"Jpeg"}</option>
<option value="DateTime">{"DateTime"}</option>
</Select<CreateUserAttributeModel>>
<CheckBox<CreateUserAttributeModel>
label="Multiple values"
form={&self.form}
field_name="is_list"
ontoggle={link.callback(|_| Msg::Update)} />
<CheckBox<CreateUserAttributeModel>
label="Visible to users"
form={&self.form}
field_name="is_visible"
ontoggle={link.callback(|_| Msg::Update)} />
<CheckBox<CreateUserAttributeModel>
label="Editable by users"
form={&self.form}
field_name="is_editable"
ontoggle={link.callback(|_| Msg::Update)} />
<Submit
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})}/>
</form>
{ if let Some(e) = &self.common.error {
html! {
<div class="alert alert-danger">
{e.to_string() }
</div>
}
} else { html! {} }
}
</div>
}
}
}

View File

@@ -1,172 +0,0 @@
use crate::infra::{
common_component::{CommonComponent, CommonComponentParts},
modal::Modal,
};
use anyhow::{Error, Result};
use graphql_client::GraphQLQuery;
use yew::prelude::*;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "queries/delete_group_attribute.graphql",
response_derives = "Debug",
custom_scalars_module = "crate::infra::graphql"
)]
pub struct DeleteGroupAttributeQuery;
pub struct DeleteGroupAttribute {
common: CommonComponentParts<Self>,
node_ref: NodeRef,
modal: Option<Modal>,
}
#[derive(yew::Properties, Clone, PartialEq, Debug)]
pub struct DeleteGroupAttributeProps {
pub attribute_name: String,
pub on_attribute_deleted: Callback<String>,
pub on_error: Callback<Error>,
}
pub enum Msg {
ClickedDeleteGroupAttribute,
ConfirmDeleteGroupAttribute,
DismissModal,
DeleteGroupAttributeResponse(Result<delete_group_attribute_query::ResponseData>),
}
impl CommonComponent<DeleteGroupAttribute> for DeleteGroupAttribute {
fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg {
Msg::ClickedDeleteGroupAttribute => {
self.modal.as_ref().expect("modal not initialized").show();
}
Msg::ConfirmDeleteGroupAttribute => {
self.update(ctx, Msg::DismissModal);
self.common.call_graphql::<DeleteGroupAttributeQuery, _>(
ctx,
delete_group_attribute_query::Variables {
name: ctx.props().attribute_name.clone(),
},
Msg::DeleteGroupAttributeResponse,
"Error trying to delete group attribute",
);
}
Msg::DismissModal => {
self.modal.as_ref().expect("modal not initialized").hide();
}
Msg::DeleteGroupAttributeResponse(response) => {
response?;
ctx.props()
.on_attribute_deleted
.emit(ctx.props().attribute_name.clone());
}
}
Ok(true)
}
fn mut_common(&mut self) -> &mut CommonComponentParts<Self> {
&mut self.common
}
}
impl Component for DeleteGroupAttribute {
type Message = Msg;
type Properties = DeleteGroupAttributeProps;
fn create(_: &Context<Self>) -> Self {
Self {
common: CommonComponentParts::<Self>::create(),
node_ref: NodeRef::default(),
modal: None,
}
}
fn rendered(&mut self, _: &Context<Self>, first_render: bool) {
if first_render {
self.modal = Some(Modal::new(
self.node_ref
.cast::<web_sys::Element>()
.expect("Modal node is not an element"),
));
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update_and_report_error(
self,
ctx,
msg,
ctx.props().on_error.clone(),
)
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = &ctx.link();
html! {
<>
<button
class="btn btn-danger"
disabled={self.common.is_task_running()}
onclick={link.callback(|_| Msg::ClickedDeleteGroupAttribute)}>
<i class="bi-x-circle-fill" aria-label="Delete attribute" />
</button>
{self.show_modal(ctx)}
</>
}
}
}
impl DeleteGroupAttribute {
fn show_modal(&self, ctx: &Context<Self>) -> Html {
let link = &ctx.link();
html! {
<div
class="modal fade"
id={"deleteGroupAttributeModal".to_string() + &ctx.props().attribute_name}
tabindex="-1"
aria-labelledby="deleteGroupAttributeModalLabel"
aria-hidden="true"
ref={self.node_ref.clone()}>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteGroupAttributeModalLabel">{"Delete group attribute?"}</h5>
<button
type="button"
class="btn-close"
aria-label="Close"
onclick={link.callback(|_| Msg::DismissModal)} />
</div>
<div class="modal-body">
<span>
{"Are you sure you want to delete group attribute "}
<b>{&ctx.props().attribute_name}</b>{"?"}
</span>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
onclick={link.callback(|_| Msg::DismissModal)}>
<i class="bi-x-circle me-2"></i>
{"Cancel"}
</button>
<button
type="button"
onclick={link.callback(|_| Msg::ConfirmDeleteGroupAttribute)}
class="btn btn-danger">
<i class="bi-check-circle me-2"></i>
{"Yes, I'm sure"}
</button>
</div>
</div>
</div>
</div>
}
}
}

View File

@@ -1,172 +0,0 @@
use crate::infra::{
common_component::{CommonComponent, CommonComponentParts},
modal::Modal,
};
use anyhow::{Error, Result};
use graphql_client::GraphQLQuery;
use yew::prelude::*;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "queries/delete_user_attribute.graphql",
response_derives = "Debug",
custom_scalars_module = "crate::infra::graphql"
)]
pub struct DeleteUserAttributeQuery;
pub struct DeleteUserAttribute {
common: CommonComponentParts<Self>,
node_ref: NodeRef,
modal: Option<Modal>,
}
#[derive(yew::Properties, Clone, PartialEq, Debug)]
pub struct DeleteUserAttributeProps {
pub attribute_name: String,
pub on_attribute_deleted: Callback<String>,
pub on_error: Callback<Error>,
}
pub enum Msg {
ClickedDeleteUserAttribute,
ConfirmDeleteUserAttribute,
DismissModal,
DeleteUserAttributeResponse(Result<delete_user_attribute_query::ResponseData>),
}
impl CommonComponent<DeleteUserAttribute> for DeleteUserAttribute {
fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg {
Msg::ClickedDeleteUserAttribute => {
self.modal.as_ref().expect("modal not initialized").show();
}
Msg::ConfirmDeleteUserAttribute => {
self.update(ctx, Msg::DismissModal);
self.common.call_graphql::<DeleteUserAttributeQuery, _>(
ctx,
delete_user_attribute_query::Variables {
name: ctx.props().attribute_name.clone(),
},
Msg::DeleteUserAttributeResponse,
"Error trying to delete user attribute",
);
}
Msg::DismissModal => {
self.modal.as_ref().expect("modal not initialized").hide();
}
Msg::DeleteUserAttributeResponse(response) => {
response?;
ctx.props()
.on_attribute_deleted
.emit(ctx.props().attribute_name.clone());
}
}
Ok(true)
}
fn mut_common(&mut self) -> &mut CommonComponentParts<Self> {
&mut self.common
}
}
impl Component for DeleteUserAttribute {
type Message = Msg;
type Properties = DeleteUserAttributeProps;
fn create(_: &Context<Self>) -> Self {
Self {
common: CommonComponentParts::<Self>::create(),
node_ref: NodeRef::default(),
modal: None,
}
}
fn rendered(&mut self, _: &Context<Self>, first_render: bool) {
if first_render {
self.modal = Some(Modal::new(
self.node_ref
.cast::<web_sys::Element>()
.expect("Modal node is not an element"),
));
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update_and_report_error(
self,
ctx,
msg,
ctx.props().on_error.clone(),
)
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = &ctx.link();
html! {
<>
<button
class="btn btn-danger"
disabled={self.common.is_task_running()}
onclick={link.callback(|_| Msg::ClickedDeleteUserAttribute)}>
<i class="bi-x-circle-fill" aria-label="Delete attribute" />
</button>
{self.show_modal(ctx)}
</>
}
}
}
impl DeleteUserAttribute {
fn show_modal(&self, ctx: &Context<Self>) -> Html {
let link = &ctx.link();
html! {
<div
class="modal fade"
id={"deleteUserAttributeModal".to_string() + &ctx.props().attribute_name}
tabindex="-1"
aria-labelledby="deleteUserAttributeModalLabel"
aria-hidden="true"
ref={self.node_ref.clone()}>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteUserAttributeModalLabel">{"Delete user attribute?"}</h5>
<button
type="button"
class="btn-close"
aria-label="Close"
onclick={link.callback(|_| Msg::DismissModal)} />
</div>
<div class="modal-body">
<span>
{"Are you sure you want to delete user attribute "}
<b>{&ctx.props().attribute_name}</b>{"?"}
</span>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
onclick={link.callback(|_| Msg::DismissModal)}>
<i class="bi-x-circle me-2"></i>
{"Cancel"}
</button>
<button
type="button"
onclick={link.callback(|_| Msg::ConfirmDeleteUserAttribute)}
class="btn btn-danger">
<i class="bi-check-circle me-2"></i>
{"Yes, I'm sure"}
</button>
</div>
</div>
</div>
</div>
}
}
}

View File

@@ -1,68 +0,0 @@
use yew::{function_component, html, virtual_dom::AttrValue, Callback, InputEvent, Properties, NodeRef};
use crate::infra::schema::AttributeType;
/*
<input
ref={&ctx.props().input_ref}
type="text"
class="input-component"
placeholder={placeholder}
onmouseover={ctx.link().callback(|_| Msg::Hover)}
/>
*/
#[derive(Properties, PartialEq)]
struct AttributeInputProps {
name: AttrValue,
attribute_type: AttributeType,
#[prop_or(None)]
value: Option<String>,
}
#[function_component(AttributeInput)]
fn attribute_input(props: &AttributeInputProps) -> Html {
let input_type = match props.attribute_type {
AttributeType::String => "text",
AttributeType::Integer => "number",
AttributeType::DateTime => "datetime-local",
AttributeType::Jpeg => "file",
};
let accept = match props.attribute_type {
AttributeType::Jpeg => Some("image/jpeg"),
_ => None,
};
html! {
<input
type={input_type}
accept={accept}
name={props.name.clone()}
class="form-control"
value={props.value.clone()} />
}
}
#[derive(Properties, PartialEq)]
pub struct SingleAttributeInputProps {
pub name: AttrValue,
pub attribute_type: AttributeType,
#[prop_or(None)]
pub value: Option<String>,
}
#[function_component(SingleAttributeInput)]
pub fn single_attribute_input(props: &SingleAttributeInputProps) -> Html {
html! {
<div class="row mb-3">
<label for={props.name.clone()}
class="form-label col-4 col-form-label">
{&props.name}{":"}
</label>
<div class="col-8">
<AttributeInput
attribute_type={props.attribute_type}
name={props.name}
value={props.value} />
</div>
</div>
}
}

View File

@@ -1,35 +0,0 @@
use yew::{function_component, html, virtual_dom::AttrValue, Callback, Properties};
use yew_form::{Form, Model};
#[derive(Properties, PartialEq)]
pub struct Props<T: Model> {
pub label: AttrValue,
pub field_name: String,
pub form: Form<T>,
#[prop_or(false)]
pub required: bool,
#[prop_or_else(Callback::noop)]
pub ontoggle: Callback<bool>,
}
#[function_component(CheckBox)]
pub fn checkbox<T: Model>(props: &Props<T>) -> Html {
html! {
<div class="form-group row mb-3">
<label for={props.field_name.clone()}
class="form-label col-4 col-form-label">
{&props.label}
{if props.required {
html!{<span class="text-danger">{"*"}</span>}
} else {html!{}}}
{":"}
</label>
<div class="col-8">
<yew_form::CheckBox<T>
form={&props.form}
field_name={props.field_name.clone()}
ontoggle={props.ontoggle.clone()} />
</div>
</div>
}
}

View File

@@ -1,48 +0,0 @@
use yew::{function_component, html, virtual_dom::AttrValue, Callback, InputEvent, Properties};
use yew_form::{Form, Model};
#[derive(Properties, PartialEq)]
pub struct Props<T: Model> {
pub label: AttrValue,
pub field_name: String,
pub form: Form<T>,
#[prop_or(false)]
pub required: bool,
#[prop_or(String::from("text"))]
pub input_type: String,
// If not present, will default to field_name
#[prop_or(None)]
pub autocomplete: Option<String>,
#[prop_or_else(Callback::noop)]
pub oninput: Callback<InputEvent>,
}
#[function_component(Field)]
pub fn field<T: Model>(props: &Props<T>) -> Html {
html! {
<div class="row mb-3">
<label for={props.field_name.clone()}
class="form-label col-4 col-form-label">
{&props.label}
{if props.required {
html!{<span class="text-danger">{"*"}</span>}
} else {html!{}}}
{":"}
</label>
<div class="col-8">
<yew_form::Field<T>
form={&props.form}
field_name={props.field_name.clone()}
input_type={props.input_type.clone()}
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete={props.autocomplete.clone().unwrap_or(props.field_name.clone())}
oninput={&props.oninput} />
<div class="invalid-feedback">
{&props.form.field_message(&props.field_name)}
</div>
</div>
</div>
}
}

View File

@@ -1,6 +0,0 @@
pub mod attribute_input;
pub mod checkbox;
pub mod field;
pub mod select;
pub mod static_value;
pub mod submit;

View File

@@ -1,46 +0,0 @@
use yew::{
function_component, html, virtual_dom::AttrValue, Callback, Children, InputEvent, Properties,
};
use yew_form::{Form, Model};
#[derive(Properties, PartialEq)]
pub struct Props<T: Model> {
pub label: AttrValue,
pub field_name: String,
pub form: Form<T>,
#[prop_or(false)]
pub required: bool,
#[prop_or_else(Callback::noop)]
pub oninput: Callback<InputEvent>,
pub children: Children,
}
#[function_component(Select)]
pub fn select<T: Model>(props: &Props<T>) -> Html {
html! {
<div class="row mb-3">
<label for={props.field_name.clone()}
class="form-label col-4 col-form-label">
{&props.label}
{if props.required {
html!{<span class="text-danger">{"*"}</span>}
} else {html!{}}}
{":"}
</label>
<div class="col-8">
<yew_form::Select<T>
form={&props.form}
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
field_name={props.field_name.clone()}
oninput={&props.oninput} >
{for props.children.iter()}
</yew_form::Select<T>>
<div class="invalid-feedback">
{&props.form.field_message(&props.field_name)}
</div>
</div>
</div>
}
}

View File

@@ -1,26 +0,0 @@
use yew::{function_component, html, virtual_dom::AttrValue, Children, Properties};
#[derive(Properties, PartialEq)]
pub struct Props {
pub label: AttrValue,
pub id: AttrValue,
pub children: Children,
}
#[function_component(StaticValue)]
pub fn static_value(props: &Props) -> Html {
html! {
<div class="row mb-3">
<label for={props.id.clone()}
class="form-label col-4 col-form-label">
{&props.label}
{":"}
</label>
<div class="col-8">
<span id={props.id.clone()} class="form-control-static">
{for props.children.iter()}
</span>
</div>
</div>
}
}

View File

@@ -1,30 +0,0 @@
use web_sys::MouseEvent;
use yew::{function_component, html, virtual_dom::AttrValue, Callback, Children, Properties};
#[derive(Properties, PartialEq)]
pub struct Props {
pub disabled: bool,
pub onclick: Callback<MouseEvent>,
// Additional elements to insert after the button, in the same div
#[prop_or_default]
pub children: Children,
#[prop_or(AttrValue::from("Submit"))]
pub text: AttrValue,
}
#[function_component(Submit)]
pub fn submit(props: &Props) -> Html {
html! {
<div class="form-group row justify-content-center">
<button
class="btn btn-primary col-auto col-form-label"
type="submit"
disabled={props.disabled}
onclick={&props.onclick}>
<i class="bi-save me-2"></i>
{props.text.clone()}
</button>
{for props.children.iter()}
</div>
}
}

View File

@@ -1,198 +0,0 @@
use crate::{
components::{
delete_group_attribute::DeleteGroupAttribute,
router::{AppRoute, Link},
},
convert_attribute_type,
infra::{
common_component::{CommonComponent, CommonComponentParts},
schema::AttributeType,
},
};
use anyhow::{anyhow, Error, Result};
use gloo_console::log;
use graphql_client::GraphQLQuery;
use yew::prelude::*;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "queries/get_group_attributes_schema.graphql",
response_derives = "Debug,Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql"
)]
pub struct GetGroupAttributesSchema;
use get_group_attributes_schema::ResponseData;
pub type Attribute =
get_group_attributes_schema::GetGroupAttributesSchemaSchemaGroupSchemaAttributes;
convert_attribute_type!(get_group_attributes_schema::AttributeType);
#[derive(yew::Properties, Clone, PartialEq, Eq)]
pub struct Props {
pub hardcoded: bool,
}
pub struct GroupSchemaTable {
common: CommonComponentParts<Self>,
attributes: Option<Vec<Attribute>>,
}
pub enum Msg {
ListAttributesResponse(Result<ResponseData>),
OnAttributeDeleted(String),
OnError(Error),
}
impl CommonComponent<GroupSchemaTable> for GroupSchemaTable {
fn handle_msg(&mut self, _: &Context<Self>, msg: <Self as Component>::Message) -> Result<bool> {
match msg {
Msg::ListAttributesResponse(schema) => {
self.attributes =
Some(schema?.schema.group_schema.attributes.into_iter().collect());
Ok(true)
}
Msg::OnError(e) => Err(e),
Msg::OnAttributeDeleted(attribute_name) => {
match self.attributes {
None => {
log!(format!("Attribute {attribute_name} was deleted but component has no attributes"));
Err(anyhow!("invalid state"))
}
Some(_) => {
self.attributes
.as_mut()
.unwrap()
.retain(|a| a.name != attribute_name);
Ok(true)
}
}
}
}
}
fn mut_common(&mut self) -> &mut CommonComponentParts<Self> {
&mut self.common
}
}
impl Component for GroupSchemaTable {
type Message = Msg;
type Properties = Props;
fn create(ctx: &Context<Self>) -> Self {
let mut table = GroupSchemaTable {
common: CommonComponentParts::<Self>::create(),
attributes: None,
};
table.common.call_graphql::<GetGroupAttributesSchema, _>(
ctx,
get_group_attributes_schema::Variables {},
Msg::ListAttributesResponse,
"Error trying to fetch group schema",
);
table
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, ctx, msg)
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<div>
{self.view_attributes(ctx)}
{self.view_errors()}
</div>
}
}
}
impl GroupSchemaTable {
fn view_attributes(&self, ctx: &Context<Self>) -> Html {
let hardcoded = ctx.props().hardcoded;
let make_table = |attributes: &Vec<Attribute>| {
html! {
<div class="table-responsive">
<h3>{if hardcoded {"Hardcoded"} else {"User-defined"}}{" attributes"}</h3>
<table class="table table-hover">
<thead>
<tr>
<th>{"Attribute name"}</th>
<th>{"Type"}</th>
<th>{"Visible"}</th>
{if hardcoded {html!{}} else {html!{<th>{"Delete"}</th>}}}
</tr>
</thead>
<tbody>
{attributes.iter().map(|u| self.view_attribute(ctx, u)).collect::<Vec<_>>()}
</tbody>
</table>
</div>
}
};
match &self.attributes {
None => html! {{"Loading..."}},
Some(attributes) => {
let mut attributes = attributes.clone();
attributes.retain(|attribute| attribute.is_hardcoded == ctx.props().hardcoded);
make_table(&attributes)
}
}
}
fn view_attribute(&self, ctx: &Context<Self>, attribute: &Attribute) -> Html {
let link = ctx.link();
let attribute_type = AttributeType::from(attribute.attribute_type.clone());
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">
<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>
};
let hardcoded = ctx.props().hardcoded;
html! {
<tr key={attribute.name.clone()}>
<td>{&attribute.name}</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>
{
if hardcoded {
html!{}
} else {
html!{
<td>
<DeleteGroupAttribute
attribute_name={attribute.name.clone()}
on_attribute_deleted={link.callback(Msg::OnAttributeDeleted)}
on_error={link.callback(Msg::OnError)}/>
</td>
}
}
}
</tr>
}
}
fn view_errors(&self) -> Html {
match &self.common.error {
None => html! {},
Some(e) => html! {<div>{"Error: "}{e.to_string()}</div>},
}
}
}
#[function_component(ListGroupSchema)]
pub fn list_group_schema() -> Html {
html! {
<div>
<GroupSchemaTable hardcoded={true} />
<GroupSchemaTable hardcoded={false} />
<Link classes="btn btn-primary" to={AppRoute::CreateGroupAttribute}>
<i class="bi-plus-circle me-2"></i>
{"Create an attribute"}
</Link>
</div>
}
}

View File

@@ -1,8 +1,5 @@
use crate::{
components::{
form::submit::Submit,
router::{AppRoute, Link},
},
components::router::{AppRoute, Link},
infra::{
api::HostService,
common_component::{CommonComponent, CommonComponentParts},
@@ -69,7 +66,7 @@ impl CommonComponent<LoginForm> for LoginForm {
opaque::client::login::start_login(&password, &mut rng)
.context("Could not initialize login")?;
let req = login::ClientLoginStartRequest {
username: username.into(),
username,
login_start_request: message,
};
self.common
@@ -158,62 +155,68 @@ impl Component for LoginForm {
}
} else {
html! {
<form class="form center-block col-sm-4 col-offset-4">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="bi-person-fill"/>
</span>
<form
class="form center-block col-sm-4 col-offset-4">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="bi-person-fill"/>
</span>
</div>
<Field
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
form={&self.form}
field_name="username"
placeholder="Username"
autocomplete="username"
oninput={link.callback(|_| Msg::Update)} />
</div>
<Field
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
form={&self.form}
field_name="username"
placeholder="Username"
autocomplete="username"
oninput={link.callback(|_| Msg::Update)} />
</div>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="bi-lock-fill"/>
</span>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="bi-lock-fill"/>
</span>
</div>
<Field
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
form={&self.form}
field_name="password"
input_type="password"
placeholder="Password"
autocomplete="current-password" />
</div>
<div class="form-group mt-3">
<button
type="submit"
class="btn btn-primary"
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})}>
<i class="bi-box-arrow-in-right me-2"/>
{"Login"}
</button>
{ if password_reset_enabled {
html! {
<Link
classes="btn-link btn"
disabled={self.common.is_task_running()}
to={AppRoute::StartResetPassword}>
{"Forgot your password?"}
</Link>
}
} else {
html!{}
}}
</div>
<div class="form-group">
{ if let Some(e) = &self.common.error {
html! { e.to_string() }
} else { html! {} }
}
</div>
<Field
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
form={&self.form}
field_name="password"
input_type="password"
placeholder="Password"
autocomplete="current-password" />
</div>
<Submit
text="Login"
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})}>
{ if password_reset_enabled {
html! {
<Link
classes="btn-link btn"
disabled={self.common.is_task_running()}
to={AppRoute::StartResetPassword}>
{"Forgot your password?"}
</Link>
}
} else {
html!{}
}}
</Submit>
<div class="form-group">
{ if let Some(e) = &self.common.error {
html! { e.to_string() }
} else { html! {} }
}
</div>
</form>
}
}

View File

@@ -1,20 +1,12 @@
pub mod add_group_member;
pub mod add_user_to_group;
pub mod app;
pub mod avatar;
pub mod banner;
pub mod change_password;
pub mod create_group;
pub mod create_group_attribute;
pub mod create_user;
pub mod create_user_attribute;
pub mod delete_group;
pub mod delete_group_attribute;
pub mod delete_user;
pub mod delete_user_attribute;
pub mod form;
pub mod group_details;
pub mod group_schema_table;
pub mod group_table;
pub mod login;
pub mod logout;
@@ -25,5 +17,4 @@ pub mod router;
pub mod select;
pub mod user_details;
pub mod user_details_form;
pub mod user_schema_table;
pub mod user_table;

View File

@@ -1,8 +1,5 @@
use crate::{
components::{
form::{field::Field, submit::Submit},
router::{AppRoute, Link},
},
components::router::{AppRoute, Link},
infra::{
api::HostService,
common_component::{CommonComponent, CommonComponentParts},
@@ -68,10 +65,10 @@ impl CommonComponent<ResetPasswordStep2Form> for ResetPasswordStep2Form {
let mut rng = rand::rngs::OsRng;
let new_password = self.form.model().password;
let registration_start_request =
opaque_registration::start_registration(new_password.as_bytes(), &mut rng)
opaque_registration::start_registration(&new_password, &mut rng)
.context("Could not initiate password change")?;
let req = registration::ClientRegistrationStartRequest {
username: self.username.as_ref().unwrap().into(),
username: self.username.clone().unwrap(),
registration_start_request: registration_start_request.message,
};
self.opaque_data = Some(registration_start_request.state);
@@ -167,29 +164,61 @@ impl Component for ResetPasswordStep2Form {
}
_ => (),
};
type Field = yew_form::Field<FormModel>;
html! {
<>
<h2>{"Reset your password"}</h2>
<form class="form">
<Field<FormModel>
label="New password"
required=true
form={&self.form}
field_name="password"
autocomplete="new-password"
input_type="password"
oninput={link.callback(|_| Msg::FormUpdate)} />
<Field<FormModel>
label="Confirm password"
required=true
form={&self.form}
field_name="confirm_password"
autocomplete="new-password"
input_type="password"
oninput={link.callback(|_| Msg::FormUpdate)} />
<Submit
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})} />
<form
class="form">
<div class="form-group row">
<label for="new_password"
class="form-label col-sm-2 col-form-label">
{"New password*:"}
</label>
<div class="col-sm-10">
<Field
form={&self.form}
field_name="password"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="new-password"
input_type="password"
oninput={link.callback(|_| Msg::FormUpdate)} />
<div class="invalid-feedback">
{&self.form.field_message("password")}
</div>
</div>
</div>
<div class="form-group row">
<label for="confirm_password"
class="form-label col-sm-2 col-form-label">
{"Confirm password*:"}
</label>
<div class="col-sm-10">
<Field
form={&self.form}
field_name="confirm_password"
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
autocomplete="new-password"
input_type="password"
oninput={link.callback(|_| Msg::FormUpdate)} />
<div class="invalid-feedback">
{&self.form.field_message("confirm_password")}
</div>
</div>
</div>
<div class="form-group row mt-2">
<button
class="btn btn-primary col-sm-1 col-form-label"
type="submit"
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})}>
{"Submit"}
</button>
</div>
</form>
{ if let Some(e) = &self.common.error {
html! {

View File

@@ -22,14 +22,6 @@ pub enum AppRoute {
ListGroups,
#[at("/group/:group_id")]
GroupDetails { group_id: i64 },
#[at("/user-attributes")]
ListUserSchema,
#[at("/user-attributes/create")]
CreateUserAttribute,
#[at("/group-attributes")]
ListGroupSchema,
#[at("/group-attributes/create")]
CreateGroupAttribute,
#[at("/")]
Index,
}

View File

@@ -1,10 +1,7 @@
use std::str::FromStr;
use crate::{
components::{
form::{field::Field, static_value::StaticValue, submit::Submit},
user_details::User,
},
components::user_details::User,
infra::common_component::{CommonComponent, CommonComponentParts},
};
use anyhow::{bail, Error, Result};
@@ -14,10 +11,9 @@ use gloo_file::{
};
use graphql_client::GraphQLQuery;
use validator_derive::Validate;
use web_sys::{FileList, HtmlInputElement, InputEvent, SubmitEvent};
use web_sys::{FileList, HtmlInputElement, InputEvent};
use yew::prelude::*;
use yew_form_derive::Model;
use gloo_console::log;
#[derive(Default)]
struct JsFile {
@@ -27,7 +23,10 @@ struct JsFile {
impl ToString for JsFile {
fn to_string(&self) -> String {
self.file.as_ref().map(File::name).unwrap_or_default()
self.file
.as_ref()
.map(File::name)
.unwrap_or_else(String::new)
}
}
@@ -68,8 +67,7 @@ pub struct UpdateUser;
pub struct UserDetailsForm {
common: CommonComponentParts<Self>,
form: yew_form::Form<UserModel>,
// None means that the avatar hasn't changed.
avatar: Option<JsFile>,
avatar: JsFile,
reader: Option<FileReader>,
/// True if we just successfully updated the user, to display a success message.
just_updated: bool,
@@ -83,14 +81,10 @@ pub enum Msg {
FileSelected(File),
/// The "Submit" button was clicked.
SubmitClicked,
/// The "Clear" button for the avatar was clicked.
ClearAvatarClicked,
/// A picked file finished loading.
FileLoaded(String, Result<Vec<u8>>),
/// We got the response from the server about our update message.
UserUpdated(Result<update_user::ResponseData>),
/// The "Submit" button was clicked.
OnSubmit(SubmitEvent),
}
#[derive(yew::Properties, Clone, PartialEq, Eq)]
@@ -108,12 +102,7 @@ impl CommonComponent<UserDetailsForm> for UserDetailsForm {
match msg {
Msg::Update => Ok(true),
Msg::FileSelected(new_avatar) => {
if self
.avatar
.as_ref()
.and_then(|f| f.file.as_ref().map(|f| f.name()))
!= Some(new_avatar.name())
{
if self.avatar.file.as_ref().map(|f| f.name()) != Some(new_avatar.name()) {
let file_name = new_avatar.name();
let link = ctx.link().clone();
self.reader = Some(read_as_bytes(&new_avatar, move |res| {
@@ -122,42 +111,32 @@ impl CommonComponent<UserDetailsForm> for UserDetailsForm {
res.map_err(|e| anyhow::anyhow!("{:#}", e)),
))
}));
self.avatar = Some(JsFile {
self.avatar = JsFile {
file: Some(new_avatar),
contents: None,
});
};
}
Ok(true)
}
Msg::SubmitClicked => self.submit_user_update_form(ctx),
Msg::ClearAvatarClicked => {
self.avatar = Some(JsFile::default());
Ok(true)
}
Msg::UserUpdated(response) => self.user_update_finished(response),
Msg::FileLoaded(file_name, data) => {
if let Some(avatar) = &mut self.avatar {
if let Some(file) = &avatar.file {
if file.name() == file_name {
let data = data?;
if !is_valid_jpeg(data.as_slice()) {
// Clear the selection.
self.avatar = None;
bail!("Chosen image is not a valid JPEG");
} else {
avatar.contents = Some(data);
return Ok(true);
}
if let Some(file) = &self.avatar.file {
if file.name() == file_name {
let data = data?;
if !is_valid_jpeg(data.as_slice()) {
// Clear the selection.
self.avatar = JsFile::default();
bail!("Chosen image is not a valid JPEG");
} else {
self.avatar.contents = Some(data);
return Ok(true);
}
}
}
self.reader = None;
Ok(false)
}
Msg::OnSubmit(e) => {
log!(format!("{:#?}", e));
Ok(true)
}
}
}
@@ -180,7 +159,7 @@ impl Component for UserDetailsForm {
Self {
common: CommonComponentParts::<Self>::create(),
form: yew_form::Form::new(model),
avatar: None,
avatar: JsFile::default(),
just_updated: false,
reader: None,
user: ctx.props().user.clone(),
@@ -193,52 +172,118 @@ impl Component for UserDetailsForm {
}
fn view(&self, ctx: &Context<Self>) -> Html {
type Field = yew_form::Field<UserModel>;
let link = &ctx.link();
let avatar_string = match &self.avatar {
Some(avatar) => {
let avatar_base64 = to_base64(avatar);
avatar_base64.as_deref().unwrap_or("").to_owned()
}
None => self.user.avatar.as_deref().unwrap_or("").to_owned(),
};
let avatar_base64 = maybe_to_base64(&self.avatar).unwrap_or_default();
let avatar_string = avatar_base64
.as_deref()
.or(self.user.avatar.as_deref())
.unwrap_or("");
html! {
<div class="py-3">
<form class="form" onsubmit={link.callback(|e: SubmitEvent| {e.prevent_default(); Msg::OnSubmit(e)})}>
<StaticValue label="User ID" id="userId">
<i>{&self.user.id}</i>
</StaticValue>
<StaticValue label="Creation date" id="creationDate">
{&self.user.creation_date.naive_local().date()}
</StaticValue>
<StaticValue label="UUID" id="uuid">
{&self.user.uuid}
</StaticValue>
<Field<UserModel>
form={&self.form}
required=true
label="Email"
field_name="email"
input_type="email"
oninput={link.callback(|_| Msg::Update)} />
<Field<UserModel>
form={&self.form}
label="Display name"
field_name="display_name"
autocomplete="name"
oninput={link.callback(|_| Msg::Update)} />
<Field<UserModel>
form={&self.form}
label="First name"
field_name="first_name"
autocomplete="given-name"
oninput={link.callback(|_| Msg::Update)} />
<Field<UserModel>
form={&self.form}
label="Last name"
field_name="last_name"
autocomplete="family-name"
oninput={link.callback(|_| Msg::Update)} />
<form class="form">
<div class="form-group row mb-3">
<label for="userId"
class="form-label col-4 col-form-label">
{"User ID: "}
</label>
<div class="col-8">
<span id="userId" class="form-control-static"><i>{&self.user.id}</i></span>
</div>
</div>
<div class="form-group row mb-3">
<label for="creationDate"
class="form-label col-4 col-form-label">
{"Creation date: "}
</label>
<div class="col-8">
<span id="creationDate" class="form-control-static">{&self.user.creation_date.naive_local().date()}</span>
</div>
</div>
<div class="form-group row mb-3">
<label for="uuid"
class="form-label col-4 col-form-label">
{"UUID: "}
</label>
<div class="col-8">
<span id="creationDate" class="form-control-static">{&self.user.uuid}</span>
</div>
</div>
<div class="form-group row mb-3">
<label for="email"
class="form-label col-4 col-form-label">
{"Email"}
<span class="text-danger">{"*"}</span>
{":"}
</label>
<div class="col-8">
<Field
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
form={&self.form}
field_name="email"
autocomplete="email"
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("email")}
</div>
</div>
</div>
<div class="form-group row mb-3">
<label for="display_name"
class="form-label col-4 col-form-label">
{"Display Name: "}
</label>
<div class="col-8">
<Field
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
form={&self.form}
field_name="display_name"
autocomplete="name"
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("display_name")}
</div>
</div>
</div>
<div class="form-group row mb-3">
<label for="first_name"
class="form-label col-4 col-form-label">
{"First Name: "}
</label>
<div class="col-8">
<Field
class="form-control"
form={&self.form}
field_name="first_name"
autocomplete="given-name"
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("first_name")}
</div>
</div>
</div>
<div class="form-group row mb-3">
<label for="last_name"
class="form-label col-4 col-form-label">
{"Last Name: "}
</label>
<div class="col-8">
<Field
class="form-control"
form={&self.form}
field_name="last_name"
autocomplete="family-name"
oninput={link.callback(|_| Msg::Update)} />
<div class="invalid-feedback">
{&self.form.field_message("last_name")}
</div>
</div>
</div>
<div class="form-group row align-items-center mb-3">
<label for="avatar"
class="form-label col-4 col-form-label">
@@ -246,7 +291,7 @@ impl Component for UserDetailsForm {
</label>
<div class="col-8">
<div class="row align-items-center">
<div class="col-5">
<div class="col-8">
<input
class="form-control"
id="avatarInput"
@@ -257,35 +302,26 @@ impl Component for UserDetailsForm {
Self::upload_files(input.files())
})} />
</div>
<div class="col-3">
<button
class="btn btn-secondary col-auto"
id="avatarClear"
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::ClearAvatarClicked})}>
{"Clear"}
</button>
</div>
<div class="col-4">
{
if !avatar_string.is_empty() {
html!{
<img
id="avatarDisplay"
src={format!("data:image/jpeg;base64, {}", avatar_string)}
style="max-height:128px;max-width:128px;height:auto;width:auto;"
alt="Avatar" />
}
} else { html! {} }
}
<img
id="avatarDisplay"
src={format!("data:image/jpeg;base64, {}", avatar_string)}
style="max-height:128px;max-width:128px;height:auto;width:auto;"
alt="Avatar" />
</div>
</div>
</div>
</div>
<Submit
text="Save changes"
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {Msg::SubmitClicked})} />
<div class="form-group row justify-content-center mt-3">
<button
type="submit"
class="btn btn-primary col-auto col-form-label"
disabled={self.common.is_task_running()}
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitClicked})}>
<i class="bi-save me-2"></i>
{"Save changes"}
</button>
</div>
</form>
{
if let Some(e) = &self.common.error {
@@ -309,10 +345,10 @@ impl UserDetailsForm {
if !self.form.validate() {
bail!("Invalid inputs");
}
if let Some(JsFile {
if let JsFile {
file: Some(_),
contents: None,
}) = &self.avatar
} = &self.avatar
{
bail!("Image file hasn't finished loading, try again");
}
@@ -324,8 +360,6 @@ impl UserDetailsForm {
firstName: None,
lastName: None,
avatar: None,
removeAttributes: None,
insertAttributes: None,
};
let default_user_input = user_input.clone();
let model = self.form.model();
@@ -342,9 +376,7 @@ impl UserDetailsForm {
if base_user.last_name != model.last_name {
user_input.lastName = Some(model.last_name);
}
if let Some(avatar) = &self.avatar {
user_input.avatar = Some(to_base64(avatar)?);
}
user_input.avatar = maybe_to_base64(&self.avatar)?;
// Nothing changed.
if user_input == default_user_input {
return Ok(false);
@@ -366,8 +398,8 @@ impl UserDetailsForm {
self.user.display_name = model.display_name;
self.user.first_name = model.first_name;
self.user.last_name = model.last_name;
if let Some(avatar) = &self.avatar {
self.user.avatar = Some(to_base64(avatar)?);
if let Some(avatar) = maybe_to_base64(&self.avatar)? {
self.user.avatar = Some(avatar);
}
self.just_updated = true;
Ok(true)
@@ -392,12 +424,12 @@ fn is_valid_jpeg(bytes: &[u8]) -> bool {
.is_ok()
}
fn to_base64(file: &JsFile) -> Result<String> {
fn maybe_to_base64(file: &JsFile) -> Result<Option<String>> {
match file {
JsFile {
file: None,
contents: _,
} => Ok(String::new()),
} => Ok(None),
JsFile {
file: Some(_),
contents: None,
@@ -409,7 +441,7 @@ fn to_base64(file: &JsFile) -> Result<String> {
if !is_valid_jpeg(data.as_slice()) {
bail!("Chosen image is not a valid JPEG");
}
Ok(base64::encode(data))
Ok(Some(base64::encode(data)))
}
}
}

View File

@@ -1,198 +0,0 @@
use crate::{
components::{
delete_user_attribute::DeleteUserAttribute,
router::{AppRoute, Link},
},
convert_attribute_type,
infra::{
common_component::{CommonComponent, CommonComponentParts},
schema::AttributeType,
},
};
use anyhow::{anyhow, Error, Result};
use gloo_console::log;
use graphql_client::GraphQLQuery;
use yew::prelude::*;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "queries/get_user_attributes_schema.graphql",
response_derives = "Debug,Clone,PartialEq,Eq",
custom_scalars_module = "crate::infra::graphql"
)]
pub struct GetUserAttributesSchema;
use get_user_attributes_schema::ResponseData;
pub type Attribute = get_user_attributes_schema::GetUserAttributesSchemaSchemaUserSchemaAttributes;
convert_attribute_type!(get_user_attributes_schema::AttributeType);
#[derive(yew::Properties, Clone, PartialEq, Eq)]
pub struct Props {
pub hardcoded: bool,
}
pub struct UserSchemaTable {
common: CommonComponentParts<Self>,
attributes: Option<Vec<Attribute>>,
}
pub enum Msg {
ListAttributesResponse(Result<ResponseData>),
OnAttributeDeleted(String),
OnError(Error),
}
impl CommonComponent<UserSchemaTable> for UserSchemaTable {
fn handle_msg(&mut self, _: &Context<Self>, msg: <Self as Component>::Message) -> Result<bool> {
match msg {
Msg::ListAttributesResponse(schema) => {
self.attributes = Some(schema?.schema.user_schema.attributes.into_iter().collect());
Ok(true)
}
Msg::OnError(e) => Err(e),
Msg::OnAttributeDeleted(attribute_name) => {
match self.attributes {
None => {
log!(format!("Attribute {attribute_name} was deleted but component has no attributes"));
Err(anyhow!("invalid state"))
}
Some(_) => {
self.attributes
.as_mut()
.unwrap()
.retain(|a| a.name != attribute_name);
Ok(true)
}
}
}
}
}
fn mut_common(&mut self) -> &mut CommonComponentParts<Self> {
&mut self.common
}
}
impl Component for UserSchemaTable {
type Message = Msg;
type Properties = Props;
fn create(ctx: &Context<Self>) -> Self {
let mut table = UserSchemaTable {
common: CommonComponentParts::<Self>::create(),
attributes: None,
};
table.common.call_graphql::<GetUserAttributesSchema, _>(
ctx,
get_user_attributes_schema::Variables {},
Msg::ListAttributesResponse,
"Error trying to fetch user schema",
);
table
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, ctx, msg)
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<div>
{self.view_attributes(ctx)}
{self.view_errors()}
</div>
}
}
}
impl UserSchemaTable {
fn view_attributes(&self, ctx: &Context<Self>) -> Html {
let hardcoded = ctx.props().hardcoded;
let make_table = |attributes: &Vec<Attribute>| {
html! {
<div class="table-responsive">
<h3>{if hardcoded {"Hardcoded"} else {"User-defined"}}{" attributes"}</h3>
<table class="table table-hover">
<thead>
<tr>
<th>{"Attribute name"}</th>
<th>{"Type"}</th>
<th>{"Editable"}</th>
<th>{"Visible"}</th>
{if hardcoded {html!{}} else {html!{<th>{"Delete"}</th>}}}
</tr>
</thead>
<tbody>
{attributes.iter().map(|u| self.view_attribute(ctx, u)).collect::<Vec<_>>()}
</tbody>
</table>
</div>
}
};
match &self.attributes {
None => html! {{"Loading..."}},
Some(attributes) => {
let mut attributes = attributes.clone();
attributes.retain(|attribute| attribute.is_hardcoded == ctx.props().hardcoded);
make_table(&attributes)
}
}
}
fn view_attribute(&self, ctx: &Context<Self>, attribute: &Attribute) -> Html {
let link = ctx.link();
let attribute_type = AttributeType::from(attribute.attribute_type.clone());
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">
<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>
};
let hardcoded = ctx.props().hardcoded;
html! {
<tr key={attribute.name.clone()}>
<td>{&attribute.name}</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_visible {checkmark.clone()} else {html!{}}}</td>
{
if hardcoded {
html!{}
} else {
html!{
<td>
<DeleteUserAttribute
attribute_name={attribute.name.clone()}
on_attribute_deleted={link.callback(Msg::OnAttributeDeleted)}
on_error={link.callback(Msg::OnError)}/>
</td>
}
}
}
</tr>
}
}
fn view_errors(&self) -> Html {
match &self.common.error {
None => html! {},
Some(e) => html! {<div>{"Error: "}{e.to_string()}</div>},
}
}
}
#[function_component(ListUserSchema)]
pub fn list_user_schema() -> Html {
html! {
<div>
<UserSchemaTable hardcoded={true} />
<UserSchemaTable hardcoded={false} />
<Link classes="btn btn-primary" to={AppRoute::CreateUserAttribute}>
<i class="bi-plus-circle me-2"></i>
{"Create an attribute"}
</Link>
</div>
}
}

View File

@@ -18,10 +18,6 @@ fn get_claims_from_jwt(jwt: &str) -> Result<JWTClaims> {
const NO_BODY: Option<()> = None;
fn base_url() -> String {
yew_router::utils::base_url().unwrap_or_default()
}
async fn call_server(
url: &str,
body: Option<impl Serialize>,
@@ -101,7 +97,7 @@ impl HostService {
};
let request_body = QueryType::build_query(variables);
call_server_json_with_error_message::<graphql_client::Response<_>, _>(
&(base_url() + "/api/graphql"),
"/api/graphql",
Some(request_body),
error_message,
)
@@ -113,7 +109,7 @@ impl HostService {
request: login::ClientLoginStartRequest,
) -> Result<Box<login::ServerLoginStartResponse>> {
call_server_json_with_error_message(
&(base_url() + "/auth/opaque/login/start"),
"/auth/opaque/login/start",
Some(request),
"Could not start authentication: ",
)
@@ -122,7 +118,7 @@ impl HostService {
pub async fn login_finish(request: login::ClientLoginFinishRequest) -> Result<(String, bool)> {
call_server_json_with_error_message::<login::ServerLoginResponse, _>(
&(base_url() + "/auth/opaque/login/finish"),
"/auth/opaque/login/finish",
Some(request),
"Could not finish authentication",
)
@@ -134,7 +130,7 @@ impl HostService {
request: registration::ClientRegistrationStartRequest,
) -> Result<Box<registration::ServerRegistrationStartResponse>> {
call_server_json_with_error_message(
&(base_url() + "/auth/opaque/register/start"),
"/auth/opaque/register/start",
Some(request),
"Could not start registration: ",
)
@@ -145,7 +141,7 @@ impl HostService {
request: registration::ClientRegistrationFinishRequest,
) -> Result<()> {
call_server_empty_response_with_error_message(
&(base_url() + "/auth/opaque/register/finish"),
"/auth/opaque/register/finish",
Some(request),
"Could not finish registration",
)
@@ -154,7 +150,7 @@ impl HostService {
pub async fn refresh() -> Result<(String, bool)> {
call_server_json_with_error_message::<login::ServerLoginResponse, _>(
&(base_url() + "/auth/refresh"),
"/auth/refresh",
NO_BODY,
"Could not start authentication: ",
)
@@ -164,21 +160,13 @@ impl HostService {
// The `_request` parameter is to make it the same shape as the other functions.
pub async fn logout() -> Result<()> {
call_server_empty_response_with_error_message(
&(base_url() + "/auth/logout"),
NO_BODY,
"Could not logout",
)
.await
call_server_empty_response_with_error_message("/auth/logout", NO_BODY, "Could not logout")
.await
}
pub async fn reset_password_step1(username: String) -> Result<()> {
call_server_empty_response_with_error_message(
&format!(
"{}/auth/reset/step1/{}",
base_url(),
url_escape::encode_query(&username)
),
&format!("/auth/reset/step1/{}", url_escape::encode_query(&username)),
NO_BODY,
"Could not initiate password reset",
)
@@ -189,7 +177,7 @@ impl HostService {
token: String,
) -> Result<lldap_auth::password_reset::ServerPasswordResetResponse> {
call_server_json_with_error_message(
&format!("{}/auth/reset/step2/{}", base_url(), token),
&format!("/auth/reset/step2/{}", token),
NO_BODY,
"Could not validate token",
)
@@ -197,13 +185,13 @@ impl HostService {
}
pub async fn probe_password_reset() -> Result<bool> {
Ok(gloo_net::http::Request::get(
&(base_url() + "/auth/reset/step1/lldap_unlikely_very_long_user_name"),
Ok(
gloo_net::http::Request::get("/auth/reset/step1/lldap_unlikely_very_long_user_name")
.header("Content-Type", "application/json")
.send()
.await?
.status()
!= http::StatusCode::NOT_FOUND,
)
.header("Content-Type", "application/json")
.send()
.await?
.status()
!= http::StatusCode::NOT_FOUND)
}
}

View File

@@ -22,11 +22,10 @@ pub fn set_cookie(cookie_name: &str, value: &str, expiration: &DateTime<Utc>) ->
.map_err(|_| anyhow!("Document is not an HTMLDocument"))
})?;
let cookie_string = format!(
"{}={}; expires={}; sameSite=Strict; path={}/",
"{}={}; expires={}; sameSite=Strict; path=/",
cookie_name,
value,
expiration.to_rfc2822(),
yew_router::utils::base_url().unwrap_or_default()
expiration.to_rfc2822()
);
doc.set_cookie(&cookie_string)
.map_err(|_| anyhow!("Could not set cookie"))

View File

@@ -1,38 +0,0 @@
use crate::infra::api::HostService;
use anyhow::Result;
use graphql_client::GraphQLQuery;
use wasm_bindgen_futures::spawn_local;
use yew::{use_effect, use_state, UseStateHandle};
// Enum to represent a result that is fetched asynchronously.
#[derive(Debug)]
pub enum LoadableResult<T> {
// The result is still being fetched
Loading,
// The async call is completed
Loaded(Result<T>),
}
pub fn use_graphql_call<QueryType>(
variables: QueryType::Variables,
) -> UseStateHandle<LoadableResult<QueryType::ResponseData>>
where
QueryType: GraphQLQuery + 'static,
{
let loadable_result: UseStateHandle<LoadableResult<QueryType::ResponseData>> =
use_state(|| LoadableResult::Loading);
{
let loadable_result = loadable_result.clone();
use_effect(move || {
let task = HostService::graphql_query::<QueryType>(variables, "Failed graphql query");
spawn_local(async move {
let response = task.await;
loadable_result.set(LoadableResult::Loaded(response));
});
|| ()
})
}
loadable_result.clone()
}

View File

@@ -1,7 +1,5 @@
pub mod api;
pub mod common_component;
pub mod cookies;
pub mod functional;
pub mod graphql;
pub mod modal;
pub mod schema;

View File

@@ -1,96 +0,0 @@
use anyhow::Result;
use std::{fmt::Display, str::FromStr};
use validator::ValidationError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AttributeType {
String,
Integer,
DateTime,
Jpeg,
}
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 AttributeType {
fn from(value: $source_type) -> Self {
match value {
<$source_type>::STRING => AttributeType::String,
<$source_type>::INTEGER => AttributeType::Integer,
<$source_type>::DATE_TIME => AttributeType::DateTime,
<$source_type>::JPEG_PHOTO => AttributeType::Jpeg,
_ => panic!("Unknown attribute type"),
}
}
}
impl From<AttributeType> for $source_type {
fn from(value: AttributeType) -> Self {
match value {
AttributeType::String => <$source_type>::STRING,
AttributeType::Integer => <$source_type>::INTEGER,
AttributeType::DateTime => <$source_type>::DATE_TIME,
AttributeType::Jpeg => <$source_type>::JPEG_PHOTO,
}
}
}
};
}
<<<<<<< HEAD
#[derive(Clone, PartialEq, Eq)]
pub struct Attribute {
pub name: String,
pub value: Vec<String>,
pub attribute_type: AttributeType,
pub is_list: bool,
pub is_editable: bool,
pub is_hardcoded: bool,
}
// Macro to generate traits for converting between AttributeType and the
// graphql generated equivalents.
#[macro_export]
macro_rules! combine_schema_and_values {
($schema_list:ident, $value_list:ident, $output_list:ident) => {
let set_attributes = value_list.clone();
let mut attribute_schema = schema_list.clone();
attribute_schema.retain(|schema| !schema.is_hardcoded);
let $output_list = attribute_schema.into_iter().map(|schema| {
Attribute {
name: schema.name.clone(),
value: set_attributes.iter().find(|attribute_value| attribute_value.name == schema.name).unwrap().value.clone(),
attribute_type: AttributeType::from(schema.attribute_type),
is_list: schema.is_list,
}
}).collect();
};
=======
pub fn validate_attribute_type(attribute_type: &str) -> Result<(), ValidationError> {
AttributeType::from_str(attribute_type)
.map_err(|_| ValidationError::new("Invalid attribute type"))?;
Ok(())
>>>>>>> 8f2391a (app: create group attribute schema page (#825))
}

View File

@@ -6,14 +6,13 @@ homepage = "https://github.com/lldap/lldap"
license = "GPL-3.0-only"
name = "lldap_auth"
repository = "https://github.com/lldap/lldap"
version = "0.4.0"
version = "0.3.0"
[features]
default = ["opaque_server", "opaque_client"]
opaque_server = []
opaque_client = []
js = []
sea_orm = ["dep:sea-orm"]
[dependencies]
rust-argon2 = "0.8"
@@ -21,27 +20,21 @@ curve25519-dalek = "3"
digest = "0.9"
generic-array = "0.14"
rand = "0.8"
serde = "*"
serde = "1"
sha2 = "0.9"
thiserror = "*"
thiserror = "1"
[dependencies.opaque-ke]
version = "0.6"
[dependencies.chrono]
version = "*"
version = "0.4"
features = [ "serde" ]
[dependencies.sea-orm]
version= "0.12"
default-features = false
features = ["macros"]
optional = true
# For WASM targets, use the JS getrandom.
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.getrandom]
version = "0.2"
features = ["js"]
[target.'cfg(target_arch = "wasm32")'.dependencies.getrandom]
version = "0.2"
features = ["js"]

View File

@@ -9,17 +9,17 @@ pub mod opaque;
/// The messages for the 3-step OPAQUE and simple login process.
pub mod login {
use super::{types::UserId, *};
use super::*;
#[derive(Serialize, Deserialize, Clone)]
pub struct ServerData {
pub username: UserId,
pub username: String,
pub server_login: opaque::server::login::ServerLogin,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct ClientLoginStartRequest {
pub username: UserId,
pub username: String,
pub login_start_request: opaque::server::login::CredentialRequest,
}
@@ -39,14 +39,14 @@ pub mod login {
#[derive(Serialize, Deserialize, Clone)]
pub struct ClientSimpleLoginRequest {
pub username: UserId,
pub username: String,
pub password: String,
}
impl fmt::Debug for ClientSimpleLoginRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ClientSimpleLoginRequest")
.field("username", &self.username.as_str())
.field("username", &self.username)
.field("password", &"***********")
.finish()
}
@@ -63,16 +63,16 @@ pub mod login {
/// The messages for the 3-step OPAQUE registration process.
/// It is used to reset a user's password.
pub mod registration {
use super::{types::UserId, *};
use super::*;
#[derive(Serialize, Deserialize, Clone)]
pub struct ServerData {
pub username: UserId,
pub username: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct ClientRegistrationStartRequest {
pub username: UserId,
pub username: String,
pub registration_start_request: opaque::server::registration::RegistrationRequest,
}
@@ -104,100 +104,6 @@ pub mod password_reset {
}
}
pub mod types {
use serde::{Deserialize, Serialize};
#[cfg(feature = "sea_orm")]
use sea_orm::{DbErr, DeriveValueType, QueryResult, TryFromU64, Value};
#[derive(
PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Default, Hash, Serialize, Deserialize,
)]
#[cfg_attr(feature = "sea_orm", derive(DeriveValueType))]
#[serde(from = "String")]
pub struct CaseInsensitiveString(String);
impl CaseInsensitiveString {
pub fn new(s: &str) -> Self {
Self(s.to_ascii_lowercase())
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn into_string(self) -> String {
self.0
}
}
impl From<String> for CaseInsensitiveString {
fn from(mut s: String) -> Self {
s.make_ascii_lowercase();
Self(s)
}
}
impl From<&String> for CaseInsensitiveString {
fn from(s: &String) -> Self {
Self::new(s.as_str())
}
}
impl From<&str> for CaseInsensitiveString {
fn from(s: &str) -> Self {
Self::new(s)
}
}
#[derive(
PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Default, Hash, Serialize, Deserialize,
)]
#[cfg_attr(feature = "sea_orm", derive(DeriveValueType))]
#[serde(from = "CaseInsensitiveString")]
pub struct UserId(CaseInsensitiveString);
impl UserId {
pub fn new(s: &str) -> Self {
s.into()
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn into_string(self) -> String {
self.0.into_string()
}
}
impl<T> From<T> for UserId
where
T: Into<CaseInsensitiveString>,
{
fn from(s: T) -> Self {
Self(s.into())
}
}
impl std::fmt::Display for UserId {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0.as_str())
}
}
#[cfg(feature = "sea_orm")]
impl From<&UserId> for Value {
fn from(user_id: &UserId) -> Self {
user_id.as_str().into()
}
}
#[cfg(feature = "sea_orm")]
impl TryFromU64 for UserId {
fn try_from_u64(_n: u64) -> Result<Self, DbErr> {
Err(DbErr::ConvertFromU64(
"UserId cannot be constructed from u64",
))
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct JWTClaims {
pub exp: DateTime<Utc>,

View File

@@ -1,4 +1,3 @@
use crate::types::UserId;
use opaque_ke::ciphersuite::CipherSuite;
use rand::{CryptoRng, RngCore};
@@ -78,10 +77,10 @@ pub mod client {
pub use opaque_ke::ClientRegistrationFinishParameters;
/// Initiate the registration negotiation.
pub fn start_registration<R: RngCore + CryptoRng>(
password: &[u8],
password: &str,
rng: &mut R,
) -> AuthenticationResult<ClientRegistrationStartResult> {
Ok(ClientRegistration::start(rng, password)?)
Ok(ClientRegistration::start(rng, password.as_bytes())?)
}
/// Finalize the registration negotiation.
@@ -146,12 +145,12 @@ pub mod server {
pub fn start_registration(
server_setup: &ServerSetup,
registration_request: RegistrationRequest,
username: &UserId,
username: &str,
) -> AuthenticationResult<ServerRegistrationStartResult> {
Ok(ServerRegistration::start(
server_setup,
registration_request,
username.as_str().as_bytes(),
username.as_bytes(),
)?)
}
@@ -179,14 +178,14 @@ pub mod server {
server_setup: &ServerSetup,
password_file: Option<ServerRegistration>,
credential_request: CredentialRequest,
username: &UserId,
username: &str,
) -> AuthenticationResult<ServerLoginStartResult> {
Ok(ServerLogin::start(
rng,
server_setup,
password_file,
credential_request,
username.as_str().as_bytes(),
username.as_bytes(),
ServerLoginStartParameters::default(),
)?)
}

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
CONFIG_FILE=/data/lldap_config.toml
if [ ! -f "$CONFIG_FILE" ]; then
echo "[entrypoint] Copying the default config to $CONFIG_FILE"
echo "[entrypoint] Edit this $CONFIG_FILE to configure LLDAP."
if cp /app/lldap_config.docker_template.toml $CONFIG_FILE; then
echo "Configuration copied successfully."
else
echo "Fail to copy configuration, check permission on /data or manually create one by copying from LLDAP repository"
exit 1
fi
fi
echo "> Starting lldap.."
echo ""
exec /app/lldap "$@"
exec "$@"

View File

@@ -20,7 +20,7 @@ LLDAP has a command that will connect to a target database and initialize the
schema. If running with docker, run the following command to use your active
instance (this has the benefit of ensuring your container has access):
```sh
```
docker exec -it <LLDAP container name> /app/lldap create_schema -d <Target database url>
```
@@ -34,7 +34,7 @@ databases (SQLite in this example) will give an error if LLDAP is in the middle
statements. There are various ways to do this, but a simple enough way is filtering a
whole database dump. This repo contains [a script](/scripts/sqlite_dump_commands.sh) to generate SQLite commands for creating an appropriate dump:
```sh
```
./sqlite_dump_commands.sh | sqlite3 /path/to/lldap/config/users.db > /path/to/dump.sql
```
@@ -49,22 +49,20 @@ a transaction in case one of the statements fail.
PostgreSQL uses a different hex string format. The command below should switch SQLite
format to PostgreSQL format, and wrap it all in a transaction:
```sh
```
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 '$aSELECT setval(pg_get_serial_sequence('\''groups'\'', '\''group_id'\''), COALESCE((SELECT MAX(group_id) FROM groups), 1));' \
-e '$aCOMMIT;' /path/to/dump.sql
```
### To MySQL
MySQL mostly cooperates, but it gets some errors if you don't escape the `groups` table. It also uses
backticks to escape table name instead of quotes. Run the
backticks to escape table name instead of quotes. Run the
following command to wrap all table names in backticks for good measure, and wrap the inserts in
a transaction:
```sh
```
sed -i -r -e 's/^INSERT INTO "?([a-zA-Z0-9_]+)"?/INSERT INTO `\1`/' \
-e '1s/^/START TRANSACTION;\n/' \
-e '$aCOMMIT;' \
@@ -76,7 +74,7 @@ sed -i -r -e 's/^INSERT INTO "?([a-zA-Z0-9_]+)"?/INSERT INTO `\1`/' \
While MariaDB is supposed to be identical to MySQL, it doesn't support timezone offsets on DATETIME
strings. Use the following command to remove those and perform the additional MySQL sanitization:
```sh
```
sed -i -r -e "s/([^']'[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{9})\+00:00'([^'])/\1'\2/g" \
-e 's/^INSERT INTO "?([a-zA-Z0-9_]+)"?/INSERT INTO `\1`/' \
-e '1s/^/START TRANSACTION;\n/' \
@@ -108,4 +106,4 @@ Modify your `database_url` in `lldap_config.toml` (or `LLDAP_DATABASE_URL` in th
to point to your new database (the same value used when generating schema). Restart
LLDAP and check the logs to ensure there were no errors.
#### More details/examples can be seen in the CI process [here](https://raw.githubusercontent.com/lldap/lldap/main/.github/workflows/docker-build-static.yml), look for the job `lldap-database-migration-test`
#### More details/examples can be seen in the CI process [here](https://raw.githubusercontent.com/nitnelave/lldap/main/.github/workflows/docker-build-static.yml), look for the job `lldap-database-migration-test`

View File

@@ -18,15 +18,6 @@ still supports basic RootDSE queries.
Anonymous bind is not supported.
## `lldap-cli`
There is a community-built CLI frontend,
[Zepmann/lldap-cli](https://github.com/Zepmann/lldap-cli), that supports all
(as of this writing) the operations possible. Getting information from the
server, creating users, adding them to groups, creating new custom attributes
and populating them, all of that is supported. It is currently the easiest way
to script the interaction with LLDAP.
## GraphQL
The best way to interact with LLDAP programmatically is via the GraphQL

View File

@@ -1,18 +0,0 @@
# Configuration for Apereo CAS Server
Replace `dc=example,dc=com` with your LLDAP configured domain, and hostname for your LLDAP server.
The `search-filter` provided here requires users to be members of the `cas_auth` group in LLDAP.
Configuration to use LDAP in e.g. `/etc/cas/config/standalone.yml`
```
cas:
authn:
ldap:
- base-dn: dc=example,dc=com
bind-credential: password
bind-dn: uid=admin,ou=people,dc=example,dc=com
ldap-url: ldap://ldap.example.com:3890
search-filter: (&(objectClass=person)(memberOf=uid=cas_auth,ou=groups,dc=example,dc=com))
```

View File

@@ -33,7 +33,7 @@ authentication_backend:
users_filter: "(&({username_attribute}={input})(objectClass=person))"
# Set this to ou=groups, because all groups are stored in this ou
additional_groups_dn: ou=groups
# The groups are not displayed in the UI, but this filter works.
# Only this filter is supported right now
groups_filter: "(member={dn})"
# The attribute holding the name of the group.
group_name_attribute: cn

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -1,254 +0,0 @@
# Bootstrapping lldap using [bootstrap.sh](bootstrap.sh) script
bootstrap.sh allows managing your lldap in a git-ops, declarative way using JSON config files.
The script can:
* create, update users
* set/update all lldap built-in user attributes
* add/remove users to/from corresponding groups
* set/update user avatar from file, link or from gravatar by user email
* set/update user password
* create groups
* delete redundant users and groups (when `DO_CLEANUP` env var is true)
* maintain the desired state described in JSON config files
![](bootstrap-example-log-1.jpeg)
## Required packages
> The script will automatically install the required packages for alpine and debian-based distributions
> when run by root, or you can install them by yourself.
- curl
- [jq](https://github.com/jqlang/jq)
- [jo](https://github.com/jpmens/jo)
## Environment variables
- `LLDAP_URL` or `LLDAP_URL_FILE` - URL to your lldap instance or path to file that contains URL (**MANDATORY**)
- `LLDAP_ADMIN_USERNAME` or `LLDAP_ADMIN_USERNAME_FILE` - admin username or path to file that contains username (**MANDATORY**)
- `LLDAP_ADMIN_PASSWORD` or `LLDAP_ADMIN_PASSWORD_FILE` - admin password or path to file that contains password (**MANDATORY**)
- `USER_CONFIGS_DIR` (default value: `/user-configs`) - directory where the user JSON configs could be found
- `GROUP_CONFIGS_DIR` (default value: `/group-configs`) - directory where the group JSON configs could be found
- `LLDAP_SET_PASSWORD_PATH` - path to the `lldap_set_password` utility (default value: `/app/lldap_set_password`)
- `DO_CLEANUP` (default value: `false`) - delete groups and users not specified in config files, also remove users from groups that they do not belong to
## Config files
There are two types of config files: [group](#group-config-file-example) and [user](#user-config-file-example) configs.
Each config file can be as one JSON file with nested JSON top-level values as several JSON files.
### Group config file example
Group configs are used to define groups that will be created by the script
Fields description:
* `name`: name of the group (**MANDATORY**)
```json
{
"name": "group-1"
}
{
"name": "group-2"
}
```
### User config file example
User config defines all the lldap user structures,
if the non-mandatory field is omitted, the script will clean this field in lldap as well.
Fields description:
* `id`: it's just username (**MANDATORY**)
* `email`: self-explanatory (**MANDATORY**)
* `password`: would be used to set the password using `lldap_set_password` utility
* `displayName`: self-explanatory
* `firstName`: self-explanatory
* `lastName`: self-explanatory
* `avatar_file`: must be a valid path to jpeg file (ignored if `avatar_url` specified)
* `avatar_url`: must be a valid URL to jpeg file (ignored if `gravatar_avatar` specified)
* `gravatar_avatar` (`false` by default): the script will try to get an avatar from [gravatar](https://gravatar.com/) by previously specified `email` (has the highest priority)
* `weserv_avatar` (`false` by default): avatar file from `avatar_url` or `gravatar_avatar` would be converted to jpeg using [wsrv.nl](https://wsrv.nl) (useful when your avatar is png)
* `groups`: an array of groups the user would be a member of (all the groups must be specified in group config files)
```json
{
"id": "username",
"email": "username@example.com",
"password": "changeme",
"displayName": "Display Name",
"firstName": "First",
"lastName": "Last",
"avatar_file": "/path/to/avatar.jpg",
"avatar_url": "https://i.imgur.com/nbCxk3z.jpg",
"gravatar_avatar": "false",
"weserv_avatar": "false",
"groups": [
"group-1",
"group-2"
]
}
```
## Usage example
### Manually
The script can be run manually in the terminal for initial bootstrapping of your lldap instance.
You should make sure that the [required packages](#required-packages) are installed
and the [environment variables](#environment-variables) are configured properly.
```bash
export LLDAP_URL=http://localhost:8080
export LLDAP_ADMIN_USERNAME=admin
export LLDAP_ADMIN_PASSWORD=changeme
export USER_CONFIGS_DIR="$(realpath ./configs/user)"
export GROUP_CONFIGS_DIR="$(realpath ./configs/group)"
export LLDAP_SET_PASSWORD_PATH="$(realpath ./lldap_set_password)"
export DO_CLEANUP=false
./bootstrap.sh
```
### Docker compose
Let's suppose you have the next file structure:
```text
./
├─ docker-compose.yaml
└─ bootstrap
├─ bootstrap.sh
└─ user-configs
│ ├─ user-1.json
│ ├─ ...
│ └─ user-n.json
└─ group-configs
├─ group-1.json
├─ ...
└─ group-n.json
```
You should mount `bootstrap` dir to lldap container and set the corresponding `env` variables:
```yaml
version: "3"
services:
lldap:
image: lldap/lldap:v0.5.0
volumes:
- ./bootstrap:/bootstrap
ports:
- "3890:3890" # For LDAP
- "17170:17170" # For the web front-end
environment:
# envs required for lldap
- LLDAP_LDAP_USER_EMAIL=admin@example.com
- LLDAP_LDAP_USER_PASS=changeme
- LLDAP_LDAP_BASE_DN=dc=example,dc=com
# envs required for bootstrap.sh
- LLDAP_URL=http://localhost:17170
- LLDAP_ADMIN_USERNAME=admin
- LLDAP_ADMIN_PASSWORD=changeme # same as LLDAP_LDAP_USER_PASS
- USER_CONFIGS_DIR=/bootstrap/user-configs
- GROUP_CONFIGS_DIR=/bootstrap/group-configs
- DO_CLEANUP=false
```
Then, to bootstrap your lldap just run `docker compose exec lldap /bootstrap/bootstrap.sh`.
If config files were changed, re-run the `bootstrap.sh` with the same command.
### Kubernetes job
```yaml
apiVersion: batch/v1
kind: Job
metadata:
name: lldap-bootstrap
# Next annotations are required if the job managed by Argo CD,
# so Argo CD can relaunch the job on every app sync action
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: lldap-bootstrap
image: lldap/lldap:v0.5.0
command:
- /bootstrap/bootstrap.sh
env:
- name: LLDAP_URL
value: "http://lldap:8080"
- name: LLDAP_ADMIN_USERNAME
valueFrom: { secretKeyRef: { name: lldap-admin-user, key: username } }
- name: LLDAP_ADMIN_PASSWORD
valueFrom: { secretKeyRef: { name: lldap-admin-user, key: password } }
- name: DO_CLEANUP
value: "true"
volumeMounts:
- name: bootstrap
mountPath: /bootstrap/bootstrap.sh
subPath: bootstrap.sh
- name: user-configs
mountPath: /user-configs
readOnly: true
- name: group-configs
mountPath: /group-configs
readOnly: true
volumes:
- name: bootstrap
configMap:
name: bootstrap
defaultMode: 0555
items:
- key: bootstrap.sh
path: bootstrap.sh
- name: user-configs
projected:
sources:
- secret:
name: lldap-admin-user
items:
- key: user-config.json
path: admin-config.json
- secret:
name: lldap-password-manager-user
items:
- key: user-config.json
path: password-manager-config.json
- secret:
name: lldap-bootstrap-configs
items:
- key: user-configs.json
path: user-configs.json
- name: group-configs
projected:
sources:
- secret:
name: lldap-bootstrap-configs
items:
- key: group-configs.json
path: group-configs.json
```

View File

@@ -1,490 +0,0 @@
#!/usr/bin/env bash
set -e
set -o pipefail
LLDAP_URL="${LLDAP_URL}"
LLDAP_ADMIN_USERNAME="${LLDAP_ADMIN_USERNAME}"
LLDAP_ADMIN_PASSWORD="${LLDAP_ADMIN_PASSWORD}"
USER_CONFIGS_DIR="${USER_CONFIGS_DIR:-/user-configs}"
GROUP_CONFIGS_DIR="${GROUP_CONFIGS_DIR:-/group-configs}"
LLDAP_SET_PASSWORD_PATH="${LLDAP_SET_PASSWORD_PATH:-/app/lldap_set_password}"
DO_CLEANUP="${DO_CLEANUP:-false}"
check_install_dependencies() {
local commands=('curl' 'jq' 'jo')
local commands_not_found='false'
if ! hash "${commands[@]}" 2>/dev/null; then
if hash 'apk' 2>/dev/null && [[ $EUID -eq 0 ]]; then
apk add "${commands[@]}"
elif hash 'apt' 2>/dev/null && [[ $EUID -eq 0 ]]; then
apt update -yqq
apt install -yqq "${commands[@]}"
else
local command=''
for command in "${commands[@]}"; do
if ! hash "$command" 2>/dev/null; then
printf 'Command not found "%s"\n' "$command"
fi
done
commands_not_found='true'
fi
fi
if [[ "$commands_not_found" == 'true' ]]; then
return 1
fi
}
check_required_env_vars() {
local env_var_not_specified='false'
local dual_env_vars_list=(
'LLDAP_URL'
'LLDAP_ADMIN_USERNAME'
'LLDAP_ADMIN_PASSWORD'
)
local dual_env_var_name=''
for dual_env_var_name in "${dual_env_vars_list[@]}"; do
local dual_env_var_file_name="${dual_env_var_name}_FILE"
if [[ -z "${!dual_env_var_name}" ]] && [[ -z "${!dual_env_var_file_name}" ]]; then
printf 'Please specify "%s" or "%s" variable!\n' "$dual_env_var_name" "$dual_env_var_file_name" >&2
env_var_not_specified='true'
else
if [[ -n "${!dual_env_var_file_name}" ]]; then
declare -g "$dual_env_var_name"="$(cat "${!dual_env_var_file_name}")"
fi
fi
done
if [[ "$env_var_not_specified" == 'true' ]]; then
return 1
fi
}
check_configs_validity() {
local config_file='' config_invalid='false'
for config_file in "$@"; do
local error=''
if ! error="$(jq '.' -- "$config_file" 2>&1 >/dev/null)"; then
printf '%s: %s\n' "$config_file" "$error"
config_invalid='true'
fi
done
if [[ "$config_invalid" == 'true' ]]; then
return 1
fi
}
auth() {
local url="$1" admin_username="$2" admin_password="$3"
local response
response="$(curl --silent --request POST \
--url "$url/auth/simple/login" \
--header 'Content-Type: application/json' \
--data "$(jo -- username="$admin_username" password="$admin_password")")"
TOKEN="$(printf '%s' "$response" | jq --raw-output .token)"
}
make_query() {
local query_file="$1" variables_file="$2"
curl --silent --request POST \
--url "$LLDAP_URL/api/graphql" \
--header "Authorization: Bearer $TOKEN" \
--header 'Content-Type: application/json' \
--data @<(jq --slurpfile variables "$variables_file" '. + {"variables": $variables[0]}' "$query_file")
}
get_group_list() {
local query='{"query":"query GetGroupList {groups {id displayName}}","operationName":"GetGroupList"}'
make_query <(printf '%s' "$query") <(printf '{}')
}
get_group_array() {
get_group_list | jq --raw-output '.data.groups[].displayName'
}
group_exists() {
if [[ "$(get_group_list | jq --raw-output --arg displayName "$1" '.data.groups | any(.[]; select(.displayName == $displayName))')" == 'true' ]]; then
return 0
else
return 1
fi
}
get_group_id() {
get_group_list | jq --raw-output --arg displayName "$1" '.data.groups[] | if .displayName == $displayName then .id else empty end'
}
create_group() {
local group_name="$1"
if group_exists "$group_name"; then
printf 'Group "%s" (%s) already exists\n' "$group_name" "$(get_group_id "$group_name")"
return
fi
# shellcheck disable=SC2016
local query='{"query":"mutation CreateGroup($name: String!) {createGroup(name: $name) {id displayName}}","operationName":"CreateGroup"}'
local response='' error=''
response="$(make_query <(printf '%s' "$query") <(jo -- name="$group_name"))"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'Group "%s" (%s) successfully created\n' "$group_name" "$(printf '%s' "$response" | jq --raw-output '.data.createGroup.id')"
fi
}
delete_group() {
local group_name="$1" id=''
if ! group_exists "$group_name"; then
printf '[WARNING] Group "%s" does not exist\n' "$group_name"
return
fi
id="$(get_group_id "$group_name")"
# shellcheck disable=SC2016
local query='{"query":"mutation DeleteGroupQuery($groupId: Int!) {deleteGroup(groupId: $groupId) {ok}}","operationName":"DeleteGroupQuery"}'
local response='' error=''
response="$(make_query <(printf '%s' "$query") <(jo -- groupId="$id"))"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'Group "%s" (%s) successfully deleted\n' "$group_name" "$id"
fi
}
get_user_details() {
local id="$1"
# shellcheck disable=SC2016
local query='{"query":"query GetUserDetails($id: String!) {user(userId: $id) {id email displayName firstName lastName creationDate uuid groups {id displayName}}}","operationName":"GetUserDetails"}'
make_query <(printf '%s' "$query") <(jo -- id="$id")
}
user_in_group() {
local user_id="$1" group_name="$2"
if ! group_exists "$group_name"; then
printf '[WARNING] Group "%s" does not exist\n' "$group_name"
return
fi
if ! user_exists "$user_id"; then
printf 'User "%s" is not exists\n' "$user_id"
return
fi
if [[ "$(get_user_details "$user_id" | jq --raw-output --arg displayName "$group_name" '.data.user.groups | any(.[]; select(.displayName == $displayName))')" == 'true' ]]; then
return 0
else
return 1
fi
}
add_user_to_group() {
local user_id="$1" group_name="$2" group_id=''
if ! group_exists "$group_name"; then
printf '[WARNING] Group "%s" does not exist\n' "$group_name"
return
fi
group_id="$(get_group_id "$group_name")"
if user_in_group "$user_id" "$group_name"; then
printf 'User "%s" already in group "%s" (%s)\n' "$user_id" "$group_name" "$group_id"
return
fi
# shellcheck disable=SC2016
local query='{"query":"mutation AddUserToGroup($user: String!, $group: Int!) {addUserToGroup(userId: $user, groupId: $group) {ok}}","operationName":"AddUserToGroup"}'
local response='' error=''
response="$(make_query <(printf '%s' "$query") <(jo -- user="$user_id" group="$group_id"))"
error="$(printf '%s' "$response" | jq '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'User "%s" successfully added to the group "%s" (%s)\n' "$user_id" "$group_name" "$group_id"
fi
}
remove_user_from_group() {
local user_id="$1" group_name="$2" group_id=''
if ! group_exists "$group_name"; then
printf '[WARNING] Group "%s" does not exist\n' "$group_name"
return
fi
group_id="$(get_group_id "$group_name")"
# shellcheck disable=SC2016
local query='{"operationName":"RemoveUserFromGroup","query":"mutation RemoveUserFromGroup($user: String!, $group: Int!) {removeUserFromGroup(userId: $user, groupId: $group) {ok}}"}'
local response='' error=''
response="$(make_query <(printf '%s' "$query") <(jo -- user="$user_id" group="$group_id"))"
error="$(printf '%s' "$response" | jq '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'User "%s" successfully removed from the group "%s" (%s)\n' "$user_id" "$group_name" "$group_id"
fi
}
get_users_list() {
# shellcheck disable=SC2016
local query='{"query": "query ListUsersQuery($filters: RequestFilter) {users(filters: $filters) {id email displayName firstName lastName creationDate}}","operationName": "ListUsersQuery"}'
make_query <(printf '%s' "$query") <(jo -- filters=null)
}
user_exists() {
if [[ "$(get_users_list | jq --raw-output --arg id "$1" '.data.users | any(.[]; contains({"id": $id}))')" == 'true' ]]; then
return 0
else
return 1
fi
}
delete_user() {
local id="$1"
if ! user_exists "$id"; then
printf 'User "%s" is not exists\n' "$id"
return
fi
# shellcheck disable=SC2016
local query='{"query": "mutation DeleteUserQuery($user: String!) {deleteUser(userId: $user) {ok}}","operationName": "DeleteUserQuery"}'
local response='' error=''
response="$(make_query <(printf '%s' "$query") <(jo -- user="$id"))"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'User "%s" successfully deleted\n' "$id"
fi
}
__common_user_mutation_query() {
local \
query="$1" \
id="${2:-null}" \
email="${3:-null}" \
displayName="${4:-null}" \
firstName="${5:-null}" \
lastName="${6:-null}" \
avatar_file="${7:-null}" \
avatar_url="${8:-null}" \
gravatar_avatar="${9:-false}" \
weserv_avatar="${10:-false}"
local variables_arr=(
'-s' "id=$id"
'-s' "email=$email"
'-s' "displayName=$displayName"
'-s' "firstName=$firstName"
'-s' "lastName=$lastName"
)
local temp_avatar_file=''
if [[ "$gravatar_avatar" == 'true' ]]; then
avatar_url="https://gravatar.com/avatar/$(printf '%s' "$email" | sha256sum | cut -d ' ' -f 1)?size=512"
fi
if [[ "$avatar_url" != 'null' ]]; then
temp_avatar_file="${TMP_AVATAR_DIR}/$(printf '%s' "$avatar_url" | md5sum | cut -d ' ' -f 1)"
if ! [[ -f "$temp_avatar_file" ]]; then
if [[ "$weserv_avatar" == 'true' ]]; then
avatar_url="https://wsrv.nl/?url=$avatar_url&output=jpg"
fi
curl --silent --location --output "$temp_avatar_file" "$avatar_url"
fi
avatar_file="$temp_avatar_file"
fi
if [[ "$avatar_file" == 'null' ]]; then
variables_arr+=('-s' 'avatar=null')
else
variables_arr+=("avatar=%$avatar_file")
fi
make_query <(printf '%s' "$query") <(jo -- user=:<(jo -- "${variables_arr[@]}"))
}
create_user() {
local id="$1"
if user_exists "$id"; then
printf 'User "%s" already exists\n' "$id"
return
fi
# shellcheck disable=SC2016
local query='{"query":"mutation CreateUser($user: CreateUserInput!) {createUser(user: $user) {id creationDate}}","operationName":"CreateUser"}'
local response='' error=''
response="$(__common_user_mutation_query "$query" "$@")"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'User "%s" successfully created\n' "$id"
fi
}
update_user() {
local id="$1"
if ! user_exists "$id"; then
printf 'User "%s" is not exists\n' "$id"
return
fi
# shellcheck disable=SC2016
local query='{"query":"mutation UpdateUser($user: UpdateUserInput!) {updateUser(user: $user) {ok}}","operationName":"UpdateUser"}'
local response='' error=''
response="$(__common_user_mutation_query "$query" "$@")"
error="$(printf '%s' "$response" | jq --raw-output '.errors | if . != null then .[].message else empty end')"
if [[ -n "$error" ]]; then
printf '%s\n' "$error"
else
printf 'User "%s" successfully updated\n' "$id"
fi
}
create_update_user() {
local id="$1"
if user_exists "$id"; then
update_user "$@"
else
create_user "$@"
fi
}
main() {
check_install_dependencies
check_required_env_vars
local user_config_files=("${USER_CONFIGS_DIR}"/*.json)
local group_config_files=("${GROUP_CONFIGS_DIR}"/*.json)
if ! check_configs_validity "${group_config_files[@]}" "${user_config_files[@]}"; then
exit 1
fi
until curl --silent -o /dev/null "$LLDAP_URL"; do
printf 'Waiting lldap to start...\n'
sleep 10
done
auth "$LLDAP_URL" "$LLDAP_ADMIN_USERNAME" "$LLDAP_ADMIN_PASSWORD"
local redundant_groups=''
redundant_groups="$(get_group_list | jq '[ .data.groups[].displayName ]' | jq --compact-output '. - ["lldap_admin","lldap_password_manager","lldap_strict_readonly"]')"
printf -- '\n--- groups ---\n'
local group_config=''
while read -r group_config; do
local group_name=''
group_name="$(printf '%s' "$group_config" | jq --raw-output '.name')"
create_group "$group_name"
redundant_groups="$(printf '%s' "$redundant_groups" | jq --compact-output --arg name "$group_name" '. - [$name]')"
done < <(jq --compact-output '.' -- "${group_config_files[@]}")
printf -- '--- groups ---\n'
printf -- '\n--- redundant groups ---\n'
if [[ "$redundant_groups" == '[]' ]]; then
printf 'There are no redundant groups\n'
else
local group_name=''
while read -r group_name; do
if [[ "$DO_CLEANUP" == 'true' ]]; then
delete_group "$group_name"
else
printf '[WARNING] Group "%s" is not declared in config files\n' "$group_name"
fi
done < <(printf '%s' "$redundant_groups" | jq --raw-output '.[]')
fi
printf -- '--- redundant groups ---\n'
local redundant_users=''
redundant_users="$(get_users_list | jq '[ .data.users[].id ]' | jq --compact-output --arg admin_id "$LLDAP_ADMIN_USERNAME" '. - [$admin_id]')"
TMP_AVATAR_DIR="$(mktemp -d)"
local user_config=''
while read -r user_config; do
local field='' id='' email='' displayName='' firstName='' lastName='' avatar_file='' avatar_url='' gravatar_avatar='' weserv_avatar='' password=''
for field in 'id' 'email' 'displayName' 'firstName' 'lastName' 'avatar_file' 'avatar_url' 'gravatar_avatar' 'weserv_avatar' 'password'; do
declare "$field"="$(printf '%s' "$user_config" | jq --raw-output --arg field "$field" '.[$field]')"
done
printf -- '\n--- %s ---\n' "$id"
create_update_user "$id" "$email" "$displayName" "$firstName" "$lastName" "$avatar_file" "$avatar_url" "$gravatar_avatar" "$weserv_avatar"
redundant_users="$(printf '%s' "$redundant_users" | jq --compact-output --arg id "$id" '. - [$id]')"
if [[ "$password" != 'null' ]] && [[ "$password" != '""' ]]; then
"$LLDAP_SET_PASSWORD_PATH" --base-url "$LLDAP_URL" --token "$TOKEN" --username "$id" --password "$password"
fi
local redundant_user_groups=''
redundant_user_groups="$(get_user_details "$id" | jq '[ .data.user.groups[].displayName ]')"
local group=''
while read -r group; do
if [[ -n "$group" ]]; then
add_user_to_group "$id" "$group"
redundant_user_groups="$(printf '%s' "$redundant_user_groups" | jq --compact-output --arg group "$group" '. - [$group]')"
fi
done < <(printf '%s' "$user_config" | jq --raw-output '.groups | if . == null then "" else .[] end')
local user_group_name=''
while read -r user_group_name; do
if [[ "$DO_CLEANUP" == 'true' ]]; then
remove_user_from_group "$id" "$user_group_name"
else
printf '[WARNING] User "%s" is not declared as member of the "%s" group in the config files\n' "$id" "$user_group_name"
fi
done < <(printf '%s' "$redundant_user_groups" | jq --raw-output '.[]')
printf -- '--- %s ---\n' "$id"
done < <(jq --compact-output '.' -- "${user_config_files[@]}")
rm -r "$TMP_AVATAR_DIR"
printf -- '\n--- redundant users ---\n'
if [[ "$redundant_users" == '[]' ]]; then
printf 'There are no redundant users\n'
else
local id=''
while read -r id; do
if [[ "$DO_CLEANUP" == 'true' ]]; then
delete_user "$id"
else
printf '[WARNING] User "%s" is not declared in config files\n' "$id"
fi
done < <(printf '%s' "$redundant_users" | jq --raw-output '.[]')
fi
printf -- '--- redundant users ---\n'
}
main "$@"

View File

@@ -6,12 +6,11 @@ LDAP configuration is in ```/dokuwiki/conf/local.protected.php```:
<?php
$conf['useacl'] = 1; //enable ACL
$conf['authtype'] = 'authldap'; //enable this Auth plugin
$conf['superuser'] = 'admin';
$conf['plugin']['authldap']['server'] = 'ldap://lldap_server:3890'; #IP of your lldap
$conf['plugin']['authldap']['usertree'] = 'ou=people,dc=example,dc=com';
$conf['plugin']['authldap']['grouptree'] = 'ou=groups, dc=example, dc=com';
$conf['plugin']['authldap']['userfilter'] = '(&(uid=%{user})(objectClass=person))';
$conf['plugin']['authldap']['groupfilter'] = '(&(member=%{dn})(objectClass=groupOfUniqueNames))';
$conf['plugin']['authldap']['groupfilter'] = '(objectClass=group)';
$conf['plugin']['authldap']['attributes'] = array('cn', 'displayname', 'mail', 'givenname', 'objectclass', 'sn', 'uid', 'memberof');
$conf['plugin']['authldap']['version'] = 3;
$conf['plugin']['authldap']['binddn'] = 'cn=admin,ou=people,dc=example,dc=com';

View File

@@ -1,22 +0,0 @@
# Basic LDAP auth for an Ergo IRC server
[Main documentation here.](https://github.com/ergochat/ergo-ldap)
For simple user auth prepare a ldap-config.yaml with the following settings
```
host: "127.0.0.1"
port: 3890
timeout: 30s
# uncomment for TLS / LDAPS:
# use-ssl: true
bind-dn: "uid=%s,ou=people,dc=example,dc=org"
```
Then add the compiled ergo-ldap program to your Ergo folder and make sure it can be executed by the same user your Ergo IRCd runs as.
Follow the instructions in the main Ergo config file's accounts section on how to execute an external auth program.
Make sure SASL auth is enabled and then restart Ergo to enable LDAP linked SASL auth.

View File

@@ -1,30 +0,0 @@
# GitLab Configuration
Members of the group ``git_user`` will have access to GitLab.
Edit ``/etc/gitlab/gitlab.rb``:
```ruby
gitlab_rails['ldap_enabled'] = true
gitlab_rails['ldap_servers'] = {
'main' => {
'label' => 'LDAP',
'host' => 'ldap.example.com',
'port' => 3890,
'uid' => 'uid',
'base' => 'ou=people,dc=example,dc=com',
'encryption' => 'plain',
'bind_dn' => 'uid=bind_user,ou=people,dc=example,dc=com',
'password' => '<bind user password>',
'active_directory' => false,
'user_filter' => '(&(objectclass=person)(memberof=cn=git_user,ou=groups,dc=example,dc=com))',
'attributes' => {
'username' => 'uid',
'email' => 'mail',
'name' => 'displayName',
'first_name' => 'givenName',
'last_name' => 'sn'
}
}
}
```

View File

@@ -37,7 +37,7 @@ search_base_dns = ["dc=example,dc=org"]
[servers.attributes]
member_of = "memberOf"
email = "mail"
name = "displayName"
name = "givenName"
surname = "sn"
username = "uid"

View File

@@ -1,28 +0,0 @@
# Configuration for Grocy
Adjust the following values in the file `config/data/config.php` or add environment variables for them (prefixed with `GROCY_`).
NOTE: If the environment variables are not working (for example in the linuxserver.io Docker Image), you need to add `clear_env = no` under the `[www]` in `/config/php/www2.conf`.
Replace `dc=example,dc=com` with your LLDAP configured domain.
### AUTH_CLASS
Needs to be set to `Grocy\Middleware\LdapAuthMiddleware` in order to use LDAP
### LDAP_ADDRESS
The address of your ldap server, eg: `ldap://lldap.example.com:389`
### LDAP_BASE_DN
The base dn, usually points directly to the `people`, eg: `ou=people,dc=example,dc=com`
### LDAP_BIND_DN
The reader user for lldap, eg: `uid=ldap-reader,ou=people,dc=example,dc=com`
### LDAP_BIND_PW
The password for the reader user
### LDAP_USER_FILTER
The filter to use for the users, eg. for a separate group: `(&(objectClass=person)(memberof=cn=grocy_users,ou=groups,dc=example,dc=com))`
### LDAP_UID_ATTR
The user id attribute, should be `uid`

View File

@@ -6,8 +6,8 @@ Home Assistant configures ldap auth via the [Command Line Auth Provider](https:/
The [auth script](lldap-ha-auth.sh) attempts to authenticate a user against an LLDAP server, using credentials provided via `username` and `password` environment variables. The first argument must be the URL of your LLDAP server, accessible from Home Assistant. You can provide an additional optional argument to confine allowed logins to a single group. The script will output the user's display name as the `name` variable, if not empty.
1. Copy the [auth script](lldap-ha-auth.sh) to your home assistant instance. In this example, we use `/config/lldap-ha-auth.sh`.
- Set the script as executable by running `chmod +x /config/lldap-ha-auth.sh`
1. Copy the [auth script](lldap-ha-auth.sh) to your home assistant instance. In this example, we use `/config/lldap-auth.sh`.
- Set the script as executable by running `chmod +x /config/lldap-auth-sh`
2. Add the following to your configuration.yaml in Home assistant:
```yaml
homeassistant:
@@ -15,21 +15,10 @@ homeassistant:
# Ensure you have the homeassistant provider enabled if you want to continue using your existing accounts
- type: homeassistant
- type: command_line
command: /config/lldap-ha-auth.sh
# arguments: [<LDAP Host>, <regular user group>, <admin user group>, <local user group>]
# <regular user group>: Find users that has permission to access homeassistant, anyone inside
# this group will have the default 'system-users' permission in homeassistant.
#
# <admin user group>: Allow users in the <regular user group> to be assigned into 'system-admin' group.
# Anyone inside this group will not have the 'system-users' permission as only one permission group
# is allowed in homeassistant
#
# <local user group>: Users in the <local user group> (e.g., 'homeassistant_local') can only access
# homeassistant inside LAN network.
#
# Only the first argument is required. ["https://lldap.example.com"] allows all users to log in from
# anywhere and have 'system-users' permissions.
args: ["https://lldap.example.com", "homeassistant_user", "homeassistant_admin", "homeassistant_local"]
command: /config/lldap-auth.sh
# Only allow users in the 'homeassistant_user' group to login.
# Change to ["https://lldap.example.com"] to allow all users
args: ["https://lldap.example.com", "homeassistant_user"]
meta: true
```
3. Reload your config or restart Home Assistant

View File

@@ -37,9 +37,9 @@ Otherwise, just use:
```
### Admin Base DN
The DN to search for your admins.
The DN of your admin group. If you have `media_admin` as your group you would use:
```
ou=people,dc=example,dc=com
cn=media_admin,ou=groups,dc=example,dc=com
```
### Admin Filter
@@ -49,15 +49,8 @@ that), use:
```
(memberof=cn=media_admin,ou=groups,dc=example,dc=com)
```
Bear in mind that admins must also be a member of the users group if you use one.
Otherwise, you can use LLDAP's admin group:
```
(memberof=cn=lldap_admin,ou=groups,dc=example,dc=com)
```
## Password change
To allow changing Passwords via Jellyfin the following things are required
- The bind user needs to have the group lldap_password_manager (changing passwords of members of the group lldap_admin does not work to prevent privilege escalation)
- Check `Allow Password Change`
- `LDAP Password Attribute` Needs to be set to `userPassword`

View File

@@ -1,81 +0,0 @@
# Configuration for Jenkins
## Jenkins base setup
To setup LLDAP for Jenkins navigate to Dashboard/Manage Jenkins/Security.
*Note: Jenkins LDAP plugin has to be installed!</br>*
*Note: "dc=example,dc=com" is default configuration, you should replace it with your base DN.*
1) Set **Security Realm** to **LDAP**
2) Click Add Server
3) Setup config fields as stated below
## Config fields
#### Server
*(This can be replaced by server ip/your domain etc.)*
```
ldap://example.com:3890
```
### Advanced Server Configuration Dropdown
#### root DN
```
dc=example,dc=com
```
#### Allow blank rootDN
```
true
```
#### User search base
```
ou=people
```
#### User search filter
```
uid={0}
```
#### Group search base
```
ou=groups
```
#### Group search filter
```
(& (cn={0})(objectclass=groupOfNames))
```
#### Group membership
Select Search for LDAP groups containing user and leave Group membership filter empty
#### Manager DN
Leave here your admin account
```
cn=admin,ou=people,dc=example,dc=com
```
#### Manager Password
Leave it as is
#### Display Name LDAP attribute
Leave cn as it inputs username
```
cn
```
#### Email Address LDAP attribute
```
mail
```
### Tips & Tricks
- Always use Test LDAP settings so you won't get locked out. It works without password.
- If you want to setup your permissions, go to Authorization setting and select Matrix-based security. Add group/user (it has to exist in LLDAP) and you can grant him permissions. Note that Overall Read forbids users to read jenkins and execute actions. Administer gives full rights.
### Useful links:
https://plugins.jenkins.io/ldap/</br>
https://www.jenkins.io/doc/book/security/managing-security/

View File

@@ -1,19 +0,0 @@
# Configuration for Kasm
In Kasm, go to *Admin* -> *Authentication* -> *LDAP* and add a configuration.
- *Name*: whatever you like
- *Url* is your lldap host (or IP) and port, e.g. `ldap://lldap.example.com:3890`
- *Search Base* is is your base dn, e.g `dc=example,dc=com`
- *Search Filter* is `(&(objectClass=person)(uid={0})(memberof=cn=kasm,ou=groups,dc=example,dc=com))`. Replace `cn=kasm,ou=groups,dc=example,dc=com` with the dn to the group necessary to login to Kasm.
- *Group Membership Filter* `(&(objectClass=groupOfUniqueNames)(member={0}))`
- *Email attribute* `mail`
- *Service Account DN* a lldap user, preferably not a admin but a member of the group `lldap_strict readonly`. Mine is called `cn=query,ou=people,dc=example,dc=com`
- *Service Account Password*: querys password
- Activate *Search Subtree*, *Auto Create App User* and *Enabled*
- under *Attribute Mapping* you can map the following:
- *Email* -> `mail`
- *First Name* -> `givenname`
- *Last Name* -> `sn`
- If you want to map groups from your lldap to Kasm, edit the group, scroll to *SSO Group Mappings* and add a new SSO mapping:
- select your lldap as provider
- *Group Attributes* is the full DN of your group, e.g. `cn=kasm_moreaccess,ou=groups,dc=example,dc=com`

View File

@@ -62,11 +62,3 @@ Once the groups are synchronized, go to "Manage > Groups" on the left. Click on
Assign the role "admin" to the group. Now you can log in as the LLDAP admin to
the KeyCloak admin console.
## Fixing duplicate names or missing First Names for users
Since Keycloak and LLDAP use different attributes for different parts of a user's name, you may see duplicated or missing names for users in Keycloak. To fix this, update the attribute mappings:
Go back to "User Federation", edit your LDAP integration and click on the "Mappers" tab.
Find or create the "first name" mapper (it should have type `user-attribute-ldap-mapper`) and ensure the "LDAP Attribute" setting is set to `givenname`. Keycloak may have defaulted to `cn` which LLDAP uses for the "Display Name" of a user.

View File

@@ -1,193 +0,0 @@
# Configuration for LibreNMS
You can either configure LibreNMS from the webui or from the command line. This is a list of the variables that you should set.
## Essential
## auth_ldap_uid_attribute
```
uid
```
This sets 'uid' as the unique ldap attribute for users.
## auth_ldap_groupmemberattr
```
member
```
## auth_ldap_groups
```'
{"nms_admin": {"level": 10}}'
```
or
```
auth_ldap_groups.nms_admin.level: 10
```
These are both the same.
This example sets the group nms_admin as Admin (level 10).
Set others to match more groups at different levels.
## auth_ldap_starttls
```
false
```
## auth_ldap_server
```
[lldap server ip]
```
## auth_ldap_port
```
3890
```
## auth_ldap_suffix
```
,ou=people,dc=example,dc=com
```
Not sure if the case of people actually matters.
Make sure you keep the initial comma.
## auth_ldap_groupbase
```
ou=groups,dc=example,dc=com
```
## auth_mechanism
```
ldap
```
Be careful with this as you will lock yourself out if ldap does not work correctly. Set back to 'mysql' to turn ldap off.
### auth_ldap_require_groupmembership
```
false
```
## Testing
Use the test script to make sure it works.
```
./script/auth_test.php -u <user>
```
Make sure the level is correctly populated. Should look like this:
```
librenms:/opt/librenms# ./scripts/auth_test.php -uadmin
Authentication Method: ldap
Password:
Authenticate user admin:
AUTH SUCCESS
User (admin):
username => admin
realname => Administrator
user_id => admin
email => admin@example.com
level => 10
Groups: cn=nms_admin,ou=groups,dc=example,dc=com
```
## Setting variables
### Web UI
You can set all the varibles in the web UI in: Settings -> Authentication -> LDAP Settings
### Command line
You can use the lnms command to *get* config options like this:
```
lnms config:get auth_ldap_uid_attribute
```
You can use the lnms command to *set* config options like this:
```
lnms config:set auth_ldap_uid_attribute uid
```
Read more [here](https://docs.librenms.org/Support/Configuration/)
### Pre load configuration for Docker
You can create a file named: /data/config/ldap.yaml and place your variables in there.
```
librenms:/opt/librenms# cat /data/config/auth.yaml
auth_mechanism: ldap
auth_ldap_server: 172.17.0.1
auth_ldap_port: 3890
auth_ldap_version: 3
auth_ldap_suffix: ,ou=people,dc=example,dc=com
auth_ldap_groupbase: ou=groups,dc=example,dc=com
auth_ldap_prefix: uid=
auth_ldap_starttls: False
auth_ldap_attr: {"uid": "uid"}
auth_ldap_uid_attribute: uid
auth_ldap_groups: {"nms_admin": {"level": 10}}
auth_ldap_groupmemberattr: member
auth_ldap_require_groupmembership: False
auth_ldap_debug: False
auth_ldap_group: cn=groupname,ou=groups,dc=example,dc=com
auth_ldap_groupmembertype: username
auth_ldap_timeout: 5
auth_ldap_emailattr: mail
auth_ldap_userdn: True
auth_ldap_userlist_filter:
auth_ldap_wildcard_ou: False
```
Read more [here](https://github.com/librenms/docker#configuration-management)
## Issue with current LibreNMS
The current version (23.7.0 at the time of writing) does not support lldap. A fix has been accepted to LibreNMS so the next version should just work.
[Link to the commit](https://github.com/librenms/librenms/commit/a71ca98fac1a75753b102be8b3644c4c3ee1a624)
If you want to apply the fix manually, run git apply with this patch.
```
diff --git a/LibreNMS/Authentication/LdapAuthorizer.php b/LibreNMS/Authentication/LdapAuthorizer.php
index 5459759ab..037a7382b 100644
--- a/LibreNMS/Authentication/LdapAuthorizer.php
+++ b/LibreNMS/Authentication/LdapAuthorizer.php
@@ -233,7 +233,7 @@ class LdapAuthorizer extends AuthorizerBase
$entries = ldap_get_entries($connection, $search);
foreach ($entries as $entry) {
$user = $this->ldapToUser($entry);
- if ((int) $user['user_id'] !== (int) $user_id) {
+ if ($user['user_id'] != $user_id) {
continue;
}
@@ -360,7 +360,7 @@ class LdapAuthorizer extends AuthorizerBase
return [
'username' => $entry['uid'][0],
'realname' => $entry['cn'][0],
- 'user_id' => (int) $entry[$uid_attr][0],
+ 'user_id' => $entry[$uid_attr][0],
'email' => $entry[Config::get('auth_ldap_emailattr', 'mail')][0],
'level' => $this->getUserlevel($entry['uid'][0]),
];
```

View File

@@ -66,26 +66,5 @@ fi
DISPLAY_NAME=$(jq -r .displayName <<< $USER_JSON)
IS_ADMIN=false
if [[ ! -z "$3" ]] && jq -e '.groups|map(.displayName)|index("'"$3"'")' <<< "$USER_JSON" > /dev/null 2>&1; then
IS_ADMIN=true
fi
IS_LOCAL=false
if [[ ! -z "$4" ]] && jq -e '.groups|map(.displayName)|index("'"$4"'")' <<< "$USER_JSON" > /dev/null 2>&1; then
IS_LOCAL=true
fi
[[ ! -z "$DISPLAY_NAME" ]] && echo "name = $DISPLAY_NAME"
if [[ "$IS_ADMIN" = true ]]; then
echo "group = system-admin"
else
echo "group = system-users"
fi
if [[ "$IS_LOCAL" = true ]]; then
echo "local_only = true"
else
echo "local_only = false"
fi

View File

@@ -1,6 +1,6 @@
[Unit]
Description=Nitnelave LLDAP
Documentation=https://github.com/lldap/lldap
Documentation=https://github.com/nitnelave/lldap
# Only sqlite
After=network.target

View File

@@ -1,83 +0,0 @@
# Configuration for Maddy Mail Server
Documentation for maddy LDAP can be found [here](https://maddy.email/reference/auth/ldap/).
Maddy will automatically create an imap-acct if a new user connects via LDAP.
Replace `dc=example,dc=com` with your LLDAP configured domain.
## Simple Setup
Depending on the mail client(s) the simple setup can work for you. However, if this does not work for you, follow the instructions in the `Advanced Setup` section.
### DN Template
You only have to specify the dn template:
```
dn_template "cn={username},ou=people,dc=example,dc=com"
```
### Config Example with Docker
Example maddy configuration with LLDAP running in docker.
You can replace `local_authdb` with another name if you want to use multiple auth backends.
If you only want to use one storage backend make sure to disable `auth.pass_table local_authdb` in your config if it is still active.
```
auth.ldap local_authdb {
urls ldap://lldap:3890
dn_template "cn={username},ou=people,dc=example,dc=com"
starttls off
debug off
connect_timeout 1m
}
```
## Advanced Setup
If the simple setup does not work for you, you can use a proper lookup.
### Bind Credentials
If you have a service account in LLDAP with restricted rights (e.g. `lldap_strict_readonly`), replace `admin` with your LLDAP service account.
Replace `admin_password` with the password of either the admin or service account.
```
bind plain "cn=admin,ou=people,dc=example,dc=com" "admin_password"
```
If you do not want to use plain auth check the [maddy LDAP page](https://maddy.email/reference/auth/ldap/) for other options.
### Base DN
```
base_dn "dc=example,dc=com"
```
### Filter
Depending on the mail client, maddy receives and sends either the username or the full E-Mail address as username (even if the username is not an E-Mail).
For the username use:
```
filter "(&(objectClass=person)(uid={username}))"
```
For mapping the username (as E-Mail):
```
filter "(&(objectClass=person)(mail={username}))"
```
For allowing both, username and username as E-Mail use:
```
filter "(&(|(uid={username})(mail={username}))(objectClass=person))"
```
### Config Example with Docker
Example maddy configuration with LLDAP running in docker.
You can replace `local_authdb` with another name if you want to use multiple auth backends.
If you only want to use one storage backend make sure to disable `auth.pass_table local_authdb` in your config if it is still active.
```
auth.ldap local_authdb {
urls ldap://lldap:3890
bind plain "cn=admin,ou=people,dc=example,dc=com" "admin_password"
base_dn "dc=example,dc=com"
filter "(&(|(uid={username})(mail={username}))(objectClass=person))"
starttls off
debug off
connect_timeout 1m
}
```

View File

@@ -1,96 +0,0 @@
# Mailserver Docker
[Docker-mailserver](https://docker-mailserver.github.io/docker-mailserver/latest/) is a Production-ready full-stack but simple mail server (SMTP, IMAP, LDAP, Antispam, Antivirus, etc.) running inside a container.
To integrate with LLDAP, ensure you correctly adjust the `docker-mailserver` container environment values.
## Compose File Sample
```yaml
version: "3.9"
services:
lldap:
image: lldap/lldap:stable
ports:
- "3890:3890"
- "17170:17170"
volumes:
- "lldap_data:/data"
environment:
- VERBOSE=true
- TZ=Etc/UTC
- LLDAP_JWT_SECRET=yourjwt
- LLDAP_LDAP_USER_PASS=adminpassword
- LLDAP_LDAP_BASE_DN=dc=example,dc=com
mailserver:
image: ghcr.io/docker-mailserver/docker-mailserver:latest
container_name: mailserver
hostname: mail.example.com
ports:
- "25:25" # SMTP (explicit TLS => STARTTLS)
- "143:143" # IMAP4 (explicit TLS => STARTTLS)
- "465:465" # ESMTP (implicit TLS)
- "587:587" # ESMTP (explicit TLS => STARTTLS)
- "993:993" # IMAP4 (implicit TLS)
volumes:
- mailserver-data:/var/mail
- mailserver-state:/var/mail-state
- mailserver-config:/tmp/docker-mailserver/
- /etc/localtime:/etc/localtime:ro
restart: always
stop_grace_period: 1m
healthcheck:
test: "ss --listening --tcp | grep -P 'LISTEN.+:smtp' || exit 1"
timeout: 3s
retries: 0
environment:
- LOG_LEVEL=debug
- SUPERVISOR_LOGLEVEL=debug
- SPAMASSASSIN_SPAM_TO_INBOX=1
- ENABLE_FAIL2BAN=0
- ENABLE_AMAVIS=0
- SPOOF_PROTECTION=1
- ENABLE_OPENDKIM=0
- ENABLE_OPENDMARC=0
# >>> Postfix LDAP Integration
- ACCOUNT_PROVISIONER=LDAP
- LDAP_SERVER_HOST=lldap:3890
- LDAP_SEARCH_BASE=ou=people,dc=example,dc=com
- LDAP_BIND_DN=uid=admin,ou=people,dc=example,dc=com
- LDAP_BIND_PW=adminpassword
- LDAP_QUERY_FILTER_USER=(&(objectClass=inetOrgPerson)(|(uid=%u)(mail=%u)))
- LDAP_QUERY_FILTER_GROUP=(&(objectClass=groupOfUniqueNames)(uid=%s))
- LDAP_QUERY_FILTER_ALIAS=(&(objectClass=inetOrgPerson)(|(uid=%u)(mail=%u)))
- LDAP_QUERY_FILTER_DOMAIN=(mail=*@%s)
# <<< Postfix LDAP Integration
# >>> Dovecot LDAP Integration
- DOVECOT_AUTH_BIND=yes
- DOVECOT_USER_FILTER=(&(objectClass=inetOrgPerson)(|(uid=%u)(mail=%u)))
- DOVECOT_USER_ATTRS==uid=5000,=gid=5000,=home=/var/mail/%Ln,=mail=maildir:~/Maildir
- POSTMASTER_ADDRESS=postmaster@d3n.com
cap_add:
- SYS_PTRACE
- NET_ADMIN # For Fail2Ban to work
roundcubemail:
image: roundcube/roundcubemail:latest
container_name: roundcubemail
restart: always
volumes:
- roundcube_data:/var/www/html
ports:
- "9002:80"
environment:
- ROUNDCUBEMAIL_DB_TYPE=sqlite
- ROUNDCUBEMAIL_SKIN=elastic
- ROUNDCUBEMAIL_DEFAULT_HOST=mailserver # IMAP
- ROUNDCUBEMAIL_SMTP_SERVER=mailserver # SMTP
volumes:
mailserver-data:
mailserver-config:
mailserver-state:
lldap_data:
roundcube_data:
```

View File

@@ -1,15 +0,0 @@
## ADD after values in the existing .env file.
## This example uses the unsecured 3890 port. For ldaps, set LDAP_METHOD=simple_tls and LDAP_PORT=6360
## For more details, see https://github.com/joylarkin/mastodon-documentation/blob/master/Running-Mastodon/Enabling-LDAP-login.md
LDAP_ENABLED=true
LDAP_METHOD=plain
LDAP_HOST=lldap
LDAP_PORT=3890
LDAP_BASE=dc=domain,dc=com
LDAP_BIND_DN=uid=admin,ou=people,dc=domain,dc=com
LDAP_PASSWORD=<lldap_admin_password_here>
LDAP_UID=uid
LDAP_MAIL=mail
LDAP_UID_CONVERSION_ENABLED=true
# match username or mail to authenticate, and onlow allow users belonging to group 'mastodon'
LDAP_SEARCH_FILTER=(&(memberof=cn=mastodon,ou=groups,dc=domain,dc=com)(|(%{uid}=%{email})(%{mail}=%{email})))

View File

@@ -1,28 +0,0 @@
# Mealie
Configuration is done solely with environmental variables in the mealie-api docker-compose config:
## Note
[LDAP integration in Mealie currently only works with the nightly branch](https://github.com/hay-kot/mealie/issues/2402#issuecomment-1560176528), so `hkotel/mealie:api-nightly` and `hkotel/mealie:frontend-nightly` rather than the current "stable" release of `v1.0.0beta-5`
## Configuration
The following config should let you login with either members of the `mealie` group as a user, or as an admin user with members of the `mealie-admin` group.
Mealie first checks credentials in the `mealie` group to authenticate, then checks for the presence of the user in the `mealie-admin` group and elevates that account to admin status if present, therefore for any account to be an admin account it must belong in both the `mealie` group and the `mealie-admin` group.
It is recommended to create a `readonly_user` and add them to the `lldap_strict_readonly` group to bind with.
```yaml
- LDAP_AUTH_ENABLED=true
- LDAP_SERVER_URL=ldap://lldap:3890
- LDAP_TLS_INSECURE=true ## Only required for LDAPS with a self-signed certificate
- LDAP_BASE_DN=ou=people,dc=example,dc=com
- LDAP_USER_FILTER=(memberof=cn=mealie,ou=groups,dc=example,dc=com)
- LDAP_ADMIN_FILTER=(memberof=cn=mealie-admin,ou=groups,dc=example,dc=com)
- LDAP_QUERY_BIND=cn=readonly_user,ou=people,dc=example,dc=com
- LDAP_QUERY_PASSWORD=READONLY_USER_PASSWORD
- LDAP_ID_ATTRIBUTE=uid
- LDAP_NAME_ATTRIBUTE=displayName
- LDAP_MAIL_ATTRIBUTE=mail
```

View File

@@ -1,37 +0,0 @@
# MinIO Configuration
MinIO is a High-Performance Object Storage released under GNU Affero General Public License v3. 0. It is API compatible with the Amazon S3 cloud storage service. This example assists with basic LDAP configuration and policy attachment.
## LDAP Config
### Navigation
- Login to the WebUI as a consoleAdmin user
- Navigate to `Administrator > Identity > LDAP`
- Click `Edit Configuration`
### Configuration Options
- Server Insecure: Enabled
- Server Address: Hostname or IP for your LLDAP host
- Lookup Bind DN: `uid=admin,ou=people,dc=example,dc=com`
- It is recommended that you create a separate user account (e.g, `bind_user`) instead of `admin` for sharing Bind credentials with other services. The `bind_user` should be a member of the `lldap_strict_readonly` group to limit access to your LDAP configuration in LLDAP.
- Lookup Bind Password: The password for the user referenced above
- User DN Search Base: `ou=people,dc=example,dc=com`
- User DN Search Filter: `(&(uid=%s)(memberOf=cn=minio_admin,ou=groups,dc=example,dc=com))`
- This search filter will only allow users that are members of the `minio_admin` group to authenticate. To allow all lldap users, this filter can be used instead `(uid=%s)`
- Group Search Base DN: `ou=groups,dc=example,dc=com`
- Group Search Filter: `(member=%d)`
### Enable LDAP
> Note there appears to be a bug in some versions of MinIO where LDAP is enabled and working, however the configuration UI reports that it is not enabled.
Now, you can enable LDAP authentication by clicking the `Enable LDAP` button, a restart of the service or container is needed. With this configuration, LLDAP users will be able to log in to MinIO now. However they will not be able to do anything, as we need to attach policies giving permissions to users.
## Policy Attachment
Creating MinIO policies is outside of the scope for this document, but it is well documented by MinIO [here](https://min.io/docs/minio/linux/administration/identity-access-management/policy-based-access-control.html). Policies are written in JSON, are extremely flexible, and can be configured to be very granular. In this example we will be using one of the built-in Policies, `consoleAdmin`. We will be applying these policies with the `mc` command line utility.
- Alias your MinIO instance: `mc alias set myMinIO http://<your-minio-address>:<your-minio-api-port> admin <your-admin-password>`
- Attach a policy to your LDAP group: `mc admin policy attach myMinIO consoleAdmin --group='cn=minio_admin,ou=groups,dc=example,dc=com'`

View File

@@ -2,7 +2,7 @@
If you're here, there are some assumptions being made about access and capabilities you have on your system:
1. You have Authelia up and running, understand its functionality, and have read through the documentation.
2. You have [LLDAP](https://github.com/lldap/lldap) up and running.
2. You have [LLDAP](https://github.com/nitnelave/lldap) up and running.
3. You have Nextcloud and LLDAP communicating and without any config errors. See the [example config for Nextcloud](nextcloud.md)
## Authelia
@@ -70,7 +70,7 @@ _The first two can be any string you'd like to identify the connection with. The
* *_Do not_* use commas in the Nextcloud Social Login app scope! This caused many issues for me.
* Be sure you update your Authelia `configuration.yml`. Specifically, the line: `redirect_uris`. The new URL should be
`https://nextcloud.example.com/apps/sociallogin/custom_oidc/Authelia`, in some cases the URL also contains the index.php file and has to look like this `https://nextcloud.example.com/index.php/apps/sociallogin/custom_oidc/Authelia`. Check if your nextcloud has index.php in it's URL because if it has this won't work without and if it hasn't the link with index.php won't work.
`https://auth.example.com/index.php/apps/sociallogin/custom_oidc/Authelia`.
* The final field in the URL (Authelia) needs to be the same value you used in the Social Login "Internal Name" field.
* If you've setup LLDAP correctly in nextcloud, the last dropdown for _Default Group_ should show you the `nextcloud_users` group you setup in LLDAP.
@@ -87,4 +87,4 @@ If this is set to *true* then the user flow will _skip_ the login page and autom
### Conclusion
And that's it! Assuming all the settings that worked for me, work for you, you should be able to login using OpenID Connect via Authelia. If you find any errors, it's a good idea to keep a document of all your settings from Authelia/Nextcloud/LLDAP etc so that you can easily reference and ensure everything lines up.
If you have any issues, please create a [discussion](https://github.com/lldap/lldap/discussions) or join the [Discord](https://discord.gg/h5PEdRMNyP).
If you have any issues, please create a [discussion](https://github.com/nitnelave/lldap/discussions) or join the [Discord](https://discord.gg/h5PEdRMNyP).

View File

@@ -1,39 +0,0 @@
# Configuration for PowerDNS Admin
## Navigate
- Login to PowerDNS Admin
- Navigate to: `Administration > Settings > Authentication`
- Select the `LDAP` tab of the `Authentication Settings`
## LDAP Config
- Enable LDAP Authentication: Checked
- Type: OpenLDAP
### Administrator Info
- LDAP URI: `ldap://<your-lldap-ip-or-hostname>:3890`
- LDAP Base DN: `ou=people,dc=example,dc=com`
- LDAP admin username: `uid=admin,ou=people,dc=example,dc=com`
- It is recommended that you create a separate user account (e.g, `bind_user`) instead of `admin` for sharing Bind credentials with other services. The `bind_user` should be a member of the `lldap_strict_readonly` group to limit access to your LDAP configuration in LLDAP.
- LDAP admin password: password of the user specified above
### Filters
- Basic filter: `(objectClass=person)`
- Username field: `uid`
- Group filter: `(objectClass=groupOfUniqueNames)`
- Group name field: `member`
### Group Security (Optional)
> If Group Security is disabled, all users authenticated via LDAP will be given the "User" role.
Group Security is an optional configuration for LLDAP users. It provides a simple 1:1 mapping between LDAP groups, and PowerDNS roles.
- Status: On
- Admin group: `cn=dns_admin,ou=groups,dc=example,dc=com`
- Operator group: `cn=dns_operator,ou=groups,dc=example,dc=com`
- User group: `cn=dns_user,ou=groups,dc=example,dc=com`

View File

@@ -1,83 +0,0 @@
# Proxmox VE Example
Proxmox Virtual Environment is a hyper-converged infrastructure open-source software. It is a hosted hypervisor that can run operating systems including Linux and Windows on x64 hardware. In this example we will setup user and group syncronization, with two example groups `proxmox_user` and `proxmox_admin`. This example was made using Proxmox VE 8.0.3.
## Navigation
- From the `Server View` open the `Datacenter` page
- Then in this page, open the `Permissions > Realms` menu
- In this menu, select `Add > LDAP Server`
## General Options
- Realm: The internal proxmox name for this authentication method
- Base Domain Name: `dc=example,dc=com`
- User Attribute Name: `uid`
- Server: Your LLDAP hostname or IP
- Port: `3890`
- SSL: Leave unchecked unless you're using LDAPS
- Comment: This field will be exposed as the "name" in the login page
## Sync Options
- Bind User: `uid=admin,ou=people,dc=example,dc=com`
- It is recommended that you create a separate user account (e.g, `bind_user`) instead of `admin` for sharing Bind credentials with other services. The `bind_user` should be a member of the `lldap_strict_readonly` group to limit access to your LDAP configuration in LLDAP.
- Bind Password: password of the user specified above
- E-Mail Attribute: `mail`
- Groupname attr: `cn`
- User Filter: `(&(objectClass=person)(|(memberof=cn=proxmox_user,ou=groups,dc=example,dc=com)(memberof=cn=proxmox_admin,ou=groups,dc=example,dc=com)))`
- This filter will only copy users that are members of the `proxmox_user` or `proxmox_admin` groups. If you want to enable all users in lldap, this filter can be used: `(objectClass=person)`
- Group Filter: `(&(objectClass=groupofuniquenames)(|(cn=proxmox_user)(cn=proxmox_admin)))`
- This filter will only copy the `proxmox_user` or `proxmox_admin` groups explicitly. If you want to sync all groups, this filter can be used: `(objectClass=groupofnames)`
- Default Sync Options:
- Scope: `Users and Groups`
- Remove Vanished Options
- Entry: Checked
- Properties: Checked
## Syncronizing
Proxmox operates LDAP authentication by syncronizing with your lldap server to a local database. This sync can be triggered manually, and on a scheduled basis. Proxmox also offers a preview feature, which will report any changes to the local DB from a sync, without applying the changes. It is highly recommended to run a preview on your first syncronization after making any filter changes, to ensure syncronization is happening as expected.
### First Sync
- With the options saved, and from the `Permissions > Realms` page, select the LDAP realm you just created and click `Sync`
- At the sync dialog, click the Preview button, and carefully check the output to ensure all the users and groups you expect are seen, and that nothing is being remove unexpectedly.
- Once the preview output is matching what we expect, we can click the Sync button, on the `Realm Sync` dialog for the ldap realm we created.
### Scheduled Sync (Optional)
- Once we are confident that LDAP syncronizing is working as expected, this can be scheduled as a job from the `Permissions > Realms` page.
- On the second half of the page, click `Add` under `Realm Sync Jobs`
- Set a schedule for this job and click `Create`
## ACLs
Once you have users and groups syncronized from lldap, it is necessary to grant some perimssions to these users or groups so that they are able to use Proxmox. Proxmox handles this with a filesystem-like tree structure, and "roles" which are collections of permissions. In our basic example, we will grant the built-in `Administrator` role to our `proxmox_admin` role to the entire system. Then we will also grant the `proxmox_user` group several roles with different paths so they can clone and create VMs within a specific resource pool (`UserVMs`), but are otherwise restricted from editing or deleting other resources.
> Note that Promox appends the realm name to groups when syncing, so if you named your realm `lldap` the groups as synced will be `proxmox_user-lldap` and `proxmox_admin-lldap`
### Administrator
- From the Datacenter pane, select the `Permissions` menu page.
- Click `Add > Group Permission`
- Path: Type or select `/`
- Group: Type or select the admin group that has syncronized (`proxmox_admin-lldap` in our example)
- Role: `Administrator`
- Finish by clicking the `Add` button and this access should now be granted
### User Role
> This example assumes we have created Resource Pools named `UserVMs` and `Templates`
- From the Datacenter pane, select the `Permissions` menu page.
- We will be adding six rules in total, for each one clicking `Add > Group Permission`
- Path: `/pool/UserVMs`, Group: `proxmox_user-lldap`, Role: PVEVMAdmin
- Path: `/pool/UserVMs`, Group: `proxmox_user-lldap`, Role: PVEPoolAdmin
- Path: `/pool/Templates`, Group: `proxmox_user-lldap`, Role: PVEPoolUser
- Path: `/pool/Templates`, Group: `proxmox_user-lldap`, Role: PVETemplateUser
- The following two rules are based on a default setup of Proxmox, and may need to be updated based on your networking and storage configuration
- Path: `/sdn/zones/localnetwork`, Group: `proxmox_user-lldap`, Role: PVESDNUser
- Path: `/storage/local-lvm`, Group: `proxmox_user-lldap`, Role: PVEDatastoreUser
That completes our basic example. The ACL rules in Proxmox are very flexible though, and custom roles can be created as well. The Proxmox documentation on [User Management](https://pve.proxmox.com/wiki/User_Management) goes into more depth if you wish to write a policy that better fits your use case.

View File

@@ -1,20 +0,0 @@
# Configuration of RADICALE authentification with lldap.
# Fork of the radicale LDAP plugin to work with LLDAP : https://github.com/shroomify-it/radicale-auth-ldap-plugin
# Full docker-compose stack : https://github.com/shroomify-it/docker-deploy_radicale-agendav-lldap
# Radicale config file v0.3 (inside docker container /etc/radicale/config https://radicale.org/v3.html#configuration)
```toml
[auth]
type = radicale_auth_ldap
ldap_url = ldap://lldap:3890
ldap_base = dc=example,dc=com
ldap_attribute = uid
ldap_filter = (objectClass=person)
ldap_binddn = uid=admin,ou=people,dc=example,dc=com
ldap_password = CHANGEME
ldap_scope = LEVEL
ldap_support_extended = no
```

View File

@@ -1,38 +1,7 @@
# Configuration for Seafile
Seafile can be bridged to LLDAP directly, or by using Authelia as an intermediary. This document will guide you through both setups.
## Configuring Seafile v11.0+ to use LLDAP directly
Starting Seafile v11.0 :
- CCNET module doesn't exist anymore
- More flexibility is given to authenticate in seafile : ID binding can now be different from user email, so LLDAP UID can be used.
Add the following to your `seafile/conf/seahub_settings.py` :
```
ENABLE_LDAP = True
LDAP_SERVER_URL = 'ldap://192.168.1.100:3890'
LDAP_BASE_DN = 'ou=people,dc=example,dc=com'
LDAP_ADMIN_DN = 'uid=admin,ou=people,dc=example,dc=com'
LDAP_ADMIN_PASSWORD = 'CHANGE_ME'
LDAP_PROVIDER = 'ldap'
LDAP_LOGIN_ATTR = 'uid'
LDAP_CONTACT_EMAIL_ATTR = 'mail'
LDAP_USER_ROLE_ATTR = ''
LDAP_USER_FIRST_NAME_ATTR = 'givenName'
LDAP_USER_LAST_NAME_ATTR = 'sn'
LDAP_USER_NAME_REVERSE = False
```
* Replace `192.168.1.100:3890` with your LLDAP server's ip/hostname and port.
* Replace every instance of `dc=example,dc=com` with your configured domain.
After restarting the Seafile server, users should be able to log in with their UID and password.
Note : There is currently no ldap binding for users' avatar. If interested, do [mention it](https://forum.seafile.com/t/feature-request-avatar-picture-from-ldap/3350/6) to the developers to give more credit to the feature.
## Configuring Seafile (prior to v11.0) to use LLDAP directly
**Note for Seafile before v11:** Seafile's LDAP interface used to require a unique, immutable user identifier in the format of `username@domain`. This isn't true starting Seafile v11.0 and ulterior versions (see previous section Configuring Seafile v11.0+ §).
For Seafile instances prior to v11, since LLDAP does not provide an attribute like `userPrincipalName`, the only attribute that somewhat qualifies is therefore `mail`. However, using `mail` as the user identifier results in the issue that Seafile will treat you as an entirely new user if you change your email address through LLDAP.
Seafile's LDAP interface requires a unique, immutable user identifier in the format of `username@domain`. Since LLDAP does not provide an attribute like `userPrincipalName`, the only attribute that somewhat qualifies is therefore `mail`. However, using `mail` as the user identifier results in the issue that Seafile will treat you as an entirely new user if you change your email address through LLDAP. If this is not an issue for you, you can configure LLDAP as an authentication source in Seafile directly. A better but more elaborate way to use Seafile with LLDAP is by using Authelia as an intermediary. This document will guide you through both setups.
## Configuring Seafile to use LLDAP directly
Add the following to your `seafile/conf/ccnet.conf` file:
```
[LDAP]
@@ -117,4 +86,4 @@ OAUTH_ATTRIBUTE_MAP = {
}
```
Restart both your Authelia and Seafile server. You should see a "Single Sign-On" button on Seafile's login page. Clicking it should redirect you to Authelia. If you use the [example config for Authelia](authelia_config.yml), you should be able to log in using your LLDAP User ID.
Restart both your Authelia and Seafile server. You should see a "Single Sign-On" button on Seafile's login page. Clicking it should redirect you to Authelia. If you use the [example config for Authelia](authelia_config.yml), you should be able to log in using your LLDAP User ID.

View File

@@ -1,57 +0,0 @@
# Squid
[Squid](http://www.squid-cache.org/) is a caching HTTP/HTTPS proxy.
This guide will show you how to configure it to allow any user of the group `proxy` to use the proxy server.
The configuration file `/etc/squid/squid.conf`
```
auth_param basic program /usr/lib/squid/basic_ldap_auth -b "dc=example,dc=com" -D "uid=admin,ou=people,dc=example,dc=com" -W /etc/squid/ldap_password -f "(&(memberOf=uid=proxy,ou=groups,dc=example,dc=com)(uid=%s))" -H ldap://IP_OF_LLDAP_SERVER:3890
acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN)
acl localnet src 10.0.0.0/8 # RFC 1918 local private network (LAN)
acl localnet src 100.64.0.0/10 # RFC 6598 shared address space (CGN)
acl localnet src 169.254.0.0/16 # RFC 3927 link-local (directly plugged) machines
acl localnet src 172.16.0.0/12 # RFC 1918 local private network (LAN)
acl localnet src 192.168.0.0/16 # RFC 1918 local private network (LAN)
acl localnet src fc00::/7 # RFC 4193 local private network range
acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines
acl SSL_ports port 443
acl Safe_ports port 80 # http
acl Safe_ports port 21 # ftp
acl Safe_ports port 443 # https
acl Safe_ports port 70 # gopher
acl Safe_ports port 210 # wais
acl Safe_ports port 1025-65535 # unregistered ports
acl Safe_ports port 280 # http-mgmt
acl Safe_ports port 488 # gss-http
acl Safe_ports port 591 # filemaker
acl Safe_ports port 777 # multiling http
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
http_access allow localhost manager
http_access deny manager
include /etc/squid/conf.d/*.conf
http_access allow localhost
acl ldap-auth proxy_auth REQUIRED
http_access allow ldap-auth
# http_access deny all
http_port 3128
coredump_dir /var/spool/squid
refresh_pattern ^ftp: 1440 20% 10080
refresh_pattern ^gopher: 1440 0% 1440
refresh_pattern -i (/cgi-bin/|\?) 0 0% 0
refresh_pattern \/(Packages|Sources)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims
refresh_pattern \/Release(|\.gpg)$ 0 0% 0 refresh-ims
refresh_pattern \/InRelease$ 0 0% 0 refresh-ims
refresh_pattern \/(Translation-.*)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims
refresh_pattern . 0 20% 4320
```
The password for the binduser is stored in `/etc/squid/ldap_password` e.g.
```
PASSWORD_FOR_BINDUSER
```
After you restart squid with `systemctl restart squid` check it is working with
```
curl -O -L "https://www.redhat.com/index.html" -x "user_name:password@proxy.example.com:3128"
```

View File

@@ -1,37 +0,0 @@
# Tandoor Recipes LDAP configuration
## LDAP settings are defined by environmental variables as defined in [Tandoor's documentation](https://docs.tandoor.dev/features/authentication/#ldap)
### #Required#
It is recommended to have a read-only account to bind to
```
LDAP_AUTH=1
AUTH_LDAP_SERVER_URI=ldap://lldap:3890/
AUTH_LDAP_BIND_DN=uid=ro_admin,ou=people,DC=example,DC=com
AUTH_LDAP_BIND_PASSWORD=CHANGEME
AUTH_LDAP_USER_SEARCH_BASE_DN=ou=people,DC=example,DC=com
```
### #Optional#
By default it authenticates everybody identified by the search base DN, this allows you to pull certain users from the ```tandoor_users``` group
```
AUTH_LDAP_USER_SEARCH_FILTER_STR=(&(&(objectclass=person)(memberOf=cn=tandoor_users,ou=groups,dc=example,dc=com))(uid=%(user)s))
```
Map Tandoor user fields with their LLDAP counterparts
```
AUTH_LDAP_USER_ATTR_MAP={'first_name': 'givenName', 'last_name': 'sn', 'email': 'mail'}
```
Set whether or not to always update user fields at login and how many seconds for a timeout
```
AUTH_LDAP_ALWAYS_UPDATE_USER=1
AUTH_LDAP_CACHE_TIMEOUT=3600
```
If you use secure LDAP
```
AUTH_LDAP_START_TLS=1
AUTH_LDAP_TLS_CACERTFILE=/etc/ssl/certs/own-ca.pem
```

View File

@@ -1,43 +0,0 @@
# Basic LDAP auth for a The Lounge IRC web-client
[Main documentation here.](https://thelounge.chat/docs/configuration#ldap-support)
## Simple Config:
In this config, The Lounge will use the credentials provided in web ui to authenticate with lldap. It'll allow access if authentication was successful.
```
ldap: {
enable: true,
url: "ldap://localhost:389",
tlsOptions: {},
primaryKey: "uid",
baseDN: "ou=people,dc=example,dc=com",
},
```
## Advanced Config:
`rootDN` is similar to bind DN in other applications. It is used in combination with `rootPassword` to query lldap. `ldap-viewer` user in `lldap` is a member of the group `lldap_strict_readonly` group. This gives `ldap-viewer` user permission to query `lldap`.
With the `filter`, You can limit The Lounge access to users who are a member of the group `thelounge`.
```
ldap: {
enable: true,
url: "ldap://localhost:389",
tlsOptions: {},
primaryKey: "uid",
searchDN: {
rootDN: "uid=ldap-viewer,ou=people,dc=example,dc=com",
rootPassword: ""
filter: "(memberOf=cn=thelounge,ou=groups,dc=example,dc=com)",
base: "dc=example,dc=com",
scope: "sub",
},
},
```

View File

@@ -1,16 +0,0 @@
<!-- Append at the end of the <entry> sections in traccar.xml -->
<entry key='ldap.enable'>true</entry>
<!-- Important: the LDAP port must be specified in both ldap.url and ldap.port -->
<entry key='ldap.url'>ldap://lldap:3890</entry>
<entry key='ldap.port'>3890</entry>
<entry key='ldap.user'>UID=admin,OU=people,DC=domain,DC=com</entry>
<entry key='ldap.password'>BIND_USER_PASSWORD_HERE</entry>
<entry key='ldap.force'>true</entry>
<entry key='ldap.base'>OU=people,DC=domain,DC=com</entry>
<entry key='ldap.idAttribute'>uid</entry>
<entry key='ldap.nameAttribute'>cn</entry>
<entry key='ldap.mailAttribute'>mail</entry>
<!-- Only allow users belonging to group 'traccar' to login -->
<entry key='ldap.searchFilter'>(&amp;(|(uid=:login)(mail=:login))(memberOf=cn=traccar,ou=groups,dc=domain,dc=com))</entry>
<!-- Make new users administrators if they belong to group 'lldap_admin' -->
<entry key='ldap.adminFilter'>(&amp;(|(uid=:login)(mail=:login))(memberOf=cn=lldap_admin,ou=groups,dc=domain,dc=com))</entry>

View File

@@ -49,7 +49,7 @@ mail
```
### Display Name Field Mapping
```
cn
givenname
```
### Avatar Picture Field Mapping
```

Some files were not shown because too many files have changed in this diff Show More