mirror of
https://github.com/lldap/lldap.git
synced 2026-07-25 17:58:44 +00:00
Compare commits
12 Commits
haveibeenpwned
...
v0.4.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 252132430c | |||
| 7f9bc95c5c | |||
| 69fca82a86 | |||
| 9a30cac7b0 | |||
| 558bb37354 | |||
| 5b74852193 | |||
| d18cf1ac37 | |||
| 96f55ff28e | |||
| 825f37d360 | |||
| 8eb27c5267 | |||
| 18d9dd6ff9 | |||
| 308521c632 |
@@ -84,7 +84,7 @@ jobs:
|
|||||||
build-ui:
|
build-ui:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: pre_job
|
needs: pre_job
|
||||||
if: ${{ needs.pre_job.outputs.should_skip != 'true' }}
|
if: ${{ needs.pre_job.outputs.should_skip != 'true' || github.event_name == 'release' }}
|
||||||
container:
|
container:
|
||||||
image: nitnelave/rust-dev:latest
|
image: nitnelave/rust-dev:latest
|
||||||
steps:
|
steps:
|
||||||
@@ -123,7 +123,7 @@ jobs:
|
|||||||
build-bin:
|
build-bin:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: pre_job
|
needs: pre_job
|
||||||
if: ${{ needs.pre_job.outputs.should_skip != 'true' }}
|
if: ${{ needs.pre_job.outputs.should_skip != 'true' || github.event_name == 'release' }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
target: [armv7-unknown-linux-gnueabihf, aarch64-unknown-linux-musl, x86_64-unknown-linux-musl]
|
target: [armv7-unknown-linux-gnueabihf, aarch64-unknown-linux-musl, x86_64-unknown-linux-musl]
|
||||||
@@ -180,11 +180,13 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- 3306:3306
|
- 3306:3306
|
||||||
env:
|
env:
|
||||||
MYSQL_USER: lldapuser
|
MARIADB_USER: lldapuser
|
||||||
MYSQL_PASSWORD: lldappass
|
MARIADB_PASSWORD: lldappass
|
||||||
MYSQL_DATABASE: lldap
|
MARIADB_DATABASE: lldap
|
||||||
MYSQL_ROOT_PASSWORD: rootpass
|
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 1
|
||||||
options: --name mariadb
|
options: >-
|
||||||
|
--name mariadb
|
||||||
|
--health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||||
|
|
||||||
postgresql:
|
postgresql:
|
||||||
image: postgres:latest
|
image: postgres:latest
|
||||||
@@ -194,7 +196,12 @@ jobs:
|
|||||||
POSTGRES_USER: lldapuser
|
POSTGRES_USER: lldapuser
|
||||||
POSTGRES_PASSWORD: lldappass
|
POSTGRES_PASSWORD: lldappass
|
||||||
POSTGRES_DB: lldap
|
POSTGRES_DB: lldap
|
||||||
options: --name postgresql
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
--name postgresql
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
@@ -256,17 +263,27 @@ jobs:
|
|||||||
POSTGRES_USER: lldapuser
|
POSTGRES_USER: lldapuser
|
||||||
POSTGRES_PASSWORD: lldappass
|
POSTGRES_PASSWORD: lldappass
|
||||||
POSTGRES_DB: lldap
|
POSTGRES_DB: lldap
|
||||||
options: --name postgresql
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
--name postgresql
|
||||||
|
|
||||||
mariadb:
|
mariadb:
|
||||||
image: mariadb:latest
|
image: mariadb:latest
|
||||||
ports:
|
ports:
|
||||||
- 3306:3306
|
- 3306:3306
|
||||||
env:
|
env:
|
||||||
MYSQL_USER: lldapuser
|
MARIADB_USER: lldapuser
|
||||||
MYSQL_PASSWORD: lldappass
|
MARIADB_PASSWORD: lldappass
|
||||||
MYSQL_DATABASE: lldap
|
MARIADB_DATABASE: lldap
|
||||||
MYSQL_ROOT_PASSWORD: rootpass
|
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 1
|
||||||
options: --name mariadb
|
options: >-
|
||||||
|
--name mariadb
|
||||||
|
--health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||||
|
|
||||||
|
|
||||||
mysql:
|
mysql:
|
||||||
image: mysql:latest
|
image: mysql:latest
|
||||||
ports:
|
ports:
|
||||||
@@ -275,8 +292,10 @@ jobs:
|
|||||||
MYSQL_USER: lldapuser
|
MYSQL_USER: lldapuser
|
||||||
MYSQL_PASSWORD: lldappass
|
MYSQL_PASSWORD: lldappass
|
||||||
MYSQL_DATABASE: lldap
|
MYSQL_DATABASE: lldap
|
||||||
MYSQL_ROOT_PASSWORD: rootpass
|
MYSQL_ALLOW_EMPTY_PASSWORD: 1
|
||||||
options: --name mysql
|
options: >-
|
||||||
|
--name mysql
|
||||||
|
--health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||||
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -314,7 +333,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create dummy user
|
- name: Create dummy user
|
||||||
run: |
|
run: |
|
||||||
TOKEN=$(curl -X POST -H "Content-Type: application/json" -d '{"username": "admin", "password": "ldappass"}' http://localhost:17170/auth/simple/login | cut -c 11-277)
|
TOKEN=$(curl -X POST -H "Content-Type: application/json" -d '{"username": "admin", "password": "ldappass"}' http://localhost:17170/auth/simple/login | jq -r .token)
|
||||||
echo "$TOKEN"
|
echo "$TOKEN"
|
||||||
curl 'http://localhost:17170/api/graphql' -H 'Content-Type: application/json' -H "Authorization: Bearer ${TOKEN//[$'\t\r\n ']}" --data-binary '{"query":"mutation{\n createUser(user:\n {\n id: \"dummyuser\",\n email: \"dummyuser@example.com\"\n }\n )\n {\n id\n email\n }\n}\n\n\n"}' --compressed
|
curl 'http://localhost:17170/api/graphql' -H 'Content-Type: application/json' -H "Authorization: Bearer ${TOKEN//[$'\t\r\n ']}" --data-binary '{"query":"mutation{\n createUser(user:\n {\n id: \"dummyuser\",\n email: \"dummyuser@example.com\"\n }\n )\n {\n id\n email\n }\n}\n\n\n"}' --compressed
|
||||||
bin/lldap_set_password --base-url http://localhost:17170 --admin-username admin --admin-password ldappass --token $TOKEN --username dummyuser --password dummypassword
|
bin/lldap_set_password --base-url http://localhost:17170 --admin-username admin --admin-password ldappass --token $TOKEN --username dummyuser --password dummypassword
|
||||||
@@ -328,7 +347,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Export and Converting to Postgress
|
- name: Export and Converting to Postgress
|
||||||
run: |
|
run: |
|
||||||
curl -L https://raw.githubusercontent.com/nitnelave/lldap/main/scripts/sqlite_dump_commands.sh -o helper.sh
|
curl -L https://raw.githubusercontent.com/lldap/lldap/main/scripts/sqlite_dump_commands.sh -o helper.sh
|
||||||
chmod +x ./helper.sh
|
chmod +x ./helper.sh
|
||||||
./helper.sh | sqlite3 ./users.db > ./dump.sql
|
./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
|
sed -i -r -e "s/X'([[:xdigit:]]+'[^'])/'\\\x\\1/g" -e '1s/^/BEGIN;\n/' -e '$aCOMMIT;' ./dump.sql
|
||||||
@@ -346,7 +365,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Export and Converting to mariadb
|
- name: Export and Converting to mariadb
|
||||||
run: |
|
run: |
|
||||||
curl -L https://raw.githubusercontent.com/nitnelave/lldap/main/scripts/sqlite_dump_commands.sh -o helper.sh
|
curl -L https://raw.githubusercontent.com/lldap/lldap/main/scripts/sqlite_dump_commands.sh -o helper.sh
|
||||||
chmod +x ./helper.sh
|
chmod +x ./helper.sh
|
||||||
./helper.sh | sqlite3 ./users.db > ./dump.sql
|
./helper.sh | sqlite3 ./users.db > ./dump.sql
|
||||||
cp ./dump.sql ./dump-no-sed.sql
|
cp ./dump.sql ./dump-no-sed.sql
|
||||||
@@ -365,7 +384,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Export and Converting to mysql
|
- name: Export and Converting to mysql
|
||||||
run: |
|
run: |
|
||||||
curl -L https://raw.githubusercontent.com/nitnelave/lldap/main/scripts/sqlite_dump_commands.sh -o helper.sh
|
curl -L https://raw.githubusercontent.com/lldap/lldap/main/scripts/sqlite_dump_commands.sh -o helper.sh
|
||||||
chmod +x ./helper.sh
|
chmod +x ./helper.sh
|
||||||
./helper.sh | sqlite3 ./users.db > ./dump.sql
|
./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 -r -e 's/^INSERT INTO "?([a-zA-Z0-9_]+)"?/INSERT INTO `\1`/' -e '1s/^/START TRANSACTION;\n/' -e '$aCOMMIT;' ./dump.sql
|
||||||
@@ -425,6 +444,34 @@ jobs:
|
|||||||
needs: [build-ui, build-bin]
|
needs: [build-ui, build-bin]
|
||||||
name: Build Docker image
|
name: Build Docker image
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
container: ["debian","alpine"]
|
||||||
|
include:
|
||||||
|
- container: alpine
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: |
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern=v{{version}}
|
||||||
|
type=semver,pattern=v{{major}}
|
||||||
|
type=semver,pattern=v{{major}}.{{minor}}
|
||||||
|
type=semver,pattern=v{{version}},suffix=
|
||||||
|
type=semver,pattern=v{{major}},suffix=
|
||||||
|
type=semver,pattern=v{{major}}.{{minor}},suffix=
|
||||||
|
type=raw,value=latest,enable={{ is_default_branch }}
|
||||||
|
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }},suffix=
|
||||||
|
type=raw,value=latest,enable={{ is_default_branch }},suffix=
|
||||||
|
- container: debian
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
tags: |
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern=v{{version}}
|
||||||
|
type=semver,pattern=v{{major}}
|
||||||
|
type=semver,pattern=v{{major}}.{{minor}}
|
||||||
|
type=raw,value=latest,enable={{ is_default_branch }}
|
||||||
|
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
@@ -446,86 +493,66 @@ jobs:
|
|||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
- uses: docker/setup-buildx-action@v2
|
- uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker ${{ matrix.container }} meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
# list of Docker images to use as base name for tags
|
# list of Docker images to use as base name for tags
|
||||||
images: |
|
images: |
|
||||||
nitnelave/lldap
|
nitnelave/lldap
|
||||||
# generate Docker tags based on the following events/attributes
|
lldap/lldap
|
||||||
tags: |
|
ghcr.io/lldap/lldap
|
||||||
type=ref,event=branch
|
# Wanted Docker tags
|
||||||
type=ref,event=pr
|
# vX-alpine
|
||||||
type=semver,pattern={{version}}
|
# vX.Y-alpine
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
# vX.Y.Z-alpine
|
||||||
type=semver,pattern={{major}}
|
# latest
|
||||||
type=sha
|
# latest-alpine
|
||||||
|
# stable
|
||||||
|
# stable-alpine
|
||||||
|
#################
|
||||||
|
# vX-debian
|
||||||
|
# vX.Y-debian
|
||||||
|
# vX.Y.Z-debian
|
||||||
|
# latest-debian
|
||||||
|
# stable-debian
|
||||||
|
#################
|
||||||
|
# Check matrix for tag list definition
|
||||||
|
flavor: |
|
||||||
|
latest=false
|
||||||
|
suffix=-${{ matrix.container }}
|
||||||
|
tags: ${{ matrix.tags }}
|
||||||
|
|
||||||
- name: parse tag
|
# Docker login to nitnelave/lldap and lldap/lldap
|
||||||
uses: gacts/github-slug@v1
|
- name: Login to Nitnelave/LLDAP Docker Hub
|
||||||
id: slug
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: nitnelave
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
#### docker image :latest tag build ####
|
#### docker image build ####
|
||||||
########################################
|
########################################
|
||||||
- name: Build and push latest alpine
|
- name: Build ${{ matrix.container }} Docker Image
|
||||||
if: github.event_name != 'release'
|
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: ${{ matrix.platforms }}
|
||||||
file: ./.github/workflows/Dockerfile.ci.alpine
|
file: ./.github/workflows/Dockerfile.ci.${{ matrix.container }}
|
||||||
tags: nitnelave/lldap:latest, nitnelave/lldap:latest-alpine
|
tags: |
|
||||||
cache-from: type=gha,mode=max
|
${{ steps.meta.outputs.tags }}
|
||||||
cache-to: type=gha,mode=max
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
- name: Build and push latest debian
|
|
||||||
if: github.event_name != 'release'
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
|
||||||
file: ./.github/workflows/Dockerfile.ci.debian
|
|
||||||
tags: nitnelave/lldap:latest-debian
|
|
||||||
cache-from: type=gha,mode=max
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
|
|
||||||
########################################
|
|
||||||
#### docker image :semver tag build ####
|
|
||||||
########################################
|
|
||||||
- name: Build and push release alpine
|
|
||||||
if: github.event_name == 'release'
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
# Tag as latest, stable, semver, major, major.minor and major.minor.patch.
|
|
||||||
file: ./.github/workflows/Dockerfile.ci.alpine
|
|
||||||
tags: nitnelave/lldap:stable, nitnelave/lldap:stable-alpine, nitnelave/lldap:v${{ steps.slug.outputs.version-semantic }}, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}.${{ steps.slug.outputs.version-patch }}, nitnelave/lldap:v${{ steps.slug.outputs.version-semantic }}-alpine, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}-alpine, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}-alpine.${{ steps.slug.outputs.version-minor }}-alpine, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}.${{ steps.slug.outputs.version-patch }}-alpine
|
|
||||||
cache-from: type=gha,mode=max
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
|
|
||||||
- name: Build and push release debian
|
|
||||||
if: github.event_name == 'release'
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
|
||||||
push: true
|
|
||||||
# Tag as latest, stable, semver, major, major.minor and major.minor.patch.
|
|
||||||
file: ./.github/workflows/Dockerfile.ci.debian
|
|
||||||
tags: nitnelave/lldap:stable-debian, nitnelave/lldap:v${{ steps.slug.outputs.version-semantic }}-debian, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}-debian, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}-debian, nitnelave/lldap:v${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}.${{ steps.slug.outputs.version-patch }}-debian
|
|
||||||
cache-from: type=gha,mode=max
|
cache-from: type=gha,mode=max
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
@@ -537,6 +564,14 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
repository: nitnelave/lldap
|
repository: nitnelave/lldap
|
||||||
|
|
||||||
|
- name: Update lldap repo description
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: peter-evans/dockerhub-description@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
|
repository: lldap/lldap
|
||||||
|
|
||||||
###############################################################
|
###############################################################
|
||||||
### Download artifacts, clean up ui, upload to release page ###
|
### Download artifacts, clean up ui, upload to release page ###
|
||||||
###############################################################
|
###############################################################
|
||||||
|
|||||||
@@ -101,6 +101,13 @@ jobs:
|
|||||||
run: cargo llvm-cov --no-run --lcov --output-path lcov.info
|
run: cargo llvm-cov --no-run --lcov --output-path lcov.info
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v3
|
||||||
|
if: github.ref != 'refs/heads/main' || github.event_name != 'push'
|
||||||
|
with:
|
||||||
|
files: lcov.info
|
||||||
|
fail_ci_if_error: true
|
||||||
|
- name: Upload coverage to Codecov (main)
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||||
with:
|
with:
|
||||||
files: lcov.info
|
files: lcov.info
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
|
|||||||
@@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.4.3] 2023-04-11
|
||||||
|
|
||||||
|
The repository has changed from `nitnelave/lldap` to `lldap/lldap`, both on GitHub
|
||||||
|
and on DockerHub (although we will keep publishing the images to
|
||||||
|
`nitnelave/lldap` for the foreseeable future). All data on GitHub has been
|
||||||
|
migrated, and the new docker images are available both on DockerHub and on the
|
||||||
|
GHCR under `lldap/lldap`.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- EC private keys are not supported for LDAPS.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- SMTP user no longer has a default value (and instead defaults to unauthenticated).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- WASM payload is now delivered uncompressed to Safari due to a Safari bug.
|
||||||
|
- Password reset no longer redirects to login page.
|
||||||
|
- NextCloud config should add the "mail" attribute.
|
||||||
|
- GraphQL parameters are now urldecoded, to support special characters in usernames.
|
||||||
|
- Healthcheck correctly checks the server certificate.
|
||||||
|
|
||||||
|
### New services
|
||||||
|
|
||||||
|
- Home Assistant
|
||||||
|
- Shaarli
|
||||||
|
|
||||||
## [0.4.2] - 2023-03-27
|
## [0.4.2] - 2023-03-27
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
Generated
+9
-10
@@ -2404,7 +2404,8 @@ dependencies = [
|
|||||||
"tracing-forest",
|
"tracing-forest",
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"uuid 0.8.2",
|
"urlencoding",
|
||||||
|
"uuid 1.3.0",
|
||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2530,12 +2531,6 @@ dependencies = [
|
|||||||
"digest 0.10.6",
|
"digest 0.10.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "md5"
|
|
||||||
version = "0.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -4399,14 +4394,17 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urlencoding"
|
||||||
|
version = "2.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
|
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
|
||||||
dependencies = [
|
|
||||||
"md5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
@@ -4415,6 +4413,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
|
checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.8",
|
"getrandom 0.2.8",
|
||||||
|
"md-5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -23,8 +23,8 @@
|
|||||||
src="https://img.shields.io/badge/unsafe-forbidden-success.svg"
|
src="https://img.shields.io/badge/unsafe-forbidden-success.svg"
|
||||||
alt="Unsafe forbidden"/>
|
alt="Unsafe forbidden"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://app.codecov.io/gh/nitnelave/lldap">
|
<a href="https://app.codecov.io/gh/lldap/lldap">
|
||||||
<img alt="Codecov" src="https://img.shields.io/codecov/c/github/nitnelave/lldap" />
|
<img alt="Codecov" src="https://img.shields.io/codecov/c/github/lldap/lldap" />
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lldap_app"
|
name = "lldap_app"
|
||||||
version = "0.4.3-alpha"
|
version = "0.4.3"
|
||||||
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
|
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
include = ["src/**/*", "queries/**/*", "Cargo.toml", "../schema.graphql"]
|
include = ["src/**/*", "queries/**/*", "Cargo.toml", "../schema.graphql"]
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Home Assistant Configuration
|
||||||
|
|
||||||
|
Home Assistant configures ldap auth via the [Command Line Auth Provider](https://www.home-assistant.io/docs/authentication/providers/#command-line). The wiki mentions a script that can be used for LDAP authentication, but it doesn't work in the container version (it is lacking both `ldapsearch` and `curl` ldap protocol support). Thankfully LLDAP has a graphql API to save the day!
|
||||||
|
|
||||||
|
## Graphql-based Auth Script
|
||||||
|
|
||||||
|
The [auth script](lldap-ha-auth.sh) attempts to authenticate a user against an LLDAP server, using credentials provided via `username` and `password` environment variables. The first argument must be the URL of your LLDAP server, accessible from Home Assistant. You can provide an additional optional argument to confine allowed logins to a single group. The script will output the user's display name as the `name` variable, if not empty.
|
||||||
|
|
||||||
|
1. Copy the [auth script](lldap-ha-auth.sh) to your home assistant instance. In this example, we use `/config/lldap-auth.sh`.
|
||||||
|
2. Add the following to your configuration.yaml in Home assistant:
|
||||||
|
```yaml
|
||||||
|
homeassistant:
|
||||||
|
auth_providers:
|
||||||
|
# Ensure you have the homeassistant provider enabled if you want to continue using your existing accounts
|
||||||
|
- type: homeassistant
|
||||||
|
- type: command_line
|
||||||
|
command: /config/lldap-auth.sh
|
||||||
|
# Only allow users in the 'homeassistant_user' group to login.
|
||||||
|
# Change to ["https://lldap.example.com"] to allow all users
|
||||||
|
args: ["https://lldap.example.com", "homeassistant_user"]
|
||||||
|
meta: true
|
||||||
|
```
|
||||||
|
3. Reload your config or restart Home Assistant
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Usernames should be validated using a regular expression to be of
|
||||||
|
# a known format. Special characters will be escaped anyway, but it is
|
||||||
|
# generally not recommended to allow more than necessary.
|
||||||
|
# This pattern is set by default. In your config file, you can either
|
||||||
|
# overwrite it with a different one or use "unset USERNAME_PATTERN" to
|
||||||
|
# disable validation completely.
|
||||||
|
USERNAME_PATTERN='^[a-z|A-Z|0-9|_|-|.]+$'
|
||||||
|
|
||||||
|
# When the timeout (in seconds) is exceeded (e.g. due to slow networking),
|
||||||
|
# authentication fails.
|
||||||
|
TIMEOUT=3
|
||||||
|
|
||||||
|
# Log messages to stderr.
|
||||||
|
log() {
|
||||||
|
echo "$1" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get server address
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
log "Usage: lldap-auth.sh <LLDAP server address> <Optional group to filter>"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
SERVER_URL="${1%/}"
|
||||||
|
|
||||||
|
# Check username and password are present and not malformed.
|
||||||
|
if [ -z "$username" ] || [ -z "$password" ]; then
|
||||||
|
log "Need username and password environment variables."
|
||||||
|
exit 2
|
||||||
|
elif [ ! -z "$USERNAME_PATTERN" ]; then
|
||||||
|
username_match=$(echo "$username" | sed -r "s/$USERNAME_PATTERN/x/")
|
||||||
|
if [ "$username_match" != "x" ]; then
|
||||||
|
log "Username '$username' has an invalid format."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
RESPONSE=$(curl -f -s -X POST -m "$TIMEOUT" -H "Content-type: application/json" -d '{"username":"'"$username"'","password":"'"$password"'"}' "$SERVER_URL/auth/simple/login")
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
log "Auth failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
TOKEN=$(jq -e -r .token <<< $RESPONSE)
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
log "Failed to parse token"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RESPONSE=$(curl -f -s -m "$TIMEOUT" -H "Content-type: application/json" -H "Authorization: Bearer ${TOKEN}" -d '{"variables":{"id":"'"$username"'"},"query":"query($id:String!){user(userId:$id){displayName groups{displayName}}}"}' "$SERVER_URL/api/graphql")
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
log "Failed to get user"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
USER_JSON=$(jq -e .data.user <<< $RESPONSE)
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
log "Failed to parse user json"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -z "$2" ]] && ! jq -e '.groups|map(.displayName)|index("'"$2"'")' <<< $USER_JSON > /dev/null 2>&1; then
|
||||||
|
log "User is not in group '$2'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DISPLAY_NAME=$(jq -r .displayName <<< $USER_JSON)
|
||||||
|
|
||||||
|
[[ ! -z "$DISPLAY_NAME" ]] && echo "name = $DISPLAY_NAME"
|
||||||
|
|
||||||
+8
-4
@@ -2,7 +2,7 @@
|
|||||||
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
|
authors = ["Valentin Tolmer <valentin@tolmer.fr>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "lldap"
|
name = "lldap"
|
||||||
version = "0.4.3-alpha"
|
version = "0.4.3"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix = "0.13"
|
actix = "0.13"
|
||||||
@@ -31,8 +31,9 @@ lber = "0.4.1"
|
|||||||
ldap3_proto = ">=0.3.1"
|
ldap3_proto = ">=0.3.1"
|
||||||
log = "*"
|
log = "*"
|
||||||
orion = "0.17"
|
orion = "0.17"
|
||||||
rustls = "0.20"
|
rustls-pemfile = "1"
|
||||||
serde = "*"
|
serde = "*"
|
||||||
|
serde_bytes = "0.11"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
thiserror = "*"
|
thiserror = "*"
|
||||||
@@ -44,8 +45,7 @@ tracing = "*"
|
|||||||
tracing-actix-web = "0.7"
|
tracing-actix-web = "0.7"
|
||||||
tracing-attributes = "^0.1.21"
|
tracing-attributes = "^0.1.21"
|
||||||
tracing-log = "*"
|
tracing-log = "*"
|
||||||
rustls-pemfile = "1"
|
urlencoding = "2"
|
||||||
serde_bytes = "0.11"
|
|
||||||
webpki-roots = "*"
|
webpki-roots = "*"
|
||||||
|
|
||||||
[dependencies.chrono]
|
[dependencies.chrono]
|
||||||
@@ -114,5 +114,9 @@ version = "0.11"
|
|||||||
default-features = false
|
default-features = false
|
||||||
features = ["rustls-tls-webpki-roots"]
|
features = ["rustls-tls-webpki-roots"]
|
||||||
|
|
||||||
|
[dependencies.rustls]
|
||||||
|
version = "0.20"
|
||||||
|
features = ["dangerous_configuration"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
mockall = "0.11"
|
mockall = "0.11"
|
||||||
|
|||||||
@@ -124,10 +124,12 @@ impl<Handler: BackendHandler> Query<Handler> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn user(context: &Context<Handler>, user_id: String) -> FieldResult<User<Handler>> {
|
pub async fn user(context: &Context<Handler>, user_id: String) -> FieldResult<User<Handler>> {
|
||||||
|
use anyhow::Context;
|
||||||
let span = debug_span!("[GraphQL query] user");
|
let span = debug_span!("[GraphQL query] user");
|
||||||
span.in_scope(|| {
|
span.in_scope(|| {
|
||||||
debug!(?user_id);
|
debug!(?user_id);
|
||||||
});
|
});
|
||||||
|
let user_id = urlencoding::decode(&user_id).context("Invalid user parameter")?;
|
||||||
let user_id = UserId::new(&user_id);
|
let user_id = UserId::new(&user_id);
|
||||||
let handler = context
|
let handler = context
|
||||||
.get_readable_handler(&user_id)
|
.get_readable_handler(&user_id)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::infra::configuration::LdapsOptions;
|
use crate::infra::{configuration::LdapsOptions, ldap_server::read_certificates};
|
||||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||||
use futures_util::SinkExt;
|
use futures_util::SinkExt;
|
||||||
use ldap3_proto::{
|
use ldap3_proto::{
|
||||||
@@ -65,6 +65,7 @@ where
|
|||||||
invalid_answer
|
invalid_answer
|
||||||
);
|
);
|
||||||
info!("Success");
|
info!("Success");
|
||||||
|
resp.close().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,15 +86,44 @@ fn get_root_certificates() -> rustls::RootCertStore {
|
|||||||
root_store
|
root_store
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_tls_connector() -> Result<RustlsTlsConnector> {
|
fn get_tls_connector(ldaps_options: &LdapsOptions) -> Result<RustlsTlsConnector> {
|
||||||
use rustls::ClientConfig;
|
let mut client_config = rustls::ClientConfig::builder()
|
||||||
let client_config = std::sync::Arc::new(
|
.with_safe_defaults()
|
||||||
ClientConfig::builder()
|
.with_root_certificates(get_root_certificates())
|
||||||
.with_safe_defaults()
|
.with_no_client_auth();
|
||||||
.with_root_certificates(get_root_certificates())
|
let (certs, _private_key) = read_certificates(ldaps_options)?;
|
||||||
.with_no_client_auth(),
|
// Check that the server cert is the one in the config file.
|
||||||
);
|
struct CertificateVerifier {
|
||||||
Ok(client_config.into())
|
certificate: rustls::Certificate,
|
||||||
|
certificate_path: String,
|
||||||
|
}
|
||||||
|
impl rustls::client::ServerCertVerifier for CertificateVerifier {
|
||||||
|
fn verify_server_cert(
|
||||||
|
&self,
|
||||||
|
end_entity: &rustls::Certificate,
|
||||||
|
_intermediates: &[rustls::Certificate],
|
||||||
|
_server_name: &rustls::ServerName,
|
||||||
|
_scts: &mut dyn Iterator<Item = &[u8]>,
|
||||||
|
_ocsp_response: &[u8],
|
||||||
|
_now: std::time::SystemTime,
|
||||||
|
) -> std::result::Result<rustls::client::ServerCertVerified, rustls::Error> {
|
||||||
|
if end_entity != &self.certificate {
|
||||||
|
return Err(rustls::Error::InvalidCertificateData(format!(
|
||||||
|
"Server certificate doesn't match the one in the config file {}",
|
||||||
|
&self.certificate_path
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(rustls::client::ServerCertVerified::assertion())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut dangerous_config = rustls::client::DangerousClientConfig {
|
||||||
|
cfg: &mut client_config,
|
||||||
|
};
|
||||||
|
dangerous_config.set_certificate_verifier(std::sync::Arc::new(CertificateVerifier {
|
||||||
|
certificate: certs.first().expect("empty certificate chain").clone(),
|
||||||
|
certificate_path: ldaps_options.cert_file.clone(),
|
||||||
|
}));
|
||||||
|
Ok(std::sync::Arc::new(client_config).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all, level = "info", err)]
|
#[instrument(skip_all, level = "info", err)]
|
||||||
@@ -102,15 +132,20 @@ pub async fn check_ldaps(ldaps_options: &LdapsOptions) -> Result<()> {
|
|||||||
info!("LDAPS not enabled");
|
info!("LDAPS not enabled");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
let tls_connector = get_tls_connector()?;
|
let tls_connector =
|
||||||
|
get_tls_connector(ldaps_options).context("while preparing the tls connection")?;
|
||||||
let url = format!("localhost:{}", ldaps_options.port);
|
let url = format!("localhost:{}", ldaps_options.port);
|
||||||
check_ldap_endpoint(
|
check_ldap_endpoint(
|
||||||
tls_connector
|
tls_connector
|
||||||
.connect(
|
.connect(
|
||||||
rustls::ServerName::try_from(url.as_str())?,
|
rustls::ServerName::try_from("localhost")
|
||||||
TcpStream::connect(&url).await?,
|
.context("while parsing the server name")?,
|
||||||
|
TcpStream::connect(&url)
|
||||||
|
.await
|
||||||
|
.context("while connecting TCP")?,
|
||||||
)
|
)
|
||||||
.await?,
|
.await
|
||||||
|
.context("while connecting TLS")?,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ use crate::{
|
|||||||
opaque_handler::OpaqueHandler,
|
opaque_handler::OpaqueHandler,
|
||||||
},
|
},
|
||||||
infra::{
|
infra::{
|
||||||
access_control::AccessControlledBackendHandler, configuration::Configuration,
|
access_control::AccessControlledBackendHandler,
|
||||||
|
configuration::{Configuration, LdapsOptions},
|
||||||
ldap_handler::LdapHandler,
|
ldap_handler::LdapHandler,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -94,7 +95,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn read_private_key(key_file: &str) -> Result<PrivateKey> {
|
fn read_private_key(key_file: &str) -> Result<PrivateKey> {
|
||||||
use rustls_pemfile::{pkcs8_private_keys, rsa_private_keys};
|
use rustls_pemfile::{ec_private_keys, pkcs8_private_keys, rsa_private_keys};
|
||||||
use std::{fs::File, io::BufReader};
|
use std::{fs::File, io::BufReader};
|
||||||
pkcs8_private_keys(&mut BufReader::new(File::open(key_file)?))
|
pkcs8_private_keys(&mut BufReader::new(File::open(key_file)?))
|
||||||
.map_err(anyhow::Error::from)
|
.map_err(anyhow::Error::from)
|
||||||
@@ -112,29 +113,36 @@ fn read_private_key(key_file: &str) -> Result<PrivateKey> {
|
|||||||
.ok_or_else(|| anyhow!("No PKCS1 key"))
|
.ok_or_else(|| anyhow!("No PKCS1 key"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
.or_else(|_| {
|
||||||
|
ec_private_keys(&mut BufReader::new(File::open(key_file)?))
|
||||||
|
.map_err(anyhow::Error::from)
|
||||||
|
.and_then(|keys| keys.into_iter().next().ok_or_else(|| anyhow!("No EC key")))
|
||||||
|
})
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"Cannot read either PKCS1 or PKCS8 private key from {}",
|
"Cannot read either PKCS1, PKCS8 or EC private key from {}",
|
||||||
key_file
|
key_file
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.map(rustls::PrivateKey)
|
.map(rustls::PrivateKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_tls_acceptor(config: &Configuration) -> Result<RustlsTlsAcceptor> {
|
pub fn read_certificates(
|
||||||
use rustls::{Certificate, ServerConfig};
|
ldaps_options: &LdapsOptions,
|
||||||
use rustls_pemfile::certs;
|
) -> Result<(Vec<rustls::Certificate>, rustls::PrivateKey)> {
|
||||||
use std::{fs::File, io::BufReader};
|
use std::{fs::File, io::BufReader};
|
||||||
// Load TLS key and cert files
|
let certs = rustls_pemfile::certs(&mut BufReader::new(File::open(&ldaps_options.cert_file)?))?
|
||||||
let certs = certs(&mut BufReader::new(File::open(
|
.into_iter()
|
||||||
&config.ldaps_options.cert_file,
|
.map(rustls::Certificate)
|
||||||
)?))?
|
.collect::<Vec<_>>();
|
||||||
.into_iter()
|
let private_key = read_private_key(&ldaps_options.key_file)?;
|
||||||
.map(Certificate)
|
Ok((certs, private_key))
|
||||||
.collect::<Vec<_>>();
|
}
|
||||||
let private_key = read_private_key(&config.ldaps_options.key_file)?;
|
|
||||||
|
fn get_tls_acceptor(ldaps_options: &LdapsOptions) -> Result<RustlsTlsAcceptor> {
|
||||||
|
let (certs, private_key) = read_certificates(ldaps_options)?;
|
||||||
let server_config = std::sync::Arc::new(
|
let server_config = std::sync::Arc::new(
|
||||||
ServerConfig::builder()
|
rustls::ServerConfig::builder()
|
||||||
.with_safe_defaults()
|
.with_safe_defaults()
|
||||||
.with_no_client_auth()
|
.with_no_client_auth()
|
||||||
.with_single_cert(certs, private_key)?,
|
.with_single_cert(certs, private_key)?,
|
||||||
@@ -185,7 +193,8 @@ where
|
|||||||
if config.ldaps_options.enabled {
|
if config.ldaps_options.enabled {
|
||||||
let tls_context = (
|
let tls_context = (
|
||||||
context_for_tls,
|
context_for_tls,
|
||||||
get_tls_acceptor(config).context("while setting up the SSL certificate")?,
|
get_tls_acceptor(&config.ldaps_options)
|
||||||
|
.context("while setting up the SSL certificate")?,
|
||||||
);
|
);
|
||||||
let tls_binder = move || {
|
let tls_binder = move || {
|
||||||
let tls_context = tls_context.clone();
|
let tls_context = tls_context.clone();
|
||||||
|
|||||||
Reference in New Issue
Block a user