Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 50 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Build Multi-Arch Docker Image
name: Test and Release Pipeline

on:
workflow_dispatch:
Expand All @@ -11,12 +11,46 @@
tags:
- "v*"

pull_request:
branches:
- main

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'

- name: Install bats
run: npm install -g bats

- name: Fetch latest BWDC version
id: bwdc_version
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION=$(gh release view --repo bitwarden/directory-connector \
--json tagName --jq '.tagName | ltrimstr("v")')
echo "version=${VERSION}" >> $GITHUB_OUTPUT

- name: Build image for integration tests
run: docker build --build-arg BWDC_VERSION=${{ steps.bwdc_version.outputs.version }} -t bwdc-test .

- name: Run tests
run: ./tests/run_tests.sh

build:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
strategy:
matrix:
include:
Expand All @@ -37,14 +71,21 @@

- name: Set version
id: version
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ -n "${{ inputs.BWDC_VERSION }}" ]; then
echo "version=${{ inputs.BWDC_VERSION }}" >> $GITHUB_OUTPUT
elif [ "${{ github.event_name }}" = "pull_request" ]; then
VERSION=$(gh release view --repo bitwarden/directory-connector \
--json tagName --jq '.tagName | ltrimstr("v")')
echo "version=${VERSION}" >> $GITHUB_OUTPUT
else
echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
fi

- name: Log in to registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
Expand All @@ -68,15 +109,17 @@
platforms: ${{ matrix.platform }}
build-args: |
BWDC_VERSION=${{ steps.version.outputs.version }}
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}

- name: Export digest
if: github.event_name != 'pull_request'
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"

- name: Upload digest
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v4
with:
name: digests-${{ steps.platform.outputs.name }}
Expand All @@ -86,7 +129,10 @@

merge:
runs-on: ubuntu-latest
needs: build
needs:
- build
- test
if: github.event_name != 'pull_request'

permissions:
contents: read
Expand Down Expand Up @@ -126,4 +172,4 @@

- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
data.json
.env
node_modules/
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build stage
FROM node:24-bookworm AS builder
FROM node:20-bookworm AS builder

ARG BWDC_VERSION=2026.4.0

Expand Down
Empty file modified healthcheck.sh
100644 → 100755
Empty file.
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "bitwarden-dc-docker",
"version": "1.0.0",
"description": "Running the Bitwarden Directory Connector on Docker",
"homepage": "https://github.com/acm-uiuc/bitwarden-dc-docker#readme",
"bugs": {
"url": "https://github.com/acm-uiuc/bitwarden-dc-docker/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/acm-uiuc/bitwarden-dc-docker.git"
},
"license": "ISC",
"author": "",
"type": "commonjs",
"main": "index.js",
"directories": {
"test": "tests"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
}
10 changes: 10 additions & 0 deletions tests/fixtures/mock_bwdc.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/sh
# Records every invocation for assertion in tests
echo "$*" >> /tmp/bwdc_calls
case "$1" in
--version) echo "bwdc-mock 0.0.0"; exit 0 ;;
login) exit 0 ;;
sync) exit 0 ;;
config) exit 0 ;;
*) exit 0 ;;
esac
220 changes: 220 additions & 0 deletions tests/integration/container.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
#!/usr/bin/env bats

# Set IMAGE_NAME in env to skip the local build (e.g. when testing a pre-built image).
IMAGE="${IMAGE_NAME:-bwdc-test}"
FIXTURES="$BATS_TEST_DIRNAME/../fixtures"
CONFIG_PATH="/home/bitwarden/.config/Bitwarden Directory Connector/data.json"

setup_file() {
chmod +x "$FIXTURES/mock_bwdc.sh"
if [ -z "$IMAGE_NAME" ]; then
docker build -t "$IMAGE" "$BATS_TEST_DIRNAME/../.." >&2
fi
}

# ---------------------------------------------------------------------------
# Image / binary sanity checks (no credentials needed)
# ---------------------------------------------------------------------------

@test "bwdc binary is present and executable" {
run docker run --rm --entrypoint /bin/sh "$IMAGE" \
-c "test -x /usr/local/bin/bwdc && echo ok"
[ "$status" -eq 0 ]
[ "$output" = "ok" ]
}

@test "container runs as non-root bitwarden user" {
run docker run --rm --entrypoint /bin/sh "$IMAGE" -c "id -un"
[ "$status" -eq 0 ]
[ "$output" = "bitwarden" ]
}

@test "BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS is true" {
run docker run --rm --entrypoint /bin/sh "$IMAGE" \
-c 'echo "$BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS"'
[ "$status" -eq 0 ]
[ "$output" = "true" ]
}

@test "healthcheck.sh is present and executable" {
run docker run --rm --entrypoint /bin/sh "$IMAGE" \
-c "test -x /usr/local/bin/healthcheck.sh && echo ok"
[ "$status" -eq 0 ]
[ "$output" = "ok" ]
}

# ---------------------------------------------------------------------------
# Entrypoint behaviour
# ---------------------------------------------------------------------------

@test "entrypoint exits 1 with an error when data.json is not mounted" {
run docker run --rm "$IMAGE"
[ "$status" -eq 1 ]
[[ "$output" =~ "Configuration file not found" ]]
}

@test "sync loop starts and writes heartbeat file" {
local cid
cid=$(docker run -d \
-v "$FIXTURES/mock_bwdc.sh:/usr/local/bin/bwdc" \
-v "$FIXTURES/data.json:$CONFIG_PATH" \
-e SYNC_INTERVAL_MIN=60 \
"$IMAGE")

# Poll up to 15s for the heartbeat to appear
local i=0
while [ "$i" -lt 15 ]; do
docker exec "$cid" test -f /tmp/bwdc-heartbeat 2>/dev/null && break
sleep 1
i=$((i + 1))
done

run docker exec "$cid" test -f /tmp/bwdc-heartbeat
docker rm -f "$cid" >/dev/null 2>&1
[ "$status" -eq 0 ]
}

@test "healthcheck passes after successful sync" {
local cid
cid=$(docker run -d \
-v "$FIXTURES/mock_bwdc.sh:/usr/local/bin/bwdc" \
-v "$FIXTURES/data.json:$CONFIG_PATH" \
-e SYNC_INTERVAL_MIN=60 \
"$IMAGE")

# Wait for the sync loop to complete at least one iteration
local i=0
while [ "$i" -lt 15 ]; do
docker exec "$cid" test -f /tmp/bwdc-heartbeat 2>/dev/null && break
sleep 1
i=$((i + 1))
done

run docker exec "$cid" /usr/local/bin/healthcheck.sh
docker rm -f "$cid" >/dev/null 2>&1
[ "$status" -eq 0 ]
}

@test "BW_SERVER env var causes bwdc config server to be called" {
local cid
cid=$(docker run -d \
-v "$FIXTURES/mock_bwdc.sh:/usr/local/bin/bwdc" \
-v "$FIXTURES/data.json:$CONFIG_PATH" \
-e BW_SERVER=https://bw.example.com \
-e SYNC_INTERVAL_MIN=60 \
"$IMAGE")

sleep 8

run docker exec "$cid" cat /tmp/bwdc_calls
docker rm -f "$cid" >/dev/null 2>&1
[ "$status" -eq 0 ]
[[ "$output" =~ "config server https://bw.example.com" ]]
}

@test "BW_DIRECTORY_TYPE env var causes bwdc config directory to be called" {
local cid
cid=$(docker run -d \
-v "$FIXTURES/mock_bwdc.sh:/usr/local/bin/bwdc" \
-v "$FIXTURES/data.json:$CONFIG_PATH" \
-e BW_DIRECTORY_TYPE=0 \
-e SYNC_INTERVAL_MIN=60 \
"$IMAGE")

sleep 8

run docker exec "$cid" cat /tmp/bwdc_calls
docker rm -f "$cid" >/dev/null 2>&1
[ "$status" -eq 0 ]
[[ "$output" =~ "config directory 0" ]]
}

@test "BW_DIRECTORY_KEY with ldap type calls bwdc config ldap.password" {
local cid
cid=$(docker run -d \
-v "$FIXTURES/mock_bwdc.sh:/usr/local/bin/bwdc" \
-v "$FIXTURES/data.json:$CONFIG_PATH" \
-e BW_DIRECTORY_TYPE=0 \
-e BW_DIRECTORY_KEY=supersecret \
-e SYNC_INTERVAL_MIN=60 \
"$IMAGE")

sleep 8

run docker exec "$cid" cat /tmp/bwdc_calls
docker rm -f "$cid" >/dev/null 2>&1
[ "$status" -eq 0 ]
[[ "$output" =~ "config ldap.password supersecret" ]]
}

@test "BW_DIRECTORY_KEY with azure type (numeric) calls bwdc config azure.key" {
local cid
cid=$(docker run -d \
-v "$FIXTURES/mock_bwdc.sh:/usr/local/bin/bwdc" \
-v "$FIXTURES/data.json:$CONFIG_PATH" \
-e BW_DIRECTORY_TYPE=1 \
-e BW_DIRECTORY_KEY=my-azure-secret \
-e SYNC_INTERVAL_MIN=60 \
"$IMAGE")

sleep 8

run docker exec "$cid" cat /tmp/bwdc_calls
docker rm -f "$cid" >/dev/null 2>&1
[ "$status" -eq 0 ]
[[ "$output" =~ "config azure.key my-azure-secret" ]]
}

@test "BW_DIRECTORY_KEY with azure type (string) calls bwdc config azure.key" {
local cid
cid=$(docker run -d \
-v "$FIXTURES/mock_bwdc.sh:/usr/local/bin/bwdc" \
-v "$FIXTURES/data.json:$CONFIG_PATH" \
-e BW_DIRECTORY_TYPE=azure \
-e BW_DIRECTORY_KEY=my-azure-secret \
-e SYNC_INTERVAL_MIN=60 \
"$IMAGE")

sleep 8

run docker exec "$cid" cat /tmp/bwdc_calls
docker rm -f "$cid" >/dev/null 2>&1
[ "$status" -eq 0 ]
[[ "$output" =~ "config azure.key my-azure-secret" ]]
}

@test "azure sync: bwdc config directory is set to type 1" {
local cid
cid=$(docker run -d \
-v "$FIXTURES/mock_bwdc.sh:/usr/local/bin/bwdc" \
-v "$FIXTURES/data.json:$CONFIG_PATH" \
-e BW_DIRECTORY_TYPE=1 \
-e BW_DIRECTORY_KEY=my-azure-secret \
-e SYNC_INTERVAL_MIN=60 \
"$IMAGE")

sleep 8

run docker exec "$cid" cat /tmp/bwdc_calls
docker rm -f "$cid" >/dev/null 2>&1
[ "$status" -eq 0 ]
[[ "$output" =~ "config directory 1" ]]
[[ "$output" =~ "config azure.key my-azure-secret" ]]
[[ "$output" =~ "sync" ]]
}

@test "bwdc sync is called during the sync loop" {
local cid
cid=$(docker run -d \
-v "$FIXTURES/mock_bwdc.sh:/usr/local/bin/bwdc" \
-v "$FIXTURES/data.json:$CONFIG_PATH" \
-e SYNC_INTERVAL_MIN=60 \
"$IMAGE")

sleep 8

run docker exec "$cid" cat /tmp/bwdc_calls
docker rm -f "$cid" >/dev/null 2>&1
[ "$status" -eq 0 ]
[[ "$output" =~ "sync" ]]
}
Loading
Loading