Compare commits

..

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

17 changed files with 1302 additions and 1099 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
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v2
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- uses: actions/setup-python@v5
- uses: actions/setup-python@v2
- name: Run pre-commit hooks
uses: pre-commit/action@v3.0.1
env:
SKIP: hadolint
- name: Run hadolint
uses: hadolint/hadolint-action@v3.1.0
uses: pre-commit/action@v2.0.2

View File

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

View File

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

2157
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,4 +1,7 @@
FROM rust:1.80 as builder
ARG BUILD_TAG=1.46
ARG RUN_TAG=$BUILD_TAG
FROM rust:$BUILD_TAG as builder
WORKDIR /usr/src/
RUN USER=root cargo new --bin vaultwarden_ldap
@ -9,17 +12,13 @@ COPY Cargo.toml Cargo.lock ./
RUN cargo build --locked --release
# Remove bins to make sure we rebuild
# hadolint ignore=DL3059
RUN rm ./target/release/deps/vaultwarden_ldap*
# Copy source and install
COPY src ./src
RUN cargo build --release
# Use most recent ubuntu LTS release
FROM ubuntu:24.04
RUN apt-get update \
&& apt-get install -y --no-install-recommends 'ca-certificates=20240203' 'libssl-dev=3.*' \
&& rm -rf /var/cache/apt/lists
FROM rust:$RUN_TAG
WORKDIR /app
COPY --from=builder /usr/src/vaultwarden_ldap/target/release/vaultwarden_ldap /usr/local/bin/
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,41 +34,18 @@ test:
cargo 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
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
.PHONY: itest-env
itest-env:
docker-compose -f docker-compose.yml \
-f itest/docker-compose.itest-env.yml \
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
up --build
.PHONY: clean-itest
clean-itest:
@ -94,8 +71,12 @@ clean:
rm -f ./target
.PHONY: docker-build-all
docker-build-all: docker-build
docker-build-all: docker-build docker-build-alpine
.PHONY: docker-build
docker-build:
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
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
@ -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_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
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
There are no unit tests, but there are integration tests that require manual verification.
### 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.
All testing is manual right now. First step is to set up Bitwarden and the LDAP server.
```bash
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:
docker-compose -f docker-compose.yml -f itest/docker-compose.itest.yml up --build
## Future
* Any kind of proper logging
* Tests

View File

@ -10,7 +10,7 @@ services:
# ./root.cert:/usr/src/vaultwarden_ldap/root.cert:ro
environment:
CONFIG_PATH: /config.toml
RUST_BACKTRACE: full
RUST_BACKTRACE: 1
depends_on:
- vaultwarden
- ldap
@ -24,7 +24,6 @@ services:
ADMIN_TOKEN: admin
SIGNUPS_ALLOWED: 'false'
INVITATIONS_ALLOWED: 'true'
I_REALLY_WANT_VOLATILE_STORAGE: 'true'
ldap:
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
# Version: 1.2.5
version: 1
# Entry 1: Users group
# Entry 1: cn=Users,dc=example,dc=org
dn: cn=Users,dc=example,dc=org
cn: Users
gidnumber: 500
objectclass: posixGroup
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
cn: Someone
gidnumber: 500
@ -23,7 +29,7 @@ sn: Someone
uid: someone
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
cn: SomeoneNoEmail
gidnumber: 500
@ -35,15 +41,13 @@ sn: SomeoneNoEmail
uid: someonenoemail
uidnumber: 1001
# Entry 4: User with email containing +
dn: cn=SomeonePlus,cn=Users,dc=example,dc=org
cn: SomeonePlus
gidnumber: 500
homedirectory: /home/users/someoneplus
mail: test+plus@example.com
objectclass: inetOrgPerson
objectclass: posixAccount
objectclass: top
sn: SomeonePlus
uid: someoneplus
uidnumber: 1002
# Entry 4: cn=SomeoneNoEmailNoUid,cn=Users,dc=example,dc=org
# dn: cn=SomeoneNoEmailNoUid,cn=Users,dc=example,dc=org
# cn: SomeoneNoEmailNoUid
# gidnumber: 500
# homedirectory: /home/users/someonenoemailnoUid
# objectclass: inetOrgPerson
# objectclass: posixAccount
# objectclass: top
# sn: SomeoneNoEmail
# uidnumber: 1002

View File

@ -11,9 +11,9 @@ services:
APP_LDAP_BIND_PASSWORD: "admin"
APP_LDAP_SEARCH_BASE_DN: "dc=example,dc=org"
APP_LDAP_SEARCH_FILTER: "(&(objectClass=*)(uid=*))"
APP_LDAP_SYNC_LOOP: "false"
APP_LDAP_SYNC_INTERVAL_SECONDS: 10
vaultwarden: {}
vaultwarden:
ldap:
command: ["--copy-service"]

View File

@ -2,10 +2,8 @@
version: '3'
services:
ldap_sync:
volumes:
- ./itest/config.toml:/config.toml:ro
vaultwarden: {}
vaultwarden:
ldap:
command: ["--copy-service"]

View File

@ -37,9 +37,9 @@ pub fn read_config_from_file() -> Result<Config, String> {
let config_path = get_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())
.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);
Ok(config)

View File

@ -1,7 +1,5 @@
extern crate anyhow;
extern crate ldap3;
extern crate pledge;
extern crate unveil;
use std::collections::HashSet;
use std::thread::sleep;
@ -11,8 +9,6 @@ use anyhow::Context as _;
use anyhow::Error as AnyError;
use anyhow::Result;
use ldap3::{DerefAliases, LdapConn, LdapConnSettings, Scope, SearchEntry, SearchOptions};
use pledge::pledge;
use unveil::unveil;
mod config;
mod vw_admin;
@ -25,16 +21,6 @@ fn main() {
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())
}
@ -82,7 +68,7 @@ fn ldap_client(
let settings = LdapConnSettings::new()
.set_starttls(starttls)
.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")?;
ldap.simple_bind(bind_dn.as_str(), bind_pw.as_str())
.context("Could not bind to LDAP server")?;
@ -92,7 +78,7 @@ fn ldap_client(
/// Retrieves search results from ldap
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_bind_dn(),
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 fields = vec!["uid", "givenname", "sn", "cn", mail_field.as_str()];
// Something something error handling
// TODO: Something something error handling
let (results, _res) = ldap
.with_search_options(SearchOptions::new().deref(DerefAliases::Always))
.search(
config.get_ldap_search_base_dn().as_str(),
&config.get_ldap_search_base_dn().as_str(),
Scope::Subtree,
config.get_ldap_search_filter().as_str(),
&config.get_ldap_search_filter().as_str(),
fields,
)
.context("LDAP search failure")?

View File

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