Compare commits

..

No commits in common. "master" and "v0.6.0" have entirely different histories.

19 changed files with 1310 additions and 1112 deletions

View File

@ -1,22 +0,0 @@
# Docs: <https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/customizing-dependency-updates>
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule: {interval: monthly}
reviewers: [ViViDboarder]
assignees: [ViViDboarder]
- package-ecosystem: docker
directory: /
schedule: {interval: monthly}
reviewers: [ViViDboarder]
assignees: [ViViDboarder]
- package-ecosystem: cargo
directory: /
schedule: {interval: monthly}
reviewers: [ViViDboarder]
assignees: [ViViDboarder]

View File

@ -16,20 +16,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Build - name: Build
run: cargo build --verbose run: cargo build --verbose
- name: Run tests - name: Run tests
run: cargo test --verbose run: cargo test --verbose
- uses: actions/setup-python@v5 - uses: actions/setup-python@v2
- name: Run pre-commit hooks - name: Run pre-commit hooks
uses: pre-commit/action@v3.0.1 uses: pre-commit/action@v2.0.2
env:
SKIP: hadolint
- name: Run hadolint
uses: hadolint/hadolint-action@v3.1.0

View File

@ -19,13 +19,16 @@ jobs:
- dockerfile: Dockerfile - dockerfile: Dockerfile
latest: "auto" latest: "auto"
suffix: "" suffix: ""
- dockerfile: Dockerfile.alpine
latest: "false"
suffix: "-alpine"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v3
with: with:
images: vividboarder/vaultwarden_ldap images: vividboarder/vaultwarden_ldap
flavor: | flavor: |
@ -39,12 +42,12 @@ jobs:
- name: Login to DockerHub - name: Login to DockerHub
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v3 uses: docker/login-action@v1
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v2
with: with:
context: . context: .
file: ${{ matrix.dockerfile }} file: ${{ matrix.dockerfile }}

2
.gitignore vendored
View File

@ -6,4 +6,4 @@
**/*.rs.bk **/*.rs.bk
# Ignore config while developing # Ignore config while developing
./config.toml config.toml

View File

@ -1,7 +1,7 @@
--- ---
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0 rev: v3.4.0
hooks: hooks:
- id: check-added-large-files - id: check-added-large-files
- id: check-yaml - id: check-yaml
@ -16,10 +16,7 @@ repos:
- id: cargo-check - id: cargo-check
- id: clippy - id: clippy
- repo: https://github.com/IamTheFij/docker-pre-commit - repo: https://github.com/IamTheFij/docker-pre-commit
rev: v3.0.1 rev: v2.0.0
hooks: hooks:
- id: docker-compose-check - id: docker-compose-check
- repo: https://github.com/hadolint/hadolint
rev: v2.12.0
hooks:
- id: hadolint - id: hadolint

2157
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,15 @@
[package] [package]
name = "vaultwarden_ldap" name = "vaultwarden_ldap"
version = "2.0.2" version = "0.6.0"
authors = ["ViViDboarder <vividboarder@gmail.com>"] authors = ["ViViDboarder <vividboarder@gmail.com>"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
ldap3 = "0.11" ldap3 = "0.6"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
toml = "0.8" toml = "0.5"
reqwest = { version = "0.12", features = ["json", "blocking"] } reqwest = "0.9"
serde_json = "1.0" serde_json = "1.0"
thiserror = "1.0" thiserror = "1.0"
anyhow = "1.0" anyhow = "1.0"
envy = "0.4.1" envy = "0.4.1"
pledge = "0.4.2"
unveil = "0.3.2"

View File

@ -1,4 +1,7 @@
FROM rust:1.82 as builder ARG BUILD_TAG=1.46
ARG RUN_TAG=$BUILD_TAG
FROM rust:$BUILD_TAG as builder
WORKDIR /usr/src/ WORKDIR /usr/src/
RUN USER=root cargo new --bin vaultwarden_ldap RUN USER=root cargo new --bin vaultwarden_ldap
@ -9,17 +12,13 @@ COPY Cargo.toml Cargo.lock ./
RUN cargo build --locked --release RUN cargo build --locked --release
# Remove bins to make sure we rebuild # Remove bins to make sure we rebuild
# hadolint ignore=DL3059
RUN rm ./target/release/deps/vaultwarden_ldap* RUN rm ./target/release/deps/vaultwarden_ldap*
# Copy source and install # Copy source and install
COPY src ./src COPY src ./src
RUN cargo build --release RUN cargo build --release
# Use most recent ubuntu LTS release FROM rust:$RUN_TAG
FROM ubuntu:24.04 WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends 'ca-certificates=20240203' 'libssl-dev=3.*' \
&& rm -rf /var/cache/apt/lists
COPY --from=builder /usr/src/vaultwarden_ldap/target/release/vaultwarden_ldap /usr/local/bin/ COPY --from=builder /usr/src/vaultwarden_ldap/target/release/vaultwarden_ldap /usr/local/bin/
CMD ["/usr/local/bin/vaultwarden_ldap"] CMD ["/usr/local/bin/vaultwarden_ldap"]

21
Dockerfile.alpine Normal file
View File

@ -0,0 +1,21 @@
FROM ekidd/rust-musl-builder:1.46.0 AS builder
WORKDIR /home/rust/src
# Cache build deps
RUN USER=rust cargo init
COPY Cargo.toml Cargo.lock ./
RUN cargo build --locked --release && \
rm src/*.rs
COPY --chown=rust:rust ./src ./src
RUN touch ./src/main.rs && \
cargo build --release
FROM alpine:3
RUN apk --no-cache add ca-certificates=20191127-r5
COPY --from=builder \
/home/rust/src/target/x86_64-unknown-linux-musl/release/vaultwarden_ldap \
/usr/local/bin/
CMD ["/usr/local/bin/vaultwarden_ldap"]

View File

@ -34,45 +34,22 @@ test:
cargo test cargo test
# Run bootstrapped integration test # Run bootstrapped integration test
.PHONY: itest-up
itest-up:
docker compose -f docker-compose.yml \
-f itest/docker-compose.itest.yml \
build
docker compose -f docker-compose.yml \
-f itest/docker-compose.itest.yml \
up -d vaultwarden ldap
.PHONY: itest-run
itest-run:
docker compose -f docker-compose.yml \
-f itest/docker-compose.itest.yml \
run ldap_sync
.PHONY: itest-stop
itest-stop:
docker compose stop
.PHONY: itest .PHONY: itest
itest: itest-up itest-run itest-stop itest:
docker-compose -f docker-compose.yml \
-f itest/docker-compose.itest.yml \
up --build
# Run bootstrapped integration test using env for config # Run bootstrapped integration test using env for config
.PHONY: itest-env .PHONY: itest-env
itest-env: itest-env:
docker compose -f docker-compose.yml \ docker-compose -f docker-compose.yml \
-f itest/docker-compose.itest-env.yml \ -f itest/docker-compose.itest-env.yml \
build up --build
docker compose -f docker-compose.yml \
-f itest/docker-compose.itest-env.yml \
up -d vaultwarden ldap
docker compose -f docker-compose.yml \
-f itest/docker-compose.itest-env.yml \
run ldap_sync
docker compose stop
.PHONY: clean-itest .PHONY: clean-itest
clean-itest: clean-itest:
docker compose down -v docker-compose down -v
# Installs pre-commit hooks # Installs pre-commit hooks
.PHONY: install-hooks .PHONY: install-hooks
@ -94,8 +71,12 @@ clean:
rm -f ./target rm -f ./target
.PHONY: docker-build-all .PHONY: docker-build-all
docker-build-all: docker-build docker-build-all: docker-build docker-build-alpine
.PHONY: docker-build .PHONY: docker-build
docker-build: docker-build:
docker build -f ./Dockerfile -t $(DOCKER_TAG) . docker build -f ./Dockerfile -t $(DOCKER_TAG) .
.PHONY: docker-build-alpine
docker-build-alpine:
docker build -f ./Dockerfile.alpine -t $(DOCKER_TAG):alpine .

View File

@ -1,7 +1,7 @@
# vaultwarden_ldap # vaultwarden_ldap
LDAP user invites for [vaultwarden](https://github.com/dani-garcia/vaultwarden) An LDAP connector for [vaultwarden](https://github.com/dani-garcia/vaultwarden)
After configuring, run `vaultwarden_ldap` and it will invite any users it finds in LDAP to your `vaultwarden` instance. This is NOT a sync tool like the [Bitwarden Directory Connector](https://bitwarden.com/help/directory-sync/). After configuring, run `vaultwarden_ldap` and it will invite any users it finds in LDAP to your `vaultwarden` instance.
## Deploying ## Deploying
@ -34,8 +34,6 @@ Configuration values are as follows:
|`ldap_sync_interval_seconds`|Integer|Optional|Number of seconds to wait between each LDAP request. Defaults to `60`| |`ldap_sync_interval_seconds`|Integer|Optional|Number of seconds to wait between each LDAP request. Defaults to `60`|
|`ldap_sync_loop`|Boolean|Optional|Indicates whether or not syncing should be polled in a loop or done once. Defaults to `true`| |`ldap_sync_loop`|Boolean|Optional|Indicates whether or not syncing should be polled in a loop or done once. Defaults to `true`|
Alternatively, instead of using `config.toml`, all values can be provided using enviroment variables prefixed with `APP_`. For example: `APP_VAULTWARDEN_URL=https://vault.example.com`
## Development ## Development
This repo has a predefined set of [pre-commit](https://pre-commit.com) rules. You can install pre-commit via any means you'd like. Once your system has `pre-commit` installed, you can run `make install-hooks` to ensure the hooks will run with every commit. You can also force running all hooks with `make check`. This repo has a predefined set of [pre-commit](https://pre-commit.com) rules. You can install pre-commit via any means you'd like. Once your system has `pre-commit` installed, you can run `make install-hooks` to ensure the hooks will run with every commit. You can also force running all hooks with `make check`.
@ -44,18 +42,7 @@ For those less familiar with `cargo`, you can use the `make` targets that have b
## Testing ## Testing
There are no unit tests, but there are integration tests that require manual verification. All testing is manual right now. First step is to set up Bitwarden and the LDAP server.
### Integration tests
Running `make itest` will spin up an ldap server with a test user, a Vaultwarden server, and then run the sync. If successful the log should show an invitation sent to the test user. If you run `make itest` again, it should show no invites sent because the user already has been invited. If you'd like to reset the testing, `make clean-itest` will clear out the Vaultwarden database and start fresh.
It's also possible to test passing configs via enviornment variables by running `make itest-env`. The validation steps are the same.
### Steps for manual testing
The first step is to set up Bitwarden and the LDAP server.
```bash ```bash
docker-compose up -d vaultwarden ldap ldap_admin docker-compose up -d vaultwarden ldap ldap_admin
@ -85,3 +72,8 @@ docker-compose up ldap_sync
Alternately, you can bootstrap some of this by running: Alternately, you can bootstrap some of this by running:
docker-compose -f docker-compose.yml -f itest/docker-compose.itest.yml up --build docker-compose -f docker-compose.yml -f itest/docker-compose.itest.yml up --build
## Future
* Any kind of proper logging
* Tests

View File

@ -1,4 +1,5 @@
--- ---
version: '3'
services: services:
ldap_sync: ldap_sync:
build: build:
@ -9,7 +10,7 @@ services:
# ./root.cert:/usr/src/vaultwarden_ldap/root.cert:ro # ./root.cert:/usr/src/vaultwarden_ldap/root.cert:ro
environment: environment:
CONFIG_PATH: /config.toml CONFIG_PATH: /config.toml
RUST_BACKTRACE: full RUST_BACKTRACE: 1
depends_on: depends_on:
- vaultwarden - vaultwarden
- ldap - ldap
@ -23,7 +24,6 @@ services:
ADMIN_TOKEN: admin ADMIN_TOKEN: admin
SIGNUPS_ALLOWED: 'false' SIGNUPS_ALLOWED: 'false'
INVITATIONS_ALLOWED: 'true' INVITATIONS_ALLOWED: 'true'
I_REALLY_WANT_VOLATILE_STORAGE: 'true'
ldap: ldap:
image: osixia/openldap image: osixia/openldap

View File

@ -1,16 +1,22 @@
# LDIF Export for cn=Users,dc=example,dc=org
# Server: ldap (ldap)
# Search Scope: sub
# Search Filter: (objectClass=*)
# Total Entries: 2
#
# Generated by phpLDAPadmin (http://phpldapadmin.sourceforge.net) on May 4, 2021 6:06 pm # Generated by phpLDAPadmin (http://phpldapadmin.sourceforge.net) on May 4, 2021 6:06 pm
# Version: 1.2.5 # Version: 1.2.5
version: 1 version: 1
# Entry 1: Users group # Entry 1: cn=Users,dc=example,dc=org
dn: cn=Users,dc=example,dc=org dn: cn=Users,dc=example,dc=org
cn: Users cn: Users
gidnumber: 500 gidnumber: 500
objectclass: posixGroup objectclass: posixGroup
objectclass: top objectclass: top
# Entry 2: User with email # Entry 2: cn=Someone,cn=Users,dc=example,dc=org
dn: cn=Someone,cn=Users,dc=example,dc=org dn: cn=Someone,cn=Users,dc=example,dc=org
cn: Someone cn: Someone
gidnumber: 500 gidnumber: 500
@ -23,7 +29,7 @@ sn: Someone
uid: someone uid: someone
uidnumber: 1000 uidnumber: 1000
# Entry 3: User with no email # Entry 3: cn=SomeoneNoEmail,cn=Users,dc=example,dc=org
dn: cn=SomeoneNoEmail,cn=Users,dc=example,dc=org dn: cn=SomeoneNoEmail,cn=Users,dc=example,dc=org
cn: SomeoneNoEmail cn: SomeoneNoEmail
gidnumber: 500 gidnumber: 500
@ -35,15 +41,13 @@ sn: SomeoneNoEmail
uid: someonenoemail uid: someonenoemail
uidnumber: 1001 uidnumber: 1001
# Entry 4: User with email containing + # Entry 4: cn=SomeoneNoEmailNoUid,cn=Users,dc=example,dc=org
dn: cn=SomeonePlus,cn=Users,dc=example,dc=org # dn: cn=SomeoneNoEmailNoUid,cn=Users,dc=example,dc=org
cn: SomeonePlus # cn: SomeoneNoEmailNoUid
gidnumber: 500 # gidnumber: 500
homedirectory: /home/users/someoneplus # homedirectory: /home/users/someonenoemailnoUid
mail: test+plus@example.com # objectclass: inetOrgPerson
objectclass: inetOrgPerson # objectclass: posixAccount
objectclass: posixAccount # objectclass: top
objectclass: top # sn: SomeoneNoEmail
sn: SomeonePlus # uidnumber: 1002
uid: someoneplus
uidnumber: 1002

View File

@ -1,8 +0,0 @@
vaultwarden_url = "http://vaultwarden:80"
vaultwarden_admin_token = "admin"
ldap_host = "ldap"
ldap_bind_dn = "cn=readonly,dc=example,dc=org"
ldap_bind_password = "readonly"
ldap_search_base_dn = "dc=example,dc=org"
ldap_search_filter = "(&(objectClass=*)(uid=*))"
ldap_sync_loop = false

View File

@ -1,4 +1,5 @@
--- ---
version: '3'
services: services:
ldap_sync: ldap_sync:
environment: environment:
@ -10,11 +11,11 @@ services:
APP_LDAP_BIND_PASSWORD: "admin" APP_LDAP_BIND_PASSWORD: "admin"
APP_LDAP_SEARCH_BASE_DN: "dc=example,dc=org" APP_LDAP_SEARCH_BASE_DN: "dc=example,dc=org"
APP_LDAP_SEARCH_FILTER: "(&(objectClass=*)(uid=*))" APP_LDAP_SEARCH_FILTER: "(&(objectClass=*)(uid=*))"
APP_LDAP_SYNC_LOOP: "false" APP_LDAP_SYNC_INTERVAL_SECONDS: 10
vaultwarden: {} vaultwarden:
ldap: ldap:
command: ["--copy-service"] command: ["--copy-service"]
volumes: volumes:
- ./itest/ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom - ./itest/50-seed-user.ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom/50-seed-user.ldif

View File

@ -1,12 +1,11 @@
--- ---
version: '3'
services: services:
ldap_sync: ldap_sync:
volumes:
- ./itest/config.toml:/config.toml:ro
vaultwarden: {} vaultwarden:
ldap: ldap:
command: ["--copy-service"] command: ["--copy-service"]
volumes: volumes:
- ./itest/ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom - ./itest/50-seed-user.ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom/50-seed-user.ldif

View File

@ -37,9 +37,9 @@ pub fn read_config_from_file() -> Result<Config, String> {
let config_path = get_config_path(); let config_path = get_config_path();
let contents = fs::read_to_string(&config_path) let contents = fs::read_to_string(&config_path)
.map_err(|err| format!("Failed to open config file at {}: {}", config_path, err))?; .map_err(|_| format!("Failed to open config file at {}", config_path))?;
let config: Config = toml::from_str(contents.as_str()) let config: Config = toml::from_str(contents.as_str())
.map_err(|err| format!("Failed to parse config file at {}: {}", config_path, err))?; .map_err(|_| format!("Failed to parse config file at {}", config_path))?;
println!("Config read from file at {}", config_path); println!("Config read from file at {}", config_path);
Ok(config) Ok(config)

View File

@ -1,7 +1,5 @@
extern crate anyhow; extern crate anyhow;
extern crate ldap3; extern crate ldap3;
extern crate pledge;
extern crate unveil;
use std::collections::HashSet; use std::collections::HashSet;
use std::thread::sleep; use std::thread::sleep;
@ -11,8 +9,6 @@ use anyhow::Context as _;
use anyhow::Error as AnyError; use anyhow::Error as AnyError;
use anyhow::Result; use anyhow::Result;
use ldap3::{DerefAliases, LdapConn, LdapConnSettings, Scope, SearchEntry, SearchOptions}; use ldap3::{DerefAliases, LdapConn, LdapConnSettings, Scope, SearchEntry, SearchOptions};
use pledge::pledge;
use unveil::unveil;
mod config; mod config;
mod vw_admin; mod vw_admin;
@ -25,16 +21,6 @@ fn main() {
config.get_vaultwarden_root_cert_file(), config.get_vaultwarden_root_cert_file(),
); );
unveil(config::get_config_path(), "r")
.or_else(unveil::Error::ignore_platform)
.expect("Could not unveil config file");
unveil("", "")
.or_else(unveil::Error::ignore_platform)
.expect("Could not disable further unveils");
pledge("dns flock inet rpath stdio tty", "")
.or_else(pledge::Error::ignore_platform)
.expect("Could not pledge permissions");
invite_users(&config, &mut client, config.get_ldap_sync_loop()) invite_users(&config, &mut client, config.get_ldap_sync_loop())
} }
@ -82,7 +68,7 @@ fn ldap_client(
let settings = LdapConnSettings::new() let settings = LdapConnSettings::new()
.set_starttls(starttls) .set_starttls(starttls)
.set_no_tls_verify(no_tls_verify); .set_no_tls_verify(no_tls_verify);
let mut ldap = LdapConn::with_settings(settings, ldap_url.as_str()) let ldap = LdapConn::with_settings(settings, ldap_url.as_str())
.context("Failed to connect to LDAP server")?; .context("Failed to connect to LDAP server")?;
ldap.simple_bind(bind_dn.as_str(), bind_pw.as_str()) ldap.simple_bind(bind_dn.as_str(), bind_pw.as_str())
.context("Could not bind to LDAP server")?; .context("Could not bind to LDAP server")?;
@ -92,7 +78,7 @@ fn ldap_client(
/// Retrieves search results from ldap /// Retrieves search results from ldap
fn search_entries(config: &config::Config) -> Result<Vec<SearchEntry>, AnyError> { fn search_entries(config: &config::Config) -> Result<Vec<SearchEntry>, AnyError> {
let mut ldap = ldap_client( let ldap = ldap_client(
config.get_ldap_url(), config.get_ldap_url(),
config.get_ldap_bind_dn(), config.get_ldap_bind_dn(),
config.get_ldap_bind_password(), config.get_ldap_bind_password(),
@ -104,13 +90,13 @@ fn search_entries(config: &config::Config) -> Result<Vec<SearchEntry>, AnyError>
let mail_field = config.get_ldap_mail_field(); let mail_field = config.get_ldap_mail_field();
let fields = vec!["uid", "givenname", "sn", "cn", mail_field.as_str()]; let fields = vec!["uid", "givenname", "sn", "cn", mail_field.as_str()];
// Something something error handling // TODO: Something something error handling
let (results, _res) = ldap let (results, _res) = ldap
.with_search_options(SearchOptions::new().deref(DerefAliases::Always)) .with_search_options(SearchOptions::new().deref(DerefAliases::Always))
.search( .search(
config.get_ldap_search_base_dn().as_str(), &config.get_ldap_search_base_dn().as_str(),
Scope::Subtree, Scope::Subtree,
config.get_ldap_search_filter().as_str(), &config.get_ldap_search_filter().as_str(),
fields, fields,
) )
.context("LDAP search failure")? .context("LDAP search failure")?

View File

@ -2,7 +2,7 @@ extern crate reqwest;
extern crate serde; extern crate serde;
extern crate thiserror; extern crate thiserror;
use reqwest::blocking::Response; use reqwest::Response;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::File; use std::fs::File;
@ -23,9 +23,9 @@ pub enum ResponseError {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct User { pub struct User {
#[serde(alias = "Email")] #[serde(rename = "Email")]
email: String, email: String,
#[serde(rename = "_status", alias = "_Status")] #[serde(rename = "_Status")]
status: i32, status: i32,
} }
@ -72,9 +72,8 @@ impl Client {
reqwest::Certificate::from_der(&buf).expect("Could not load DER root cert file") reqwest::Certificate::from_der(&buf).expect("Could not load DER root cert file")
} }
fn get_http_client(&self) -> reqwest::blocking::Client { fn get_http_client(&self) -> reqwest::Client {
let mut client = let mut client = reqwest::Client::builder().redirect(reqwest::RedirectPolicy::none());
reqwest::blocking::Client::builder().redirect(reqwest::redirect::Policy::none());
if !&self.root_cert_file.is_empty() { if !&self.root_cert_file.is_empty() {
let cert = self.get_root_cert(); let cert = self.get_root_cert();