Skip to content
Merged
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
94 changes: 71 additions & 23 deletions .github/workflows/build-and-publish-ldap-sync.yml
Original file line number Diff line number Diff line change
@@ -1,36 +1,51 @@
name: build-and-publish-ldap-sync
name: build-and-publish

on:
push:
branches: [ "main", "dev" ]
branches: [ "main", "dev", "v*" ]
tags: [ "v*" ]
paths:
- "ldap-sync/**"
- ".github/workflows/build-and-publish-ldap-sync.yml"
tags:
- "ldap-sync-v*"
- "VERSION"
- "MAKEFILE"
- "ldap-sync-full-stack-dev_docker-compose.yml"
- "ldap-sync-full-stack-dev.env"
pull_request:
branches: [ "main" ]
paths:
- "ldap-sync/**"
- ".github/workflows/build-and-publish-ldap-sync.yml"
- "VERSION"
- "MAKEFILE"
- "ldap-sync-full-stack-dev_docker-compose.yml"
- "ldap-sync-full-stack-dev.env"

jobs:
docker:
runs-on: ubuntu-latest
permissions:
contents: write
contents: write # create releases
packages: write
id-token: write
id-token: write # for provenance
env:
IMAGE: modomofn/ldap-sync
CONTEXT_DIR: ldap-sync
DOCKERFILE: ldap-sync/Dockerfile
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Go cache (optional)
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

Expand All @@ -47,19 +62,51 @@ jobs:
id: vars
shell: bash
run: |
set -euo pipefail

SHA="${GITHUB_SHA::7}"
echo "sha=$SHA" >> $GITHUB_OUTPUT

# channel tags for branches
if [[ "$GITHUB_REF" == "refs/heads/main" ]]; then
echo "channel=latest" >> $GITHUB_OUTPUT
elif [[ "$GITHUB_REF" == "refs/heads/dev" ]]; then
echo "channel=edge" >> $GITHUB_OUTPUT
VERSION_FILE="VERSION"
VERSION=""
if [[ -f "$VERSION_FILE" ]]; then
VERSION=$(tr -d '\r\n' < "$VERSION_FILE")
VERSION=${VERSION#v}
fi
if [[ -n "$VERSION" ]]; then
echo "version_plain=$VERSION" >> $GITHUB_OUTPUT
fi

case "$GITHUB_REF" in
refs/heads/main)
echo "channel=latest" >> $GITHUB_OUTPUT
if [[ -n "$VERSION" ]]; then
echo "version_tag=v$VERSION" >> $GITHUB_OUTPUT
fi
;;
refs/heads/dev)
echo "channel=edge" >> $GITHUB_OUTPUT
if [[ -n "$VERSION" ]]; then
echo "dev_version_tag=v${VERSION}-dev-$SHA" >> $GITHUB_OUTPUT
fi
;;
refs/heads/v[0-9]*)
BRANCH_VERSION="${GITHUB_REF#refs/heads/}"
if [[ -n "$VERSION" ]]; then
echo "version_tag=v$VERSION" >> $GITHUB_OUTPUT
else
echo "version_tag=$BRANCH_VERSION" >> $GITHUB_OUTPUT
fi
if [[ "$BRANCH_VERSION" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "ver=$BRANCH_VERSION" >> $GITHUB_OUTPUT
echo "major=v${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT
echo "minor=v${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" >> $GITHUB_OUTPUT
fi
;;
esac

# semver if tag matches ldap-sync-vX.Y.Z
if [[ "$GITHUB_REF" =~ refs/tags/ldap-sync-v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
VER="${BASH_REMATCH[0]#refs/tags/ldap-sync-}" # vX.Y.Z
if [[ "$GITHUB_REF" =~ refs/tags/v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
VER="${BASH_REMATCH[0]#refs/tags/}"
MAJOR="v${BASH_REMATCH[1]}"
MINOR="v${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
echo "ver=$VER" >> $GITHUB_OUTPUT
Expand All @@ -70,23 +117,24 @@ jobs:
- name: Build and push (multi-arch)
uses: docker/build-push-action@v6
with:
context: ${{ env.CONTEXT_DIR }}
file: ${{ env.DOCKERFILE }}
context: ldap-sync
file: ldap-sync/Dockerfile
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
provenance: true
sbom: true
tags: |
${{ env.IMAGE }}:${{ steps.vars.outputs.sha }}
${{ steps.vars.outputs.channel && format('{0}:{1}', env.IMAGE, steps.vars.outputs.channel) || '' }}
${{ steps.vars.outputs.version_tag && format('{0}:{1}', env.IMAGE, steps.vars.outputs.version_tag) || '' }}
${{ steps.vars.outputs.dev_version_tag && format('{0}:{1}', env.IMAGE, steps.vars.outputs.dev_version_tag) || '' }}
${{ steps.vars.outputs.ver && format('{0}:{1}', env.IMAGE, steps.vars.outputs.ver) || '' }}
${{ steps.vars.outputs.minor && format('{0}:{1}', env.IMAGE, steps.vars.outputs.minor) || '' }}
${{ steps.vars.outputs.major && format('{0}:{1}', env.IMAGE, steps.vars.outputs.major) || '' }}

- name: Create GitHub Release (for ldap-sync tags)
if: startsWith(github.ref, 'refs/tags/ldap-sync-v')

- name: Create GitHub Release (for tags)
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: ldap-sync ${{ github.ref_name }}
generate_release_notes: true
generate_release_notes: true
94 changes: 92 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,92 @@
# ldap-sync
Companion app for modom-ofn/auth-portal
# ldap-sync (AuthPortal Companion)

[![Docker Pulls](https://img.shields.io/docker/pulls/modomofn/ldap-sync.svg)](https://hub.docker.com/r/modomofn/ldap-sync)
[![Docker Image Size](https://img.shields.io/docker/image-size/modomofn/ldap-sync/latest)](https://hub.docker.com/r/modomofn/ldap-sync)
[![Go Version](https://img.shields.io/badge/Go-1.25.3%2B-00ADD8?logo=go)](https://go.dev/)
[![License: GPL-3.0](https://img.shields.io/badge/License-GPL3.0-green.svg)](https://github.com/modom-ofn/ldap-sync?tab=GPL-3.0-1-ov-file#readme)
[![Vibe Coded](https://img.shields.io/badge/Vibe_Coded-OpenAI_Codex-purple)](https://developers.openai.com/codex/windows)

**ldap-sync** is the lightweight bridge that keeps your directory in lockstep with [AuthPortal](https://github.com/modom-ofn/auth-portal). It speaks Postgres on one side and LDAP on the other, exporting only the users that AuthPortal has approved for downstream apps.

> [!NOTE]
> Before diving in, make sure AuthPortal is up and humming. ldap-sync simply mirrors its source of truth.

> [!IMPORTANT]
> **Use at your own risk.** This project leans on Vibe Coding practices - AI pair-programming, automated refactors, and rapid iteration. Treat releases as starting points - test, monitor, and adapt to your stack. AuthPortal and ldap-sync remains an independent effort with no endorsement from Plex, Emby, or Jellyfin.

> [!NOTE]
> - Docker Hub: https://hub.docker.com/r/modomofn/ldap-sync
> - GitHub Repo: https://github.com/modom-ofn/ldap-sync

---

## Why it exists

- **Downstream SSO without duct tape**
Sync AuthPortal's authorized media users into LDAP so legacy services (Grafana, Jenkins, whatever) can reuse the same access decisions.

- **Multi-provider aware**
Ships the full identities matrix (Plex, Jellyfin, Emby) right into LDAP `description` attributes for cross-reference. Works with every AuthPortal `v2.0.x` release, including the latest [v2.0.3](https://github.com/modom-ofn/auth-portal/).

- **Zero-click OU bootstrap**
Creates the target OU branch at startup if it is missing, so fresh installs do not require manual LDIF prep.

- **Container ready**
Single binary, stateless. Point at your Postgres + LDAP endpoints, mount env vars, and let it stream changes on schedule (cron, systemd, Kubernetes Job, you pick).

---

## How it works

1. **Connect** to the AuthPortal database using `DATABASE_URL` (defaults to the docker-compose setup).
2. **Read** either the `identities` table (multi-provider era) or the legacy `users` table, trimming to rows with `media_access = TRUE`.
3. **Bind** to LDAP via `LDAP_HOST` and `LDAP_ADMIN_DN`, optionally StartTLS.
4. **Ensure** the base OU exists. ldap-sync will create `ou=users` under your domain component if needed.
5. **Upsert** each entry as `inetOrgPerson`, writing email + provider metadata into LDAP attributes. Existing entries get patched; missing ones are added.

All operations run within sane timeouts and log progress in plain English so you can wire it into any orchestrator.

---

## Configuration snapshot

| Variable | Default | Purpose |
| --- | --- | --- |
| `DATABASE_URL` | `postgres://authportal:change-me@postgres:5432/authportaldb?sslmode=disable` | AuthPortal Postgres connection string |
| `LDAP_HOST` | `ldap://openldap:389` | Target LDAP endpoint (supports `ldaps://`) |
| `LDAP_ADMIN_DN` | `cn=admin,dc=authportal,dc=local` | Bind DN |
| `LDAP_ADMIN_PASSWORD` | *(empty)* | Bind credential |
| `BASE_DN` | `ou=users,dc=authportal,dc=local` | Where entries are stored/created |
| `LDAP_STARTTLS` | `false` | Enable StartTLS negotiation |

Add your own scheduler (cron, k8s job, GitHub Actions, etc.) to run the binary as often as you need.

---

## Getting Started

```bash
# build the container
docker build -t modomofn/ldap-sync:latest ./ldap-sync

# run against a local stack
docker run --rm \
-e DATABASE_URL=postgres://authportal:change-me@postgres:5432/authportaldb?sslmode=disable \
-e LDAP_HOST=ldap://openldap:389 \
-e LDAP_ADMIN_DN="cn=admin,dc=authportal,dc=local" \
-e LDAP_ADMIN_PASSWORD=supersecret \
modomofn/ldap-sync:dev
```

When the run completes you will see logs for any adds/updates. Rerun anytime AuthPortal grants or revokes access.

---

## Need the portal itself?

ldap-sync is a supporting actor. The feature-rich headliner lives here: **[AuthPortal on GitHub](https://github.com/modom-ofn/auth-portal)**. Start there to bootstrap your auth stack, then drop ldap-sync in to keep everything else in sync.

---

> [!IMPORTANT]
> **Use at your own risk.** This project leans on Vibe Coding practices - AI pair-programming, automated refactors, and rapid iteration. Treat releases as starting points - test, monitor, and adapt to your stack. AuthPortal and ldap-sync remains an independent effort with no endorsement from Plex, Emby, or Jellyfin.
2 changes: 1 addition & 1 deletion ldap-sync/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# ---- builder ----
FROM golang:1.23.12-alpine3.21 AS build
FROM golang:1.25.3-alpine3.21 AS build
WORKDIR /src

# copy only go.mod first (no go.sum yet)
Expand Down
2 changes: 1 addition & 1 deletion ldap-sync/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.0.1
2.0.3
12 changes: 10 additions & 2 deletions ldap-sync/go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
module ldap-sync

go 1.23.8
toolchain go1.23.12
go 1.25

toolchain go1.25.3

require (
github.com/go-ldap/ldap/v3 v3.4.6
github.com/lib/pq v1.10.9
)

require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/google/uuid v1.3.1 // indirect
golang.org/x/crypto v0.13.0 // indirect
)
65 changes: 65 additions & 0 deletions ldap-sync/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.6 h1:ert95MdbiG7aWo/oPYp9btL3KJlMPKnP58r09rI8T+A=
github.com/go-ldap/ldap/v3 v3.4.6/go.mod h1:IGMQANNtxpsOzj7uUAMjpGBaOVTC4DYyIy8VsTdxmtc=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Loading