mirror of
https://github.com/lldap/lldap.git
synced 2026-07-25 17:58:44 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c052c091e | |||
| 278fb1630d | |||
| 86b2b5148d | |||
| b9e0e4a6dc | |||
| 1b8849ead1 | |||
| 1fe635384f | |||
| df16d66753 | |||
| 65e2c24928 | |||
| c4b8621e2a | |||
| 88a9f8a97b | |||
| fc91d59b99 | |||
| aad4711056 | |||
| c7c6d95334 | |||
| 84b4c66309 | |||
| 923d77072b | |||
| 758aa7f7f7 | |||
| 866a74fa29 | |||
| 36a51070b3 | |||
| 585b65e11d | |||
| 2c8fe2a481 | |||
| 1b67bad270 |
@@ -2,6 +2,7 @@
|
|||||||
.git/*
|
.git/*
|
||||||
.github/*
|
.github/*
|
||||||
.gitignore
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
# Don't track cargo generated files
|
# Don't track cargo generated files
|
||||||
target/*
|
target/*
|
||||||
@@ -17,6 +18,7 @@ Dockerfile
|
|||||||
*.md
|
*.md
|
||||||
LICENSE
|
LICENSE
|
||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
|
README.md
|
||||||
docs/*
|
docs/*
|
||||||
example_configs/*
|
example_configs/*
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
example-configs/** linguist-documentation
|
||||||
|
docs/** linguist-documentation
|
||||||
|
*.md linguist-documentation
|
||||||
|
lldap_config.docker_template.toml linguist-documentation
|
||||||
|
|
||||||
|
schema.graphql linguist-generated
|
||||||
|
|
||||||
|
.github/** -linguist-detectable
|
||||||
|
.devcontainer/** -linguist-detectable
|
||||||
|
.config/** -linguist-detectable
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* @nitnelave
|
||||||
@@ -4,12 +4,18 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'main'
|
- 'main'
|
||||||
|
paths-ignore:
|
||||||
|
- 'docs/**'
|
||||||
|
- 'example_configs/**'
|
||||||
release:
|
release:
|
||||||
types:
|
types:
|
||||||
- 'published'
|
- 'published'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- 'main'
|
- 'main'
|
||||||
|
paths-ignore:
|
||||||
|
- 'docs/**'
|
||||||
|
- 'example_configs/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
msg:
|
msg:
|
||||||
@@ -60,8 +66,25 @@ env:
|
|||||||
# cache based on Cargo.lock per cargo target
|
# cache based on Cargo.lock per cargo target
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
pre_job:
|
||||||
|
continue-on-error: true
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||||
|
steps:
|
||||||
|
- id: skip_check
|
||||||
|
uses: fkirc/skip-duplicate-actions@master
|
||||||
|
with:
|
||||||
|
concurrent_skipping: 'outdated_runs'
|
||||||
|
skip_after_successful_duplicate: ${{ github.ref != 'refs/heads/main' }}
|
||||||
|
paths_ignore: '["**/*.md", "**/docs/**", "example_configs/**", "*.sh", ".gitignore", "lldap_config.docker_template.toml"]'
|
||||||
|
do_not_skip: '["workflow_dispatch", "schedule"]'
|
||||||
|
cancel_others: true
|
||||||
|
|
||||||
build-ui:
|
build-ui:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: pre_job
|
||||||
|
if: ${{ needs.pre_job.outputs.should_skip != 'true' }}
|
||||||
container:
|
container:
|
||||||
image: nitnelave/rust-dev:latest
|
image: nitnelave/rust-dev:latest
|
||||||
steps:
|
steps:
|
||||||
@@ -99,6 +122,8 @@ jobs:
|
|||||||
|
|
||||||
build-bin:
|
build-bin:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: pre_job
|
||||||
|
if: ${{ needs.pre_job.outputs.should_skip != 'true' }}
|
||||||
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]
|
||||||
@@ -145,9 +170,9 @@ jobs:
|
|||||||
name: ${{ matrix.target }}-lldap_set_password-bin
|
name: ${{ matrix.target }}-lldap_set_password-bin
|
||||||
path: target/${{ matrix.target }}/release/lldap_set_password
|
path: target/${{ matrix.target }}/release/lldap_set_password
|
||||||
|
|
||||||
lldap-database-integration-test:
|
lldap-database-init-test:
|
||||||
needs: [build-ui,build-bin]
|
needs: [build-ui,build-bin]
|
||||||
name: LLDAP test
|
name: LLDAP database init test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
services:
|
services:
|
||||||
mariadb:
|
mariadb:
|
||||||
@@ -159,6 +184,7 @@ jobs:
|
|||||||
MYSQL_PASSWORD: lldappass
|
MYSQL_PASSWORD: lldappass
|
||||||
MYSQL_DATABASE: lldap
|
MYSQL_DATABASE: lldap
|
||||||
MYSQL_ROOT_PASSWORD: rootpass
|
MYSQL_ROOT_PASSWORD: rootpass
|
||||||
|
options: --name mariadb
|
||||||
|
|
||||||
postgresql:
|
postgresql:
|
||||||
image: postgres:latest
|
image: postgres:latest
|
||||||
@@ -168,6 +194,7 @@ jobs:
|
|||||||
POSTGRES_USER: lldapuser
|
POSTGRES_USER: lldapuser
|
||||||
POSTGRES_PASSWORD: lldappass
|
POSTGRES_PASSWORD: lldappass
|
||||||
POSTGRES_DB: lldap
|
POSTGRES_DB: lldap
|
||||||
|
options: --name postgresql
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
@@ -175,8 +202,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: x86_64-unknown-linux-musl-lldap-bin
|
name: x86_64-unknown-linux-musl-lldap-bin
|
||||||
path: bin/
|
path: bin/
|
||||||
- name: Where is the bin?
|
|
||||||
run: ls -alR bin
|
|
||||||
- name: Set executables to LLDAP
|
- name: Set executables to LLDAP
|
||||||
run: chmod +x bin/lldap
|
run: chmod +x bin/lldap
|
||||||
|
|
||||||
@@ -212,6 +238,188 @@ jobs:
|
|||||||
LLDAP_ldap_port: 3892
|
LLDAP_ldap_port: 3892
|
||||||
LLDAP_http_port: 17172
|
LLDAP_http_port: 17172
|
||||||
|
|
||||||
|
- name: Check DB container logs
|
||||||
|
run: |
|
||||||
|
docker logs -n 20 mariadb
|
||||||
|
docker logs -n 20 postgresql
|
||||||
|
|
||||||
|
lldap-database-migration-test:
|
||||||
|
needs: [build-ui,build-bin]
|
||||||
|
name: LLDAP database migration test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
postgresql:
|
||||||
|
image: postgres:latest
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: lldapuser
|
||||||
|
POSTGRES_PASSWORD: lldappass
|
||||||
|
POSTGRES_DB: lldap
|
||||||
|
options: --name postgresql
|
||||||
|
mariadb:
|
||||||
|
image: mariadb:latest
|
||||||
|
ports:
|
||||||
|
- 3306:3306
|
||||||
|
env:
|
||||||
|
MYSQL_USER: lldapuser
|
||||||
|
MYSQL_PASSWORD: lldappass
|
||||||
|
MYSQL_DATABASE: lldap
|
||||||
|
MYSQL_ROOT_PASSWORD: rootpass
|
||||||
|
options: --name mariadb
|
||||||
|
mysql:
|
||||||
|
image: mysql:latest
|
||||||
|
ports:
|
||||||
|
- 3307:3306
|
||||||
|
env:
|
||||||
|
MYSQL_USER: lldapuser
|
||||||
|
MYSQL_PASSWORD: lldappass
|
||||||
|
MYSQL_DATABASE: lldap
|
||||||
|
MYSQL_ROOT_PASSWORD: rootpass
|
||||||
|
options: --name mysql
|
||||||
|
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download LLDAP artifacts
|
||||||
|
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@v3
|
||||||
|
with:
|
||||||
|
name: x86_64-unknown-linux-musl-lldap_set_password-bin
|
||||||
|
path: bin/
|
||||||
|
|
||||||
|
- name: Set executables to LLDAP and LLDAP set password
|
||||||
|
run: |
|
||||||
|
chmod +x bin/lldap
|
||||||
|
chmod +x bin/lldap_set_password
|
||||||
|
|
||||||
|
- name: Install sqlite3 and ldap-utils for exporting and searching dummy user
|
||||||
|
run: sudo apt update && sudo apt install -y sqlite3 ldap-utils
|
||||||
|
|
||||||
|
- name: Run lldap with sqlite DB and healthcheck
|
||||||
|
run: |
|
||||||
|
bin/lldap run &
|
||||||
|
sleep 10s
|
||||||
|
bin/lldap healthcheck
|
||||||
|
env:
|
||||||
|
LLDAP_database_url: sqlite://users.db?mode=rwc
|
||||||
|
LLDAP_ldap_port: 3890
|
||||||
|
LLDAP_http_port: 17170
|
||||||
|
LLDAP_LDAP_USER_PASS: ldappass
|
||||||
|
LLDAP_JWT_SECRET: somejwtsecret
|
||||||
|
|
||||||
|
- name: Create dummy user
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
bin/lldap_set_password --base-url http://localhost:17170 --admin-username admin --admin-password ldappass --token $TOKEN --username dummyuser --password dummypassword
|
||||||
|
|
||||||
|
- name: Test Dummy User, This will be checked again after importing
|
||||||
|
run: |
|
||||||
|
ldapsearch -H ldap://localhost:3890 -LLL -D "uid=dummyuser,ou=people,dc=example,dc=com" -w 'dummypassword' -s "One" -b "ou=people,dc=example,dc=com"
|
||||||
|
|
||||||
|
- name: Stop LLDAP sqlite
|
||||||
|
run: pkill lldap
|
||||||
|
|
||||||
|
- name: Export and Converting to Postgress
|
||||||
|
run: |
|
||||||
|
curl -L https://raw.githubusercontent.com/nitnelave/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: |
|
||||||
|
bin/lldap create_schema -d postgres://lldapuser:lldappass@localhost:5432/lldap
|
||||||
|
|
||||||
|
- 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"
|
||||||
|
rm ./dump.sql
|
||||||
|
|
||||||
|
- name: Export and Converting to mariadb
|
||||||
|
run: |
|
||||||
|
curl -L https://raw.githubusercontent.com/nitnelave/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
|
||||||
|
|
||||||
|
- name: Create schema on mariadb
|
||||||
|
run: bin/lldap create_schema -d mysql://lldapuser:lldappass@localhost:3306/lldap
|
||||||
|
|
||||||
|
- 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"
|
||||||
|
rm ./dump.sql
|
||||||
|
|
||||||
|
- name: Export and Converting to mysql
|
||||||
|
run: |
|
||||||
|
curl -L https://raw.githubusercontent.com/nitnelave/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
|
||||||
|
|
||||||
|
- name: Create schema on mysql
|
||||||
|
run: bin/lldap create_schema -d mysql://lldapuser:lldappass@localhost:3307/lldap
|
||||||
|
|
||||||
|
- 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"
|
||||||
|
rm ./dump.sql
|
||||||
|
|
||||||
|
- name: Run lldap with postgres DB and healthcheck again
|
||||||
|
run: |
|
||||||
|
bin/lldap run &
|
||||||
|
sleep 10s
|
||||||
|
bin/lldap healthcheck
|
||||||
|
env:
|
||||||
|
LLDAP_database_url: postgres://lldapuser:lldappass@localhost:5432/lldap
|
||||||
|
LLDAP_ldap_port: 3891
|
||||||
|
LLDAP_http_port: 17171
|
||||||
|
LLDAP_LDAP_USER_PASS: ldappass
|
||||||
|
LLDAP_JWT_SECRET: somejwtsecret
|
||||||
|
|
||||||
|
- name: Run lldap with mariaDB and healthcheck again
|
||||||
|
run: |
|
||||||
|
bin/lldap run &
|
||||||
|
sleep 10s
|
||||||
|
bin/lldap healthcheck
|
||||||
|
env:
|
||||||
|
LLDAP_database_url: mysql://lldapuser:lldappass@localhost:3306/lldap
|
||||||
|
LLDAP_ldap_port: 3892
|
||||||
|
LLDAP_http_port: 17172
|
||||||
|
LLDAP_JWT_SECRET: somejwtsecret
|
||||||
|
|
||||||
|
- name: Run lldap with mysql and healthcheck again
|
||||||
|
run: |
|
||||||
|
bin/lldap run &
|
||||||
|
sleep 10s
|
||||||
|
bin/lldap healthcheck
|
||||||
|
env:
|
||||||
|
LLDAP_database_url: mysql://lldapuser:lldappass@localhost:3307/lldap
|
||||||
|
LLDAP_ldap_port: 3893
|
||||||
|
LLDAP_http_port: 17173
|
||||||
|
LLDAP_JWT_SECRET: somejwtsecret
|
||||||
|
|
||||||
|
- 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-docker-image:
|
build-docker-image:
|
||||||
needs: [build-ui, build-bin]
|
needs: [build-ui, build-bin]
|
||||||
@@ -337,6 +545,8 @@ jobs:
|
|||||||
name: Create release artifacts
|
name: Create release artifacts
|
||||||
if: github.event_name == 'release'
|
if: github.event_name == 'release'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ jobs:
|
|||||||
pre_job:
|
pre_job:
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Map a step output to a job output
|
|
||||||
outputs:
|
outputs:
|
||||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||||
steps:
|
steps:
|
||||||
@@ -22,7 +21,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
concurrent_skipping: 'outdated_runs'
|
concurrent_skipping: 'outdated_runs'
|
||||||
skip_after_successful_duplicate: 'true'
|
skip_after_successful_duplicate: 'true'
|
||||||
paths_ignore: '["**/*.md", "**/docs/**", "example_configs/**", "*.sh"]'
|
paths_ignore: '["**/*.md", "**/docs/**", "example_configs/**", "*.sh", ".dockerignore", ".gitignore", "lldap_config.docker_template.toml", "Dockerfile"]'
|
||||||
do_not_skip: '["workflow_dispatch", "schedule"]'
|
do_not_skip: '["workflow_dispatch", "schedule"]'
|
||||||
cancel_others: true
|
cancel_others: true
|
||||||
|
|
||||||
|
|||||||
Generated
+8
-14
@@ -2345,7 +2345,7 @@ checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lldap"
|
name = "lldap"
|
||||||
version = "0.4.2-alpha"
|
version = "0.4.3-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix",
|
"actix",
|
||||||
"actix-files",
|
"actix-files",
|
||||||
@@ -2404,13 +2404,13 @@ dependencies = [
|
|||||||
"tracing-forest",
|
"tracing-forest",
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"uuid 0.8.2",
|
"uuid 1.3.0",
|
||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lldap_app"
|
name = "lldap_app"
|
||||||
version = "0.4.2-alpha"
|
version = "0.4.3-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
@@ -2418,6 +2418,7 @@ dependencies = [
|
|||||||
"gloo-console",
|
"gloo-console",
|
||||||
"gloo-file",
|
"gloo-file",
|
||||||
"gloo-net",
|
"gloo-net",
|
||||||
|
"gloo-timers",
|
||||||
"graphql_client 0.10.0",
|
"graphql_client 0.10.0",
|
||||||
"http",
|
"http",
|
||||||
"image",
|
"image",
|
||||||
@@ -2427,6 +2428,7 @@ dependencies = [
|
|||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha1",
|
||||||
"url-escape",
|
"url-escape",
|
||||||
"validator",
|
"validator",
|
||||||
"validator_derive",
|
"validator_derive",
|
||||||
@@ -2441,7 +2443,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lldap_auth"
|
name = "lldap_auth"
|
||||||
version = "0.3.0-alpha.1"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
@@ -2530,12 +2532,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"
|
||||||
@@ -2544,7 +2540,7 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "migration-tool"
|
name = "migration-tool"
|
||||||
version = "0.4.2-alpha"
|
version = "0.4.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
@@ -4404,9 +4400,6 @@ 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 +4408,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]]
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ For more features (OAuth/OpenID support, reverse proxy, ...) you can install
|
|||||||
other components (KeyCloak, Authelia, ...) using this server as the source of
|
other components (KeyCloak, Authelia, ...) using this server as the source of
|
||||||
truth for users, via LDAP.
|
truth for users, via LDAP.
|
||||||
|
|
||||||
|
By default, the data is stored in SQLite, but you can swap the backend with
|
||||||
|
MySQL/MariaDB or PostgreSQL.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### With Docker
|
### With Docker
|
||||||
@@ -268,6 +271,7 @@ folder for help with:
|
|||||||
- [Portainer](example_configs/portainer.md)
|
- [Portainer](example_configs/portainer.md)
|
||||||
- [Rancher](example_configs/rancher.md)
|
- [Rancher](example_configs/rancher.md)
|
||||||
- [Seafile](example_configs/seafile.md)
|
- [Seafile](example_configs/seafile.md)
|
||||||
|
- [Shaarli](example_configs/shaarli.md)
|
||||||
- [Syncthing](example_configs/syncthing.md)
|
- [Syncthing](example_configs/syncthing.md)
|
||||||
- [Vaultwarden](example_configs/vaultwarden.md)
|
- [Vaultwarden](example_configs/vaultwarden.md)
|
||||||
- [WeKan](example_configs/wekan.md)
|
- [WeKan](example_configs/wekan.md)
|
||||||
@@ -276,6 +280,12 @@ folder for help with:
|
|||||||
- [XBackBone](example_configs/xbackbone_config.php)
|
- [XBackBone](example_configs/xbackbone_config.php)
|
||||||
- [Zendto](example_configs/zendto.md)
|
- [Zendto](example_configs/zendto.md)
|
||||||
|
|
||||||
|
## Migrating from SQLite
|
||||||
|
|
||||||
|
If you started with an SQLite database and would like to migrate to
|
||||||
|
MySQL/MariaDB or PostgreSQL, check out the [DB
|
||||||
|
migration docs](/docs/database_migration.md).
|
||||||
|
|
||||||
## Comparisons with other services
|
## Comparisons with other services
|
||||||
|
|
||||||
### vs OpenLDAP
|
### vs OpenLDAP
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lldap_app"
|
name = "lldap_app"
|
||||||
version = "0.4.2"
|
version = "0.4.3-alpha"
|
||||||
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"]
|
||||||
@@ -19,6 +19,7 @@ serde = "1"
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
url-escape = "0.1.1"
|
url-escape = "0.1.1"
|
||||||
validator = "=0.14"
|
validator = "=0.14"
|
||||||
|
sha1 = "*"
|
||||||
validator_derive = "*"
|
validator_derive = "*"
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
wasm-bindgen-futures = "*"
|
wasm-bindgen-futures = "*"
|
||||||
@@ -27,6 +28,7 @@ yew-router = "0.16"
|
|||||||
|
|
||||||
# Needed because of https://github.com/tkaitchuck/aHash/issues/95
|
# Needed because of https://github.com/tkaitchuck/aHash/issues/95
|
||||||
indexmap = "=1.6.2"
|
indexmap = "=1.6.2"
|
||||||
|
gloo-timers = "0.2.6"
|
||||||
|
|
||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
version = "0.3"
|
version = "0.3"
|
||||||
|
|||||||
+1
-1
@@ -14,4 +14,4 @@ fi
|
|||||||
|
|
||||||
wasm-pack build --target web --release
|
wasm-pack build --target web --release
|
||||||
|
|
||||||
gzip -9 -f pkg/lldap_app_bg.wasm
|
gzip -9 -k -f pkg/lldap_app_bg.wasm
|
||||||
|
|||||||
@@ -177,7 +177,13 @@ impl App {
|
|||||||
Some(AppRoute::StartResetPassword | AppRoute::FinishResetPassword { token: _ }),
|
Some(AppRoute::StartResetPassword | AppRoute::FinishResetPassword { token: _ }),
|
||||||
_,
|
_,
|
||||||
_,
|
_,
|
||||||
) if self.password_reset_enabled == Some(false) => Some(AppRoute::Login),
|
) => {
|
||||||
|
if self.password_reset_enabled == Some(false) {
|
||||||
|
Some(AppRoute::Login)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
(None, _, _) | (_, None, _) => Some(AppRoute::Login),
|
(None, _, _) | (_, None, _) => Some(AppRoute::Login),
|
||||||
// User is logged in, a URL was given, don't redirect.
|
// User is logged in, a URL was given, don't redirect.
|
||||||
(_, Some(_), Some(_)) => None,
|
(_, Some(_), Some(_)) => None,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
|
components::password_field::PasswordField,
|
||||||
components::router::{AppRoute, Link},
|
components::router::{AppRoute, Link},
|
||||||
infra::{
|
infra::{
|
||||||
api::HostService,
|
api::HostService,
|
||||||
@@ -254,14 +255,12 @@ impl Component for ChangePasswordForm {
|
|||||||
{":"}
|
{":"}
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<Field
|
<PasswordField<FormModel>
|
||||||
form={&self.form}
|
form={&self.form}
|
||||||
field_name="password"
|
field_name="password"
|
||||||
input_type="password"
|
|
||||||
class="form-control"
|
class="form-control"
|
||||||
class_invalid="is-invalid has-error"
|
class_invalid="is-invalid has-error"
|
||||||
class_valid="has-success"
|
class_valid="has-success"
|
||||||
autocomplete="new-password"
|
|
||||||
oninput={link.callback(|_| Msg::FormUpdate)} />
|
oninput={link.callback(|_| Msg::FormUpdate)} />
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
{&self.form.field_message("password")}
|
{&self.form.field_message("password")}
|
||||||
|
|||||||
@@ -149,9 +149,9 @@ impl Component for LoginForm {
|
|||||||
let link = &ctx.link();
|
let link = &ctx.link();
|
||||||
if self.refreshing {
|
if self.refreshing {
|
||||||
html! {
|
html! {
|
||||||
<div>
|
<div class="spinner-border" role="status">
|
||||||
<img src={"spinner.gif"} alt={"Loading"} />
|
<span class="sr-only">{"Loading..."}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
html! {
|
html! {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ pub mod group_details;
|
|||||||
pub mod group_table;
|
pub mod group_table;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
pub mod logout;
|
pub mod logout;
|
||||||
|
pub mod password_field;
|
||||||
pub mod remove_user_from_group;
|
pub mod remove_user_from_group;
|
||||||
pub mod reset_password_step1;
|
pub mod reset_password_step1;
|
||||||
pub mod reset_password_step2;
|
pub mod reset_password_step2;
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
use crate::infra::{
|
||||||
|
api::{hash_password, HostService, PasswordHash, PasswordWasLeaked},
|
||||||
|
common_component::{CommonComponent, CommonComponentParts},
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use gloo_timers::callback::Timeout;
|
||||||
|
use web_sys::{HtmlInputElement, InputEvent};
|
||||||
|
use yew::{html, Callback, Classes, Component, Context, Properties};
|
||||||
|
use yew_form::{Field, Form, Model};
|
||||||
|
|
||||||
|
pub enum PasswordFieldMsg {
|
||||||
|
OnInput(String),
|
||||||
|
OnInputIdle,
|
||||||
|
PasswordCheckResult(Result<(Option<PasswordWasLeaked>, PasswordHash)>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum PasswordState {
|
||||||
|
// Whether the password was found in a leak.
|
||||||
|
Checked(PasswordWasLeaked),
|
||||||
|
// Server doesn't support checking passwords (TODO: move to config).
|
||||||
|
NotSupported,
|
||||||
|
// Requested a check, no response yet from the server.
|
||||||
|
Loading,
|
||||||
|
// User is still actively typing.
|
||||||
|
Typing,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PasswordField<FormModel: Model> {
|
||||||
|
common: CommonComponentParts<Self>,
|
||||||
|
timeout_task: Option<Timeout>,
|
||||||
|
password: String,
|
||||||
|
password_check_state: PasswordState,
|
||||||
|
_marker: std::marker::PhantomData<FormModel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FormModel: Model> CommonComponent<PasswordField<FormModel>> for PasswordField<FormModel> {
|
||||||
|
fn handle_msg(
|
||||||
|
&mut self,
|
||||||
|
ctx: &Context<Self>,
|
||||||
|
msg: <Self as Component>::Message,
|
||||||
|
) -> anyhow::Result<bool> {
|
||||||
|
match msg {
|
||||||
|
PasswordFieldMsg::OnInput(password) => {
|
||||||
|
self.password = password;
|
||||||
|
if self.password_check_state != PasswordState::NotSupported {
|
||||||
|
self.password_check_state = PasswordState::Typing;
|
||||||
|
if self.password.len() >= 8 {
|
||||||
|
let link = ctx.link().clone();
|
||||||
|
self.timeout_task = Some(Timeout::new(500, move || {
|
||||||
|
link.send_message(PasswordFieldMsg::OnInputIdle)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PasswordFieldMsg::PasswordCheckResult(result) => {
|
||||||
|
self.timeout_task = None;
|
||||||
|
// If there's an error from the backend, don't retry.
|
||||||
|
self.password_check_state = PasswordState::NotSupported;
|
||||||
|
if let (Some(check), hash) = result? {
|
||||||
|
if hash == hash_password(&self.password) {
|
||||||
|
self.password_check_state = PasswordState::Checked(check)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PasswordFieldMsg::OnInputIdle => {
|
||||||
|
self.timeout_task = None;
|
||||||
|
if self.password_check_state != PasswordState::NotSupported {
|
||||||
|
self.password_check_state = PasswordState::Loading;
|
||||||
|
self.common.call_backend(
|
||||||
|
ctx,
|
||||||
|
HostService::check_password_haveibeenpwned(hash_password(&self.password)),
|
||||||
|
PasswordFieldMsg::PasswordCheckResult,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mut_common(&mut self) -> &mut CommonComponentParts<PasswordField<FormModel>> {
|
||||||
|
&mut self.common
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq, Clone)]
|
||||||
|
pub struct PasswordFieldProperties<FormModel: Model> {
|
||||||
|
pub field_name: String,
|
||||||
|
pub form: Form<FormModel>,
|
||||||
|
#[prop_or_else(|| { "form-control".into() })]
|
||||||
|
pub class: Classes,
|
||||||
|
#[prop_or_else(|| { "is-invalid".into() })]
|
||||||
|
pub class_invalid: Classes,
|
||||||
|
#[prop_or_else(|| { "is-valid".into() })]
|
||||||
|
pub class_valid: Classes,
|
||||||
|
#[prop_or_else(Callback::noop)]
|
||||||
|
pub oninput: Callback<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FormModel: Model> Component for PasswordField<FormModel> {
|
||||||
|
type Message = PasswordFieldMsg;
|
||||||
|
type Properties = PasswordFieldProperties<FormModel>;
|
||||||
|
|
||||||
|
fn create(_: &Context<Self>) -> Self {
|
||||||
|
Self {
|
||||||
|
common: CommonComponentParts::<Self>::create(),
|
||||||
|
timeout_task: None,
|
||||||
|
password: String::new(),
|
||||||
|
password_check_state: PasswordState::Typing,
|
||||||
|
_marker: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
|
CommonComponentParts::<Self>::update(self, ctx, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, ctx: &Context<Self>) -> yew::Html {
|
||||||
|
let link = &ctx.link();
|
||||||
|
html! {
|
||||||
|
<div>
|
||||||
|
<Field<FormModel>
|
||||||
|
autocomplete={"new-password"}
|
||||||
|
input_type={"password"}
|
||||||
|
field_name={ctx.props().field_name.clone()}
|
||||||
|
form={ctx.props().form.clone()}
|
||||||
|
class={ctx.props().class.clone()}
|
||||||
|
class_invalid={ctx.props().class_invalid.clone()}
|
||||||
|
class_valid={ctx.props().class_valid.clone()}
|
||||||
|
oninput={link.callback(|e: InputEvent| {
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
let target = e.target().unwrap();
|
||||||
|
let input = target.dyn_into::<HtmlInputElement>().unwrap();
|
||||||
|
PasswordFieldMsg::OnInput(input.value())
|
||||||
|
})} />
|
||||||
|
{
|
||||||
|
match self.password_check_state {
|
||||||
|
PasswordState::Checked(PasswordWasLeaked(true)) => html! { <i class="bi bi-x"></i> },
|
||||||
|
PasswordState::Checked(PasswordWasLeaked(false)) => html! { <i class="bi bi-check"></i> },
|
||||||
|
PasswordState::NotSupported | PasswordState::Typing => html!{},
|
||||||
|
PasswordState::Loading =>
|
||||||
|
html! {
|
||||||
|
<div class="spinner-border spinner-border-sm" role="status">
|
||||||
|
<span class="sr-only">{"Loading..."}</span>
|
||||||
|
</div>
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
components::router::{AppRoute, Link},
|
components::{
|
||||||
|
password_field::PasswordField,
|
||||||
|
router::{AppRoute, Link},
|
||||||
|
},
|
||||||
infra::{
|
infra::{
|
||||||
api::HostService,
|
api::HostService,
|
||||||
common_component::{CommonComponent, CommonComponentParts},
|
common_component::{CommonComponent, CommonComponentParts},
|
||||||
@@ -176,14 +179,12 @@ impl Component for ResetPasswordStep2Form {
|
|||||||
{"New password*:"}
|
{"New password*:"}
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<Field
|
<PasswordField<FormModel>
|
||||||
form={&self.form}
|
form={&self.form}
|
||||||
field_name="password"
|
field_name="password"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
class_invalid="is-invalid has-error"
|
class_invalid="is-invalid has-error"
|
||||||
class_valid="has-success"
|
class_valid="has-success"
|
||||||
autocomplete="new-password"
|
|
||||||
input_type="password"
|
|
||||||
oninput={link.callback(|_| Msg::FormUpdate)} />
|
oninput={link.callback(|_| Msg::FormUpdate)} />
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
{&self.form.field_message("password")}
|
{&self.form.field_message("password")}
|
||||||
|
|||||||
+45
-1
@@ -1,4 +1,4 @@
|
|||||||
use super::cookies::set_cookie;
|
use crate::infra::cookies::set_cookie;
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use gloo_net::http::{Method, Request};
|
use gloo_net::http::{Method, Request};
|
||||||
use graphql_client::GraphQLQuery;
|
use graphql_client::GraphQLQuery;
|
||||||
@@ -74,6 +74,19 @@ fn set_cookies_from_jwt(response: login::ServerLoginResponse) -> Result<(String,
|
|||||||
.context("Error setting cookie")
|
.context("Error setting cookie")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub struct PasswordHash(String);
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub struct PasswordWasLeaked(pub bool);
|
||||||
|
|
||||||
|
pub fn hash_password(password: &str) -> PasswordHash {
|
||||||
|
use sha1::{Digest, Sha1};
|
||||||
|
let mut hasher = Sha1::new();
|
||||||
|
hasher.update(password);
|
||||||
|
PasswordHash(format!("{:X}", hasher.finalize()))
|
||||||
|
}
|
||||||
|
|
||||||
impl HostService {
|
impl HostService {
|
||||||
pub async fn graphql_query<QueryType>(
|
pub async fn graphql_query<QueryType>(
|
||||||
variables: QueryType::Variables,
|
variables: QueryType::Variables,
|
||||||
@@ -194,4 +207,35 @@ impl HostService {
|
|||||||
!= http::StatusCode::NOT_FOUND,
|
!= http::StatusCode::NOT_FOUND,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn check_password_haveibeenpwned(
|
||||||
|
password_hash: PasswordHash,
|
||||||
|
) -> Result<(Option<PasswordWasLeaked>, PasswordHash)> {
|
||||||
|
use lldap_auth::password_reset::*;
|
||||||
|
let hash_prefix = &password_hash.0[0..5];
|
||||||
|
match call_server_json_with_error_message::<PasswordHashList, _>(
|
||||||
|
&format!("/auth/password/check/{}", hash_prefix),
|
||||||
|
NO_BODY,
|
||||||
|
"Could not validate token",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(r) => {
|
||||||
|
for PasswordHashCount { hash, count } in r.hashes {
|
||||||
|
if password_hash.0[5..] == hash && count != 0 {
|
||||||
|
return Ok((Some(PasswordWasLeaked(true)), password_hash));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((Some(PasswordWasLeaked(false)), password_hash))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if e.to_string().contains("[501]:") {
|
||||||
|
// Unimplemented, no API key.
|
||||||
|
Ok((None, password_hash))
|
||||||
|
} else {
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-2
@@ -1,6 +1,10 @@
|
|||||||
import init, { run_app } from '/pkg/lldap_app.js';
|
import init, { run_app } from '/pkg/lldap_app.js';
|
||||||
async function main() {
|
async function main() {
|
||||||
await init('/pkg/lldap_app_bg.wasm');
|
if(navigator.userAgent.indexOf('AppleWebKit') != -1) {
|
||||||
run_app();
|
await init('/pkg/lldap_app_bg.wasm');
|
||||||
|
} else {
|
||||||
|
await init('/pkg/lldap_app_bg.wasm.gz');
|
||||||
|
}
|
||||||
|
run_app();
|
||||||
}
|
}
|
||||||
main()
|
main()
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB |
@@ -102,6 +102,17 @@ pub mod password_reset {
|
|||||||
pub user_id: String,
|
pub user_id: String,
|
||||||
pub token: String,
|
pub token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct PasswordHashCount {
|
||||||
|
pub hash: String,
|
||||||
|
pub count: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct PasswordHashList {
|
||||||
|
pub hashes: Vec<PasswordHashCount>,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ Frontend:
|
|||||||
|
|
||||||
Data storage:
|
Data storage:
|
||||||
* The data (users, groups, memberships, active JWTs, ...) is stored in SQL.
|
* The data (users, groups, memberships, active JWTs, ...) is stored in SQL.
|
||||||
* Currently only SQLite is supported (see
|
* The main SQL DBs are supported: SQLite by default, MySQL, MariaDB, PostgreSQL
|
||||||
https://github.com/launchbadge/sqlx/issues/1225 for what blocks us from
|
(see [DB Migration](/database_migration.md) for how to migrate off of
|
||||||
supporting more SQL backends).
|
SQLite).
|
||||||
|
|
||||||
### Code organization
|
### Code organization
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ a transaction:
|
|||||||
```
|
```
|
||||||
sed -i -r -e 's/^INSERT INTO "?([a-zA-Z0-9_]+)"?/INSERT INTO `\1`/' \
|
sed -i -r -e 's/^INSERT INTO "?([a-zA-Z0-9_]+)"?/INSERT INTO `\1`/' \
|
||||||
-e '1s/^/START TRANSACTION;\n/' \
|
-e '1s/^/START TRANSACTION;\n/' \
|
||||||
-e '$aCOMMIT;' /path/to/dump.sql
|
-e '$aCOMMIT;' \
|
||||||
|
-e '1 i\SET FOREIGN_KEY_CHECKS = 0;' /path/to/dump.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
### To MariaDB
|
### To MariaDB
|
||||||
@@ -77,7 +78,8 @@ strings. Use the following command to remove those and perform the additional My
|
|||||||
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" \
|
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 's/^INSERT INTO "?([a-zA-Z0-9_]+)"?/INSERT INTO `\1`/' \
|
||||||
-e '1s/^/START TRANSACTION;\n/' \
|
-e '1s/^/START TRANSACTION;\n/' \
|
||||||
-e '$aCOMMIT;' /path/to/dump.sql
|
-e '$aCOMMIT;' \
|
||||||
|
-e '1 i\SET FOREIGN_KEY_CHECKS = 0;' /path/to/dump.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
## Insert data
|
## Insert data
|
||||||
@@ -102,4 +104,6 @@ or
|
|||||||
|
|
||||||
Modify your `database_url` in `lldap_config.toml` (or `LLDAP_DATABASE_URL` in the env)
|
Modify your `database_url` in `lldap_config.toml` (or `LLDAP_DATABASE_URL` in the env)
|
||||||
to point to your new database (the same value used when generating schema). Restart
|
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.
|
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/nitnelave/lldap/main/.github/workflows/docker-build-static.yml), look for the job `lldap-database-migration-test`
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ occ ldap:set-config s01 ldapGroupFilterMode 0
|
|||||||
occ ldap:set-config s01 ldapGroupDisplayName cn
|
occ ldap:set-config s01 ldapGroupDisplayName cn
|
||||||
occ ldap:set-config s01 ldapGroupFilterObjectclass groupOfUniqueNames
|
occ ldap:set-config s01 ldapGroupFilterObjectclass groupOfUniqueNames
|
||||||
occ ldap:set-config s01 ldapGroupMemberAssocAttr uniqueMember
|
occ ldap:set-config s01 ldapGroupMemberAssocAttr uniqueMember
|
||||||
|
occ ldap:set-config s01 ldapEmailAttribute "mail"
|
||||||
occ ldap:set-config s01 ldapLoginFilterEmail 0
|
occ ldap:set-config s01 ldapLoginFilterEmail 0
|
||||||
occ ldap:set-config s01 ldapLoginFilterUsername 1
|
occ ldap:set-config s01 ldapLoginFilterUsername 1
|
||||||
occ ldap:set-config s01 ldapMatchingRuleInChainState unknown
|
occ ldap:set-config s01 ldapMatchingRuleInChainState unknown
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Configuration for shaarli
|
||||||
|
|
||||||
|
LDAP configuration is in ```/data/config.json.php```
|
||||||
|
|
||||||
|
Just add the following lines:
|
||||||
|
```
|
||||||
|
"ldap": {
|
||||||
|
"host": "ldap://lldap_server:3890",
|
||||||
|
"dn": "uid=%s,ou=people,dc=example,dc=com"
|
||||||
|
}
|
||||||
|
```
|
||||||
+1
-2
@@ -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.2"
|
version = "0.4.3-alpha"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix = "0.13"
|
actix = "0.13"
|
||||||
@@ -59,7 +59,6 @@ version = "4"
|
|||||||
[dependencies.figment]
|
[dependencies.figment]
|
||||||
features = ["env", "toml"]
|
features = ["env", "toml"]
|
||||||
version = "*"
|
version = "*"
|
||||||
|
|
||||||
[dependencies.tracing-subscriber]
|
[dependencies.tracing-subscriber]
|
||||||
version = "0.3"
|
version = "0.3"
|
||||||
features = ["env-filter", "tracing-log"]
|
features = ["env-filter", "tracing-log"]
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
use std::collections::{hash_map::DefaultHasher, HashSet};
|
use std::collections::{hash_map::DefaultHasher, HashSet};
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::task::{Context, Poll};
|
use std::task::Poll;
|
||||||
|
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
cookie::{Cookie, SameSite},
|
cookie::{Cookie, SameSite},
|
||||||
dev::{Service, ServiceRequest, ServiceResponse, Transform},
|
dev::{Service, ServiceRequest, ServiceResponse, Transform},
|
||||||
error::{ErrorBadRequest, ErrorUnauthorized},
|
error::{ErrorBadRequest, ErrorUnauthorized},
|
||||||
web, HttpRequest, HttpResponse,
|
web, FromRequest, HttpRequest, HttpResponse,
|
||||||
};
|
};
|
||||||
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||||
use anyhow::Result;
|
use anyhow::{bail, Context, Result};
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use futures::future::{ok, Ready};
|
use futures::future::{ok, Ready};
|
||||||
use futures_util::FutureExt;
|
use futures_util::FutureExt;
|
||||||
use hmac::Hmac;
|
use hmac::Hmac;
|
||||||
use jwt::{SignWithKey, VerifyWithKey};
|
use jwt::{SignWithKey, VerifyWithKey};
|
||||||
|
use secstr::SecUtf8;
|
||||||
use sha2::Sha512;
|
use sha2::Sha512;
|
||||||
use time::ext::NumericalDuration;
|
use time::ext::NumericalDuration;
|
||||||
use tracing::{debug, instrument, warn};
|
use tracing::{debug, info, instrument, warn};
|
||||||
|
|
||||||
use lldap_auth::{login, password_reset, registration, JWTClaims};
|
use lldap_auth::{login, password_reset, registration, JWTClaims};
|
||||||
|
|
||||||
@@ -183,6 +184,7 @@ where
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
warn!("Error sending email: {:#?}", e);
|
warn!("Error sending email: {:#?}", e);
|
||||||
|
info!("Reset token: {}", token);
|
||||||
return Err(TcpError::InternalServerError(format!(
|
return Err(TcpError::InternalServerError(format!(
|
||||||
"Could not send email: {}",
|
"Could not send email: {}",
|
||||||
e
|
e
|
||||||
@@ -204,6 +206,24 @@ where
|
|||||||
.unwrap_or_else(error_to_http_response)
|
.unwrap_or_else(error_to_http_response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn check_password_reset_token<'a, Backend>(
|
||||||
|
backend_handler: &Backend,
|
||||||
|
token: &Option<&'a str>,
|
||||||
|
) -> TcpResult<Option<(&'a str, UserId)>>
|
||||||
|
where
|
||||||
|
Backend: TcpBackendHandler + 'static,
|
||||||
|
{
|
||||||
|
let token = match token {
|
||||||
|
None => return Ok(None),
|
||||||
|
Some(token) => token,
|
||||||
|
};
|
||||||
|
let user_id = backend_handler
|
||||||
|
.get_user_id_for_password_reset_token(token)
|
||||||
|
.await
|
||||||
|
.map_err(|_| TcpError::UnauthorizedError("Invalid or expired token".to_string()))?;
|
||||||
|
Ok(Some((token, user_id)))
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip_all, level = "debug")]
|
#[instrument(skip_all, level = "debug")]
|
||||||
async fn get_password_reset_step2<Backend>(
|
async fn get_password_reset_step2<Backend>(
|
||||||
data: web::Data<AppState<Backend>>,
|
data: web::Data<AppState<Backend>>,
|
||||||
@@ -212,22 +232,12 @@ async fn get_password_reset_step2<Backend>(
|
|||||||
where
|
where
|
||||||
Backend: TcpBackendHandler + BackendHandler + 'static,
|
Backend: TcpBackendHandler + BackendHandler + 'static,
|
||||||
{
|
{
|
||||||
let token = request
|
let tcp_handler = data.get_tcp_handler();
|
||||||
.match_info()
|
let (token, user_id) =
|
||||||
.get("token")
|
check_password_reset_token(tcp_handler, &request.match_info().get("token"))
|
||||||
.ok_or_else(|| TcpError::BadRequest("Missing reset token".to_owned()))?;
|
.await?
|
||||||
let user_id = data
|
.ok_or_else(|| TcpError::BadRequest("Missing token".to_string()))?;
|
||||||
.get_tcp_handler()
|
let _ = tcp_handler.delete_password_reset_token(token).await;
|
||||||
.get_user_id_for_password_reset_token(token)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
debug!("Reset token error: {e:#}");
|
|
||||||
TcpError::NotFoundError("Wrong or expired reset token".to_owned())
|
|
||||||
})?;
|
|
||||||
let _ = data
|
|
||||||
.get_tcp_handler()
|
|
||||||
.delete_password_reset_token(token)
|
|
||||||
.await;
|
|
||||||
let groups = HashSet::new();
|
let groups = HashSet::new();
|
||||||
let token = create_jwt(&data.jwt_key, user_id.to_string(), groups);
|
let token = create_jwt(&data.jwt_key, user_id.to_string(), groups);
|
||||||
Ok(HttpResponse::Ok()
|
Ok(HttpResponse::Ok()
|
||||||
@@ -402,6 +412,7 @@ where
|
|||||||
Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + LoginHandler + 'static,
|
Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + LoginHandler + 'static,
|
||||||
{
|
{
|
||||||
let user_id = UserId::new(&request.username);
|
let user_id = UserId::new(&request.username);
|
||||||
|
debug!(?user_id);
|
||||||
let bind_request = BindRequest {
|
let bind_request = BindRequest {
|
||||||
name: user_id.clone(),
|
name: user_id.clone(),
|
||||||
password: request.password.clone(),
|
password: request.password.clone(),
|
||||||
@@ -448,6 +459,115 @@ where
|
|||||||
.unwrap_or_else(error_to_http_response)
|
.unwrap_or_else(error_to_http_response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse the response from the HaveIBeenPwned API. Sample response:
|
||||||
|
//
|
||||||
|
// 0018A45C4D1DEF81644B54AB7F969B88D65:1
|
||||||
|
// 00D4F6E8FA6EECAD2A3AA415EEC418D38EC:2
|
||||||
|
// 011053FD0102E94D6AE2F8B83D76FAF94F6:13
|
||||||
|
fn parse_hash_list(response: &str) -> Result<password_reset::PasswordHashList> {
|
||||||
|
use password_reset::*;
|
||||||
|
let parse_line = |line: &str| -> Result<PasswordHashCount> {
|
||||||
|
let split = line.trim().split(':').collect::<Vec<_>>();
|
||||||
|
if let [hash, count] = &split[..] {
|
||||||
|
if hash.len() == 35 {
|
||||||
|
if let Ok(count) = str::parse::<u64>(count) {
|
||||||
|
return Ok(PasswordHashCount {
|
||||||
|
hash: hash.to_string(),
|
||||||
|
count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bail!("Invalid password hash from API: {}", line)
|
||||||
|
};
|
||||||
|
Ok(PasswordHashList {
|
||||||
|
hashes: response
|
||||||
|
.split('\n')
|
||||||
|
.map(parse_line)
|
||||||
|
.collect::<Result<Vec<_>>>()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Refactor that for testing.
|
||||||
|
async fn get_password_hash_list(
|
||||||
|
hash: &str,
|
||||||
|
api_key: &SecUtf8,
|
||||||
|
) -> Result<password_reset::PasswordHashList> {
|
||||||
|
use reqwest::*;
|
||||||
|
let client = Client::new();
|
||||||
|
let resp = client
|
||||||
|
.get(format!("https://api.pwnedpasswords.com/range/{}", hash))
|
||||||
|
.header(header::USER_AGENT, "LLDAP")
|
||||||
|
.header("hibp-api-key", api_key.unsecure())
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("Could not get response from HIBP")?
|
||||||
|
.text()
|
||||||
|
.await?;
|
||||||
|
parse_hash_list(&resp).context("Invalid HIBP response")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_password_pwned<Backend>(
|
||||||
|
data: web::Data<AppState<Backend>>,
|
||||||
|
request: HttpRequest,
|
||||||
|
payload: web::Payload,
|
||||||
|
) -> TcpResult<HttpResponse>
|
||||||
|
where
|
||||||
|
Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + 'static,
|
||||||
|
{
|
||||||
|
let has_reset_token = check_password_reset_token(
|
||||||
|
data.get_tcp_handler(),
|
||||||
|
&request
|
||||||
|
.headers()
|
||||||
|
.get("reset-token")
|
||||||
|
.map(|v| v.to_str().unwrap()),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.is_some();
|
||||||
|
let inner_payload = &mut payload.into_inner();
|
||||||
|
if !has_reset_token
|
||||||
|
&& BearerAuth::from_request(&request, inner_payload)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|bearer| check_if_token_is_valid(&data, bearer.token()).ok())
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
return Err(TcpError::UnauthorizedError(
|
||||||
|
"No token or invalid token".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if data.hibp_api_key.unsecure().is_empty() {
|
||||||
|
return Err(TcpError::NotImplemented("No HIBP API key".to_string()));
|
||||||
|
}
|
||||||
|
let hash = request
|
||||||
|
.match_info()
|
||||||
|
.get("hash")
|
||||||
|
.ok_or_else(|| TcpError::BadRequest("Missing hash".to_string()))?;
|
||||||
|
if hash.len() != 5 || !hash.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||||
|
return Err(TcpError::BadRequest(format!(
|
||||||
|
"Bad request: invalid hash format \"{}\"",
|
||||||
|
hash
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
get_password_hash_list(hash, &data.hibp_api_key)
|
||||||
|
.await
|
||||||
|
.map(|hashes| HttpResponse::Ok().json(hashes))
|
||||||
|
.map_err(|e| TcpError::InternalServerError(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_password_pwned_handler<Backend>(
|
||||||
|
data: web::Data<AppState<Backend>>,
|
||||||
|
request: HttpRequest,
|
||||||
|
payload: web::Payload,
|
||||||
|
) -> HttpResponse
|
||||||
|
where
|
||||||
|
Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + 'static,
|
||||||
|
{
|
||||||
|
check_password_pwned(data, request, payload)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(error_to_http_response)
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip_all, level = "debug")]
|
#[instrument(skip_all, level = "debug")]
|
||||||
async fn opaque_register_start<Backend>(
|
async fn opaque_register_start<Backend>(
|
||||||
request: actix_web::HttpRequest,
|
request: actix_web::HttpRequest,
|
||||||
@@ -564,7 +684,7 @@ where
|
|||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
type Future = Pin<Box<dyn core::future::Future<Output = Result<Self::Response, Self::Error>>>>;
|
type Future = Pin<Box<dyn core::future::Future<Output = Result<Self::Response, Self::Error>>>>;
|
||||||
|
|
||||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
fn poll_ready(&self, cx: &mut std::task::Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
self.service.poll_ready(cx)
|
self.service.poll_ready(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -635,6 +755,11 @@ where
|
|||||||
web::resource("/simple/login").route(web::post().to(simple_login_handler::<Backend>)),
|
web::resource("/simple/login").route(web::post().to(simple_login_handler::<Backend>)),
|
||||||
)
|
)
|
||||||
.service(web::resource("/refresh").route(web::get().to(get_refresh_handler::<Backend>)))
|
.service(web::resource("/refresh").route(web::get().to(get_refresh_handler::<Backend>)))
|
||||||
|
.service(
|
||||||
|
web::resource("/password/check/{hash}")
|
||||||
|
.wrap(CookieToHeaderTranslatorFactory)
|
||||||
|
.route(web::get().to(check_password_pwned_handler::<Backend>)),
|
||||||
|
)
|
||||||
.service(web::resource("/logout").route(web::get().to(get_logout_handler::<Backend>)))
|
.service(web::resource("/logout").route(web::get().to(get_logout_handler::<Backend>)))
|
||||||
.service(
|
.service(
|
||||||
web::scope("/opaque/register")
|
web::scope("/opaque/register")
|
||||||
|
|||||||
@@ -81,6 +81,10 @@ pub struct RunOpts {
|
|||||||
#[clap(short, long, env = "LLDAP_DATABASE_URL")]
|
#[clap(short, long, env = "LLDAP_DATABASE_URL")]
|
||||||
pub database_url: Option<String>,
|
pub database_url: Option<String>,
|
||||||
|
|
||||||
|
/// HaveIBeenPwned API key, to check passwords against leaks.
|
||||||
|
#[clap(long, env = "LLDAP_HIBP_API_KEY")]
|
||||||
|
pub hibp_api_key: Option<String>,
|
||||||
|
|
||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
pub smtp_opts: SmtpOpts,
|
pub smtp_opts: SmtpOpts,
|
||||||
|
|
||||||
@@ -132,6 +136,10 @@ pub enum SmtpEncryption {
|
|||||||
#[derive(Debug, Parser, Clone)]
|
#[derive(Debug, Parser, Clone)]
|
||||||
#[clap(next_help_heading = Some("SMTP"))]
|
#[clap(next_help_heading = Some("SMTP"))]
|
||||||
pub struct SmtpOpts {
|
pub struct SmtpOpts {
|
||||||
|
/// Enable password reset.
|
||||||
|
#[clap(long, env = "LLDAP_SMTP_OPTIONS__ENABLE_PASSWORD_RESET")]
|
||||||
|
pub smtp_enable_password_reset: Option<bool>,
|
||||||
|
|
||||||
/// Sender email address.
|
/// Sender email address.
|
||||||
#[clap(long, env = "LLDAP_SMTP_OPTIONS__FROM")]
|
#[clap(long, env = "LLDAP_SMTP_OPTIONS__FROM")]
|
||||||
pub smtp_from: Option<Mailbox>,
|
pub smtp_from: Option<Mailbox>,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ pub struct MailOptions {
|
|||||||
pub server: String,
|
pub server: String,
|
||||||
#[builder(default = "587")]
|
#[builder(default = "587")]
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
#[builder(default = r#""admin".to_string()"#)]
|
#[builder(default = r#"String::default()"#)]
|
||||||
pub user: String,
|
pub user: String,
|
||||||
#[builder(default = r#"SecUtf8::from("")"#)]
|
#[builder(default = r#"SecUtf8::from("")"#)]
|
||||||
pub password: SecUtf8,
|
pub password: SecUtf8,
|
||||||
@@ -98,6 +98,8 @@ pub struct Configuration {
|
|||||||
pub ldaps_options: LdapsOptions,
|
pub ldaps_options: LdapsOptions,
|
||||||
#[builder(default = r#"String::from("http://localhost")"#)]
|
#[builder(default = r#"String::from("http://localhost")"#)]
|
||||||
pub http_url: String,
|
pub http_url: String,
|
||||||
|
#[builder(default = r#"SecUtf8::from("")"#)]
|
||||||
|
pub hibp_api_key: SecUtf8,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
#[builder(field(private), default = "None")]
|
#[builder(field(private), default = "None")]
|
||||||
server_setup: Option<ServerSetup>,
|
server_setup: Option<ServerSetup>,
|
||||||
@@ -213,6 +215,10 @@ impl ConfigOverrider for RunOpts {
|
|||||||
if let Some(database_url) = self.database_url.as_ref() {
|
if let Some(database_url) = self.database_url.as_ref() {
|
||||||
config.database_url = database_url.to_string();
|
config.database_url = database_url.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(api_key) = self.hibp_api_key.as_ref() {
|
||||||
|
config.hibp_api_key = SecUtf8::from(api_key.clone());
|
||||||
|
}
|
||||||
self.smtp_opts.override_config(config);
|
self.smtp_opts.override_config(config);
|
||||||
self.ldaps_opts.override_config(config);
|
self.ldaps_opts.override_config(config);
|
||||||
}
|
}
|
||||||
@@ -276,6 +282,9 @@ impl ConfigOverrider for SmtpOpts {
|
|||||||
if let Some(tls_required) = self.smtp_tls_required {
|
if let Some(tls_required) = self.smtp_tls_required {
|
||||||
config.smtp_options.tls_required = Some(tls_required);
|
config.smtp_options.tls_required = Some(tls_required);
|
||||||
}
|
}
|
||||||
|
if let Some(enable_password_reset) = self.smtp_enable_password_reset {
|
||||||
|
config.smtp_options.enable_password_reset = enable_password_reset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ use actix_files::{Files, NamedFile};
|
|||||||
use actix_http::{header, HttpServiceBuilder};
|
use actix_http::{header, HttpServiceBuilder};
|
||||||
use actix_server::ServerBuilder;
|
use actix_server::ServerBuilder;
|
||||||
use actix_service::map_config;
|
use actix_service::map_config;
|
||||||
use actix_web::{dev::AppConfig, guard, middleware, web, App, HttpResponse, Responder};
|
use actix_web::{dev::AppConfig, guard, web, App, HttpResponse, Responder};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use hmac::Hmac;
|
use hmac::Hmac;
|
||||||
|
use secstr::SecUtf8;
|
||||||
use sha2::Sha512;
|
use sha2::Sha512;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -38,10 +39,10 @@ pub enum TcpError {
|
|||||||
BadRequest(String),
|
BadRequest(String),
|
||||||
#[error("Internal server error: `{0}`")]
|
#[error("Internal server error: `{0}`")]
|
||||||
InternalServerError(String),
|
InternalServerError(String),
|
||||||
#[error("Not found: `{0}`")]
|
|
||||||
NotFoundError(String),
|
|
||||||
#[error("Unauthorized: `{0}`")]
|
#[error("Unauthorized: `{0}`")]
|
||||||
UnauthorizedError(String),
|
UnauthorizedError(String),
|
||||||
|
#[error("Not implemented: `{0}`")]
|
||||||
|
NotImplemented(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type TcpResult<T> = std::result::Result<T, TcpError>;
|
pub type TcpResult<T> = std::result::Result<T, TcpError>;
|
||||||
@@ -60,14 +61,18 @@ pub(crate) fn error_to_http_response(error: TcpError) -> HttpResponse {
|
|||||||
| DomainError::EntityNotFound(_) => HttpResponse::BadRequest(),
|
| DomainError::EntityNotFound(_) => HttpResponse::BadRequest(),
|
||||||
},
|
},
|
||||||
TcpError::BadRequest(_) => HttpResponse::BadRequest(),
|
TcpError::BadRequest(_) => HttpResponse::BadRequest(),
|
||||||
TcpError::NotFoundError(_) => HttpResponse::NotFound(),
|
|
||||||
TcpError::InternalServerError(_) => HttpResponse::InternalServerError(),
|
TcpError::InternalServerError(_) => HttpResponse::InternalServerError(),
|
||||||
TcpError::UnauthorizedError(_) => HttpResponse::Unauthorized(),
|
TcpError::UnauthorizedError(_) => HttpResponse::Unauthorized(),
|
||||||
|
TcpError::NotImplemented(_) => HttpResponse::NotImplemented(),
|
||||||
}
|
}
|
||||||
.body(error.to_string())
|
.body(error.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wasm_handler() -> actix_web::Result<impl Responder> {
|
async fn wasm_handler() -> actix_web::Result<impl Responder> {
|
||||||
|
Ok(actix_files::NamedFile::open_async("./app/pkg/lldap_app_bg.wasm").await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wasm_handler_compressed() -> actix_web::Result<impl Responder> {
|
||||||
Ok(
|
Ok(
|
||||||
actix_files::NamedFile::open_async("./app/pkg/lldap_app_bg.wasm.gz")
|
actix_files::NamedFile::open_async("./app/pkg/lldap_app_bg.wasm.gz")
|
||||||
.await?
|
.await?
|
||||||
@@ -84,6 +89,7 @@ fn http_config<Backend>(
|
|||||||
jwt_blacklist: HashSet<u64>,
|
jwt_blacklist: HashSet<u64>,
|
||||||
server_url: String,
|
server_url: String,
|
||||||
mail_options: MailOptions,
|
mail_options: MailOptions,
|
||||||
|
hibp_api_key: SecUtf8,
|
||||||
) where
|
) where
|
||||||
Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Clone + 'static,
|
Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Clone + 'static,
|
||||||
{
|
{
|
||||||
@@ -94,6 +100,7 @@ fn http_config<Backend>(
|
|||||||
jwt_blacklist: RwLock::new(jwt_blacklist),
|
jwt_blacklist: RwLock::new(jwt_blacklist),
|
||||||
server_url,
|
server_url,
|
||||||
mail_options,
|
mail_options,
|
||||||
|
hibp_api_key,
|
||||||
}))
|
}))
|
||||||
.route(
|
.route(
|
||||||
"/health",
|
"/health",
|
||||||
@@ -110,12 +117,9 @@ fn http_config<Backend>(
|
|||||||
.configure(super::graphql::api::configure_endpoint::<Backend>),
|
.configure(super::graphql::api::configure_endpoint::<Backend>),
|
||||||
)
|
)
|
||||||
.service(
|
.service(
|
||||||
web::resource("/pkg/lldap_app_bg.wasm").route(
|
web::resource("/pkg/lldap_app_bg.wasm.gz").route(web::route().to(wasm_handler_compressed)),
|
||||||
web::route()
|
|
||||||
.wrap(middleware::Compress::default())
|
|
||||||
.to(wasm_handler),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
.service(web::resource("/pkg/lldap_app_bg.wasm").route(web::route().to(wasm_handler)))
|
||||||
// Serve the /pkg path with the compiled WASM app.
|
// Serve the /pkg path with the compiled WASM app.
|
||||||
.service(Files::new("/pkg", "./app/pkg"))
|
.service(Files::new("/pkg", "./app/pkg"))
|
||||||
// Serve static files
|
// Serve static files
|
||||||
@@ -132,6 +136,7 @@ pub(crate) struct AppState<Backend> {
|
|||||||
pub jwt_blacklist: RwLock<HashSet<u64>>,
|
pub jwt_blacklist: RwLock<HashSet<u64>>,
|
||||||
pub server_url: String,
|
pub server_url: String,
|
||||||
pub mail_options: MailOptions,
|
pub mail_options: MailOptions,
|
||||||
|
pub hibp_api_key: SecUtf8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Backend: BackendHandler> AppState<Backend> {
|
impl<Backend: BackendHandler> AppState<Backend> {
|
||||||
@@ -172,6 +177,7 @@ where
|
|||||||
let mail_options = config.smtp_options.clone();
|
let mail_options = config.smtp_options.clone();
|
||||||
let verbose = config.verbose;
|
let verbose = config.verbose;
|
||||||
info!("Starting the API/web server on port {}", config.http_port);
|
info!("Starting the API/web server on port {}", config.http_port);
|
||||||
|
let hibp_api_key = config.hibp_api_key.clone();
|
||||||
server_builder
|
server_builder
|
||||||
.bind(
|
.bind(
|
||||||
"http",
|
"http",
|
||||||
@@ -182,6 +188,7 @@ where
|
|||||||
let jwt_blacklist = jwt_blacklist.clone();
|
let jwt_blacklist = jwt_blacklist.clone();
|
||||||
let server_url = server_url.clone();
|
let server_url = server_url.clone();
|
||||||
let mail_options = mail_options.clone();
|
let mail_options = mail_options.clone();
|
||||||
|
let hibp_api_key = hibp_api_key.clone();
|
||||||
HttpServiceBuilder::default()
|
HttpServiceBuilder::default()
|
||||||
.finish(map_config(
|
.finish(map_config(
|
||||||
App::new()
|
App::new()
|
||||||
@@ -197,6 +204,7 @@ where
|
|||||||
jwt_blacklist,
|
jwt_blacklist,
|
||||||
server_url,
|
server_url,
|
||||||
mail_options,
|
mail_options,
|
||||||
|
hibp_api_key,
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|_| AppConfig::default(),
|
|_| AppConfig::default(),
|
||||||
|
|||||||
Reference in New Issue
Block a user