Compare commits

..

24 Commits

Author SHA1 Message Date
Chris F Ravenscroft 5147d1165a refactor: table-drive batteryOfTests to cut duplication
The twelve battery cases were near-identical closures: the same ldapsearch
argument prefix, the same got/want compare, the same t.Fatalf shape. Collapse
them into table rows holding only what differs (args, want, message) and run
them through one loop.

Shared argument builders (bind/svc/otp) remove the repeated
-LLL/-H/-D/-w/-x prefix; the two getSecond cases set a line field instead of
duplicating the assertion block.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdP1CLtdwAtAeXuS4hph6g
2026-08-30 16:20:05 -07:00
Chris F Ravenscroft 374041c358 chore: bump CI to 1.25 2026-08-30 16:12:11 -07:00
Chris F Ravenscroft 5bdeb5118b fix: remove repetitions, reduce waitForPort scope, restore arbitrary port 2026-08-30 16:02:46 -07:00
Timmy Welch 319cc9a3c7 Modernize tests, reduce execution time from 30s to 0.2s
Make output go to test logger so that go test -v will give useful output on a failure
Remove dependence on `oathtool`
Remove dependence on `ls`
Wait on killing the running glauth instance so subsequent tests don't fail
Wait for glauth to provide authentication error instead of waiting an arbitrary amount of time
2026-08-05 16:16:10 -07:00
Chris F Ravenscroft 26cd4447c8 test: support distribution binary layout 2026-07-25 14:17:38 -07:00
Chris F Ravenscroft 96a283fea2 chore: fix release build ordering 2026-07-25 14:14:49 -07:00
Chris F Ravenscroft c07a1c978c ci: release 2.5.2 (#475) 2026-07-25 13:38:12 -07:00
Chris F Ravenscroft c1c57c62e9 fix: configure 2.5.2 release 2026-07-25 13:35:53 -07:00
Chris F Ravenscroft 700745e640 feat: check plugin compatibility at release time 2026-07-25 13:34:44 -07:00
Chris F Ravenscroft 1c497d76ba ci: release 2.5.1 (#472)
* ci: release 2.5.1

* chore: set version to v2.5.1
2026-07-18 15:20:33 -07:00
Chris F Ravenscroft cf0eb7f43f Pr 470 (#473)
* fix: reject disabled users on all backends in Bind

A user with disabled = true could still bind on the config backend, since
configHandler.FindUser and LDAPOpsHelper.Bind never checked Disabled while the
SQL backends drop such users at lookup. Enforce it in Bind so every backend
behaves the same.

* fix: use configHandler.Bind rather than helper.Bind in test harness

---------

Co-authored-by: kanywst <niwatakuma@icloud.com>
2026-07-18 14:59:42 -07:00
kt fb65713fbd fix: reject disabled users on all backends in Bind (#470)
A user with disabled = true could still bind on the config backend, since
configHandler.FindUser and LDAPOpsHelper.Bind never checked Disabled while the
SQL backends drop such users at lookup. Enforce it in Bind so every backend
behaves the same.
2026-07-18 14:53:08 -07:00
Chris F Ravenscroft cb121cdc2c fix: team atlanta arbitrary password (#471) 2026-07-18 14:32:05 -07:00
Jörn Friedrich Dreyer 4b2b1398b8 Fix typo in contributing section of README (#468) 2026-07-18 13:31:54 -07:00
Chris F Ravenscroft be396ae635 ci: release 2.5.0 (#454) 2026-04-19 14:18:14 -07:00
Chris F Ravenscroft 719f6a2a2d chore: upgrade to distroless debian13 2026-04-19 14:12:37 -07:00
Chris F Ravenscroft ff7a9e6215 chore: force release 2.5.0
Release-As: 2.5.0
2026-04-19 12:25:51 -07:00
Chris F Ravenscroft 45185dafd1 chore: release 2.5.0 2026-04-19 12:21:27 -07:00
Chris F Ravenscroft aedc0f2867 chore: update readme (#467)
* Initial support for RISC-V

Add build for riscv64/linux; not available for darwin and windows.

* chore: update readme

---------

Co-authored-by: E Shattow <eshattow@users.noreply.github.com>
2026-04-19 12:18:08 -07:00
E Shattow 0774c95c1a Initial support for RISC-V (#465)
feat: Add build for riscv64/linux — not available for darwin and windows.
2026-04-19 11:56:24 -07:00
E Shattow 9e04c8b756 Use dynamic GOOS and GOARCH in integration-test.sh (#464)
Drop hard-coded linuxamd64 from path in v2/scripts/ci/integration-test.sh and use instead dynamic GOOS and GOARCH env.
2026-04-19 11:46:45 -07:00
E Shattow d3df7252ec Bump runner to Ubuntu 24.04 LTS (#462) 2026-04-19 11:39:38 -07:00
Chris F Ravenscroft 5123a3da8e chore: upgrade release-please to v4 syntax 2026-04-19 11:38:15 -07:00
Chris F Ravenscroft eabf67e266 chore: debug, openldap, samba (#466)
new target: builddebug
active directory proxying
QoL ldifs for openldap testing
2026-04-19 11:26:13 -07:00
19 changed files with 701 additions and 189 deletions
+2 -2
View File
@@ -7,10 +7,10 @@ on:
- master
jobs:
build:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
strategy:
matrix:
go-version: [1.21.x]
go-version: [1.25.x]
steps:
- name: Install Go
uses: actions/setup-go@v4
+5 -12
View File
@@ -1,5 +1,6 @@
name: release-please
on:
pull_request:
push:
branches:
- master
@@ -8,20 +9,12 @@ permissions:
contents: write
pull-requests: write
name: release-please
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: google-github-actions/release-please-action@v3
- uses: google-github-actions/release-please-action@v4
with:
release-type: go
package-name: GLAuth
path: v2
default-branch: master
pull-request-title-pattern: "ci: release ${version}"
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
extra-files: |
v2/internal/version/const.go
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
+3
View File
@@ -0,0 +1,3 @@
{
"v2": "2.5.2"
}
+7 -8
View File
@@ -1,14 +1,14 @@
# GLAuth: LDAP authentication server for developers
Go-lang LDAP Authentication (GLAuth) is a secure, easy-to-use, LDAP server w/ configurable backends.
[![Gitter](https://badges.gitter.im/glauth/community.svg)](https://gitter.im/glauth/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Matrix](https://img.shields.io/badge/chat-%2fjoin%20%23glauth_community:gitter.im-green)](hey)
# GLAuth: LDAP authentication server for developers
[![Official GLAuth Website](https://img.shields.io/badge/Official%20GLAuth%20Website-glauth.github.io-brightgreen?style=plastic)](https://glauth.github.io/)
[![kittens dns](https://img.shields.io/badge/kittens%20dns-github-blue?style=plastic)](https://github.com/fusion/kittendns)
[![wing vpn](https://img.shields.io/badge/wing%20vpn-github-orange?style=plastic)](https://github.com/fusion/wing-vpn)
Go-lang LDAP Authentication (GLAuth) is a secure, easy-to-use, LDAP server w/ configurable backends.
![GitHub all releases](https://img.shields.io/github/downloads/glauth/glauth/total)
![Docker pulls](https://badgen.net/docker/pulls/glauth/glauth)
![GitHub last commit (branch)](https://img.shields.io/github/last-commit/glauth/glauth/master)
![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability-percentage/glauth/glauth)
* Centrally manage accounts across your infrastructure
* Centrally manage SSH keys, Linux accounts, and passwords for cloud servers.
@@ -21,7 +21,7 @@ Use it to centralize account management across your Linux servers, your OSX mach
### Contributing
- Please base all Pull Requests on [dev](https://github.com/glauth/glauth/tree/dev), not master.
- Format your code autonmatically using `gofmt -d ./` before committing
- Format your code automatically using `gofmt -d ./` before committing
### Quickstart
This quickstart is a great way to try out GLAuth in a non-production environment. *Be warned that you should take the extra steps to setup SSL (TLS) for production use!*
@@ -110,4 +110,3 @@ For advanced users, GLAuth supports pluggable backends. Currently, it can use a
# Stargazers over time
[![Stargazers over time](https://starchart.cc/glauth/glauth.svg)](https://starchart.cc/glauth/glauth)
+1 -3
View File
@@ -1,5 +1,3 @@
go 1.21.0
toolchain go1.21.5
go 1.25.0
use ./v2
+13
View File
@@ -0,0 +1,13 @@
{
"packages": {
"v2": {
"release-type": "go",
"package-name": "GLAuth",
"pull-request-title-pattern": "ci: release ${version}",
"extra-files": [
"internal/version/const.go"
]
}
},
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
}
+55
View File
@@ -1,5 +1,60 @@
# Changelog
## [2.5.2](https://github.com/glauth/glauth/compare/GLAuth-v2.5.1...GLAuth-v2.5.2) (2026-07-25)
### Features
* check plugin compatibility at release time ([700745e](https://github.com/glauth/glauth/commit/700745e640c5371361a58ef3c90245a4637c5224))
## [2.5.1](https://github.com/glauth/glauth/compare/GLAuth-v2.5.0...GLAuth-v2.5.1) (2026-07-18)
### Bug Fixes
* reject disabled users on all backends in Bind ([#470](https://github.com/glauth/glauth/issues/470)) ([fb65713](https://github.com/glauth/glauth/commit/fb65713fbdfd626cb033675fb42724a5fb3d06ff))
* team atlanta arbitrary password ([#471](https://github.com/glauth/glauth/issues/471)) ([cb121cd](https://github.com/glauth/glauth/commit/cb121cdc2c9b831abdf7831835ec30ead2536a7a))
## [2.5.0](https://github.com/glauth/glauth/compare/GLAuth-v2.5.0...GLAuth-v2.5.0) (2026-04-19)
### Features
* allow tracing configuration via main config ([f692394](https://github.com/glauth/glauth/commit/f692394942aa0f93a1aa1572331fd1c3e3553156))
* alt container build registry ([f8dee8d](https://github.com/glauth/glauth/commit/f8dee8da879232f5c6ae2a2a19565562c7d0d9fd))
* embedded plugins ([c0ed908](https://github.com/glauth/glauth/commit/c0ed90822fd4d2ee7104a1979d423a0c93d758ac))
* introduce context for otlp spans into handler pkg ([d05630f](https://github.com/glauth/glauth/commit/d05630f66b80a776fd406782ad1fde5c6c66eac6))
* introduce context for otlp spans into plugins pkg ([46e49b6](https://github.com/glauth/glauth/commit/46e49b6976318a9f3670b88bdcb3411dfac0a17c))
* introduce legacy behavior, based on past versions ([#318](https://github.com/glauth/glauth/issues/318)) ([34a6b30](https://github.com/glauth/glauth/commit/34a6b30b8fe69c90cab09eb85890ffdfe8fc80fd))
* introduce otelsql ([2ca5312](https://github.com/glauth/glauth/commit/2ca53126965aa7d42b23aee15f750df12822d454))
* introduce otlp tracer ([0cf0403](https://github.com/glauth/glauth/commit/0cf04037a2a7b38c8ed7af2451b115f48c5427b5))
* proxying ldap paging compat ([#422](https://github.com/glauth/glauth/issues/422)) ([8476506](https://github.com/glauth/glauth/commit/8476506b8c51e993eb90c73d612f69fec5350b89))
* starttls and ldaps now matching features ([#458](https://github.com/glauth/glauth/issues/458)) ([d68c78a](https://github.com/glauth/glauth/commit/d68c78a18b150679038e5ce549b6b227c31518d7))
* wire up basic tracer ([1c2b23c](https://github.com/glauth/glauth/commit/1c2b23c00ff85b83a6d2e4bf4a9a68081aaf2777))
### Bug Fixes
* 246 and [#252](https://github.com/glauth/glauth/issues/252) ([ceefc66](https://github.com/glauth/glauth/commit/ceefc66dbba9ae054958269825de31d22d30e262))
* config watcher now updates whenf ile added to dir ([#424](https://github.com/glauth/glauth/issues/424)) ([a90f37b](https://github.com/glauth/glauth/commit/a90f37b23b76af22c700392d68bf227b7aee1894))
* Credentials leaked on INFO log level ([#451](https://github.com/glauth/glauth/issues/451)) ([645dbb2](https://github.com/glauth/glauth/commit/645dbb226683ae57f3d5382e21ea42444e940752))
* drop vendored toml ([24455e3](https://github.com/glauth/glauth/commit/24455e39889716ce7ae1e7e8a7dacfa7d4c96080))
* formatting ([c90cbfe](https://github.com/glauth/glauth/commit/c90cbfe5fb090fc19a55d64e01cd0e31c38bf2f0))
* go test not checking otp within allowed basedn ([#403](https://github.com/glauth/glauth/issues/403)) ([ed52a91](https://github.com/glauth/glauth/commit/ed52a91ec4117ff58fe606a1d8ba10786501a1e5))
* move all toml parsing into a new internal package, drop the mappings in favour of toml.Primitive decoding ([3ba8e11](https://github.com/glauth/glauth/commit/3ba8e1113217be647d240261322453d213ea7da4))
* patch group name fetches for the rest of handlers ([#450](https://github.com/glauth/glauth/issues/450)) ([b498736](https://github.com/glauth/glauth/commit/b498736cfdd0ee6f1585d682a5c54c9d927b8dbb))
* remove spinlock in monitoring code ([#405](https://github.com/glauth/glauth/issues/405)) ([a2c151b](https://github.com/glauth/glauth/commit/a2c151b0d025332462369a846a51ef4deff5332b))
* removed config setup from main, reshoring log configuration ([5fe8aca](https://github.com/glauth/glauth/commit/5fe8aca852bdfe7e375ea99b87074bae35fc3407))
* update ldap library to fix [#389](https://github.com/glauth/glauth/issues/389) ([#430](https://github.com/glauth/glauth/issues/430)) ([a5ecc26](https://github.com/glauth/glauth/commit/a5ecc26e064cea9e4dab3dd234ebd5d5eb06be0e))
* update tracing code to work with breaking otlp 1.20 changes ([1a37396](https://github.com/glauth/glauth/commit/1a3739610997b58100040d0c8a405596fccc8e23))
* upgrade to use BurntSushi/toml ([f9addbc](https://github.com/glauth/glauth/commit/f9addbc2c5b13ccc2779dbabc4c55bc8706f53d6))
### Miscellaneous Chores
* force release 2.5.0 ([ff7a9e6](https://github.com/glauth/glauth/commit/ff7a9e6215857e0dd3450504b7b063409ccfc374))
* release 2.3.1 ([0bf3d4a](https://github.com/glauth/glauth/commit/0bf3d4a82a8451e7bbda74e3730345aab5a855a7))
## [2.4.0](https://github.com/glauth/glauth/compare/v2.3.2...v2.4.0) (2024-08-04)
+56 -17
View File
@@ -2,7 +2,7 @@
MAKEFLAGS += --no-print-directory
VERSION=$(shell bin/linuxamd64/glauth --version)
VERSION=$(shell if [ -x bin/linuxamd64/glauth ]; then bin/linuxamd64/glauth --version; else bin/glauth-linux-amd64 --version; fi)
GIT_COMMIT=$(shell git rev-list -1 HEAD )
BUILD_TIME=$(shell date -u +%Y%m%d_%H%M%SZ)
@@ -24,8 +24,8 @@ TRIM_FLAGS>?=-trimpath
BUILD_TAGS?=noembed
# Targets
MAIN_TARGETS?=linux/amd64,linux/386,linux/arm64,linux/arm-7,darwin/amd64,darwin/arm64,windows/amd64,windows/386
PLUGIN_TARGETS?=linux/amd64,linux/386,linux/arm64,linux/arm-7,darwin/amd64,darwin/arm64
MAIN_TARGETS?=linux/amd64,linux/386,linux/arm64,linux/arm-7,linux/riscv64,darwin/amd64,darwin/arm64,windows/amd64,windows/386
PLUGIN_TARGETS?=linux/amd64,linux/386,linux/arm64,linux/arm-7,linux/riscv64,darwin/amd64,darwin/arm64
EMBED_TARGETS?=windows/amd64
# For release process
@@ -58,10 +58,9 @@ mocks:
vet:
-go vet ./...
.PHONY: vet
#test: @ runs the integration test on linuxamd64 (eventually allow the binary to be set)
#test: @ runs package, root integration, and shell integration tests
test: mocks vet
# also run unit tests for packages, skip glauth_test.go for now
go test -v -cover -coverprofile coverage.out ./pkg/... ./internal/...
go test -v -cover -coverprofile coverage.out ./pkg/... ./internal/... .
$(MAKE) runtest
#all: @ run build process for all binaries
all: setup binaries verify
@@ -73,7 +72,7 @@ fast: setup linuxamd64
fastdebug: setup linuxamd64debug
#binaries: @ list of binary formats to build
binaries: linux386 linuxamd64 linuxarm linuxarm64 darwinamd64 darwinarm64 win386 winamd64
binaries: linux386 linuxamd64 linuxarm linuxarm64 linuxriscv64 darwinamd64 darwinarm64 win386 winamd64
#setup: @ setup commands to always run
setup: getdeps format
@@ -120,6 +119,39 @@ format:
devrun:
go run ${BUILD_FILES} -c sample-simple.cfg
#testplugin: @ start GLAuth with a dynamically loaded plugin and stop it cleanly
GLAUTH_BIN?=bin/glauth-linux-amd64
PLUGIN_BIN?=bin/sqlite-linux-amd64.so
PLUGIN_CONFIG?=pkg/plugins/glauth-sqlite/sample-database.cfg
PLUGIN_LDAP_PORT?=13893
PLUGIN_API_PORT?=15555
testplugin:
@set -e; \
test -x "$(GLAUTH_BIN)"; \
test -r "$(PLUGIN_BIN)"; \
test -f "$(PLUGIN_CONFIG)"; \
tmpdir=$$(mktemp -d); \
trap 'rm -rf "$$tmpdir"' EXIT; \
sed \
-e 's#^[[:space:]]*plugin = .*# plugin = "$(abspath $(PLUGIN_BIN))"#' \
-e "s#^[[:space:]]*database = .*# database = \"$$tmpdir/gl.db\"#" \
-e 's#listen = "0.0.0.0:3893"#listen = "127.0.0.1:$(PLUGIN_LDAP_PORT)"#' \
-e 's#listen = "0.0.0.0:5555"#listen = "127.0.0.1:$(PLUGIN_API_PORT)"#' \
"$(PLUGIN_CONFIG)" > "$$tmpdir/config.cfg"; \
"$(GLAUTH_BIN)" -c "$$tmpdir/config.cfg" > "$$tmpdir/glauth.log" 2>&1 & \
pid=$$!; \
sleep 2; \
if ! kill -0 "$$pid" 2>/dev/null; then \
set +e; wait "$$pid"; status=$$?; set -e; \
cat "$$tmpdir/glauth.log"; \
exit $$status; \
fi; \
kill -TERM "$$pid"; \
set +e; wait "$$pid"; status=$$?; set -e; \
cat "$$tmpdir/glauth.log"; \
exit $$status
mkbindir:
@echo "create directory bin/$(GOOS)$(GOARCH)"
@mkdir -p bin/$(GOOS)$(GOARCH)
@@ -154,6 +186,9 @@ linuxarm:
linuxarm64:
GOOS=linux GOARCH=arm64 $(MAKE) build
linuxriscv64:
GOOS=linux GOARCH=riscv64 $(MAKE) build
testing:
mkdir -p pkg/embed && \
cat pkg/plugins/glauth-sqlite/sqlite.go | sed -e 's/func main() {}//' -e 's/^package main/package embed/' > pkg/embed/sqlite.go && \
@@ -174,7 +209,7 @@ winamd64:
GOOS=windows GOARCH=amd64 $(MAKE) build
verify:
@for binary in linux386 linuxamd64 linuxarm linuxarm64 darwinamd64 darwinarm64 win386 winamd64; do cd bin/$$binary && sha256sum glauth.sha256 -c && cd ../..; done
@for binary in linux386 linuxamd64 linuxarm linuxarm64 linuxriscv64 darwinamd64 darwinarm64 win386 winamd64; do cd bin/$$binary && sha256sum glauth.sha256 -c && cd ../..; done
pull-plugin-dependencies:
(for pkg in $$(cat $$M/go.mod | awk '/^require [a-z]/{print $$2} /^\)/{rblock=0} rblock{if($$4!="indirect" && $$1!~"glauth"){print $$1}} /^require \(/{rblock=1}'); do if [ "$$(grep $$pkg go.mod &>/dev/null)" = "" ]; then go get $$pkg; fi; done)
@@ -266,32 +301,36 @@ _checkdockerauth:
builddockermain:
$(if $(TAG),,$(error ❌ Must set TAG))
$(if $(REPO),,$(error ❌ Must set REPO - glauth or other))
@mkdir -p docker/assets/linux/amd64 docker/assets/linux/arm64 docker/assets/linux/arm/v7 && \
@mkdir -p docker/assets/linux/amd64 docker/assets/linux/arm64 docker/assets/linux/arm/v7 docker/assets/linux/riscv64 && \
cp -f bin/glauth-linux-amd64 docker/assets/linux/amd64/glauth && \
cp -f bin/glauth-linux-arm64 docker/assets/linux/arm64/glauth && \
cp -f bin/glauth-linux-arm-7 docker/assets/linux/arm/v7/glauth && \
docker buildx build --tag ghcr.io/$$REPO/glauth:$$TAG -t ghcr.io/$$REPO/glauth:latest -f docker/Dockerfile-standalone --platform linux/amd64,linux/arm64,linux/arm/v7 --push docker && \
docker buildx build --tag $$REPO/glauth:$$TAG -t $$REPO/glauth:latest -f docker/Dockerfile-standalone --platform linux/amd64,linux/arm64,linux/arm/v7 --push docker
cp -f bin/glauth-linux-riscv64 docker/assets/linux/riscv64/glauth && \
docker buildx build --tag ghcr.io/$$REPO/glauth:$$TAG -t ghcr.io/$$REPO/glauth:latest -f docker/Dockerfile-standalone --platform linux/amd64,linux/arm64,linux/arm/v7,linux/riscv64 --push docker && \
docker buildx build --tag $$REPO/glauth:$$TAG -t $$REPO/glauth:latest -f docker/Dockerfile-standalone --platform linux/amd64,linux/arm64,linux/arm/v7,linux/riscv64 --push docker
builddockerplugins:
$(if $(TAG),,$(error ❌ Must set TAG))
$(if $(REPO),,$(error ❌ Must set REPO - glauth or other))
@mkdir -p docker/assets/linux/amd64 docker/assets/linux/arm64 docker/assets/linux/arm/v7 && \
@mkdir -p docker/assets/linux/amd64 docker/assets/linux/arm64 docker/assets/linux/arm/v7 docker/assets/linux/riscv64 && \
cp -f bin/sqlite-linux-amd64.so docker/assets/linux/amd64/sqlite.so && \
cp -f bin/sqlite-linux-arm64.so docker/assets/linux/arm64/sqlite.so && \
cp -f bin/sqlite-linux-arm-7.so docker/assets/linux/arm/v7/sqlite.so && \
cp -f bin/sqlite-linux-riscv64.so docker/assets/linux/riscv64/sqlite.so && \
cp -f bin/mysql-linux-amd64.so docker/assets/linux/amd64/mysql.so && \
cp -f bin/mysql-linux-arm64.so docker/assets/linux/arm64/mysql.so && \
cp -f bin/mysql-linux-arm-7.so docker/assets/linux/arm/v7/mysql.so && \
cp -f bin/mysql-linux-riscv64.so docker/assets/linux/riscv64/mysql.so && \
cp -f bin/postgres-linux-amd64.so docker/assets/linux/amd64/postgres.so && \
cp -f bin/postgres-linux-arm64.so docker/assets/linux/arm64/postgres.so && \
cp -f bin/postgres-linux-arm-7.so docker/assets/linux/arm/v7/postgres.so && \
docker buildx build --tag ghcr.io/$$REPO/glauth-plugins:$$TAG -t ghcr.io/$$REPO/glauth-plugins:latest -f docker/Dockerfile-plugins --platform linux/amd64,linux/arm64,linux/arm/v7 --push docker && \
docker buildx build --tag $$REPO/glauth-plugins:$$TAG -t $$REPO/glauth-plugins:latest -f docker/Dockerfile-plugins --platform linux/amd64,linux/arm64,linux/arm/v7 --push docker
cp -f bin/postgres-linux-riscv64.so docker/assets/linux/riscv64/postgres.so && \
docker buildx build --tag ghcr.io/$$REPO/glauth-plugins:$$TAG -t ghcr.io/$$REPO/glauth-plugins:latest -f docker/Dockerfile-plugins --platform linux/amd64,linux/arm64,linux/arm/v7,linux/riscv64 --push docker && \
docker buildx build --tag $$REPO/glauth-plugins:$$TAG -t $$REPO/glauth-plugins:latest -f docker/Dockerfile-plugins --platform linux/amd64,linux/arm64,linux/arm/v7,linux/riscv64 --push docker
builddocker: _checkdockerauth builddockermain builddockerplugins
buildeverything: prepbuild forget-embed _buildmain _buildplugins remember-embed _buildmainembeds builddocker
buildeverything: prepbuild forget-embed _buildplugins _buildmain testplugin remember-embed _buildmainembeds builddocker
#testdocker: @ run integration test using docker
testdocker:
@@ -335,6 +374,6 @@ testdocker:
.PHONY: all \
prepbuild run test fast binaries setup getdeps runtest updatetest format devrun \
linux386 linuxamd64 linuxarm linuxarm64 darwinamd64 darwinarm64 win386 winamd64 verify \
linux386 linuxamd64 linuxarm linuxarm64 linuxriscv64 darwinamd64 darwinarm64 win386 winamd64 verify \
pull-plugin pull-base-plugins forget-plugin forget-plugins buildmain buildplugin build \
buildplugins builddockermain builddockerplugins builddocker testdocker buildeverything
buildplugins builddockermain builddockerplugins builddocker testdocker testplugin buildeverything
+1 -1
View File
@@ -10,7 +10,7 @@ RUN apk add --no-cache git make busybox-static dumb-init
# Run Step
#################
FROM gcr.io/distroless/base-debian11 as run
FROM gcr.io/distroless/base-debian13 as run
LABEL maintainers="Ben Yanke <ben@benyanke.com>, Jörn Friedrich Dreyer <jfd@butonic.de>, Chris F Ravenscroft <chris@voilaweb.com>" \
org.opencontainers.image.title="GLauth" \
+1 -1
View File
@@ -10,7 +10,7 @@ RUN apk add --no-cache git make busybox-static dumb-init
# Run Step
#################
FROM gcr.io/distroless/base-debian11 as run
FROM gcr.io/distroless/base-debian13 as run
LABEL maintainers="Ben Yanke <ben@benyanke.com>, Jörn Friedrich Dreyer <jfd@butonic.de>, Chris F Ravenscroft <chris@voilaweb.com>" \
org.opencontainers.image.title="GLauth" \
+296 -140
View File
@@ -1,11 +1,20 @@
package main
import (
"bytes"
"errors"
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/glauth/ldap"
"github.com/pquerna/otp/totp"
)
type testEnv struct {
@@ -23,10 +32,31 @@ type testEnv struct {
checkemployeetype string
}
func glauthBinary() string {
if binary := os.Getenv("GLAUTH_TEST_BINARY"); binary != "" {
return binary
}
qualified := filepath.Join("bin", runtime.GOOS+runtime.GOARCH, "glauth")
if _, err := os.Stat(qualified); err == nil {
return qualified
}
distribution := filepath.Join("bin", "glauth-"+runtime.GOOS+"-"+runtime.GOARCH)
if runtime.GOOS == "windows" {
distribution += ".exe"
}
if _, err := os.Stat(distribution); err == nil {
return distribution
}
return "bin/glauth"
}
func TestProperBuild(t *testing.T) {
info, err := os.Stat("bin/glauth")
info, err := os.Stat(glauthBinary())
if err != nil {
t.Error(err)
t.Fatal(err)
}
mode := uint32(info.Mode())
if mode&0b001001001 == 0 {
@@ -34,147 +64,229 @@ func TestProperBuild(t *testing.T) {
}
}
func TestConfigBackendRejectsPasswordlessUserBinds(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
port := listener.Addr().(*net.TCPAddr).Port
listener.Close()
configPath := filepath.Join(t.TempDir(), "passwordless.cfg")
config := fmt.Sprintf(`debug = false
[ldap]
enabled = true
listen = "127.0.0.1:%d"
tls = false
[ldaps]
enabled = false
[backend]
datastore = "config"
baseDN = "dc=glauth,dc=com"
[behaviors]
LimitFailedBinds = false
[[groups]]
name = "svc"
gidnumber = 5501
[[users]]
name = "nopass"
uidnumber = 5001
primarygroup = 5501
[[users.capabilities]]
action = "search"
object = "*"
`, port)
if err := os.WriteFile(configPath, []byte(config), 0o600); err != nil {
t.Fatal(err)
}
svc := startSvc(t, fmt.Sprintf("127.0.0.1:%d", port), glauthBinary(), "-c", configPath)
defer stopSvc(svc)
bindDN := "cn=nopass,dc=glauth,dc=com"
for _, tc := range []struct {
name string
password string
want string
}{
{name: "arbitrary password", password: "definitely-wrong", want: "exit status 49"},
{name: "empty password", password: "", want: "exit status 53"},
} {
t.Run(tc.name, func(t *testing.T) {
got := runAndSelect(t, getFirst, "ldapsearch", "-LLL", "-H", fmt.Sprintf("ldap://127.0.0.1:%d", port), "-D", bindDN, "-w", tc.password, "-x", "-b", "dc=glauth,dc=com", "(cn=nopass)")
if got != tc.want {
t.Fatalf("bind result = %q, want %q", got, tc.want)
}
})
}
}
func ldapOnlyConfig(t *testing.T, source string) string {
t.Helper()
data, err := os.ReadFile(source)
if err != nil {
t.Fatal(err)
}
contents := string(data)
sectionMarker := "\n[ldaps]\n"
markerStart := strings.Index(contents, sectionMarker)
if markerStart < 0 {
t.Fatalf("%s has no [ldaps] section", source)
}
start := markerStart + 1
relEnd := strings.Index(contents[start+len("[ldaps]"):], "\n[")
if relEnd < 0 {
t.Fatalf("%s has no section after [ldaps]", source)
}
end := start + len("[ldaps]") + relEnd
section := contents[start:end]
sectionWithoutLDAPS := strings.Replace(section, "enabled = true", "enabled = false", 1)
if section == sectionWithoutLDAPS {
t.Fatalf("%s [ldaps] section does not enable LDAPS", source)
}
contents = contents[:start] + sectionWithoutLDAPS + contents[end:]
path := filepath.Join(t.TempDir(), filepath.Base(source))
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
return path
}
func batteryOfTests(t *testing.T, env *testEnv) {
// We aren't checking that "github.com/pquerna/otp/totp" creates correct values.
// That's what their own tests are for.
// We are checking that the totp code gets through glauth code successfully
otpvalue, err := totp.GenerateCode("3hnvnk4ycv44glzigd6s25j4dougs3rk", time.Now())
if err != nil {
t.Fatal("Failed to generate totp code:", err)
}
const addr = "ldap://127.0.0.1:3893"
bind := func(dn, password string, rest ...string) []string {
return append([]string{"-LLL", "-H", addr, "-D", dn, "-w", password, "-x"}, rest...)
}
svc := func(rest ...string) []string { return bind(env.svcdn, "mysecret", rest...) }
otp := func(password string) []string {
return bind(env.otpdn, password, "-bou=superheros,dc=glauth,dc=com", "cn=hackers")
}
const (
inSuperheros = "should find them in the 'superheros' group"
listFromUser = "should get a list starting with the 'hackers' user"
badCreds = "should get 'Invalid credentials(49)'"
)
tests := []struct {
Name string
Path string
Check func(t testing.TB)
name string
skip bool
args []string
line func(string) string
want string
msg string
}{
{
Name: "searching for the 'hackers' user",
Check: func(t testing.TB) {
out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "cn=hackers")
if got, want := out, env.expectedaccount; got != want {
t.Fatalf("should find them in the 'superheros' group\ngot: %s\nwant: %s", got, want)
}
},
name: "searching for the 'hackers' user",
args: svc("-bdc=glauth,dc=com", "cn=hackers"),
want: env.expectedaccount,
msg: inSuperheros,
},
{
Name: "searching for the 'hackers' user without binding with a group",
Check: func(t testing.TB) {
if env.svcdnnogroup == "" {
t.SkipNow()
}
out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdnnogroup, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "cn=hackers")
if got, want := out, env.expectedaccount; got != want {
t.Fatalf("should find them in the 'superheros' group\ngot: %s\nwant: %s", got, want)
}
},
name: "searching for the 'hackers' user without binding with a group",
skip: env.svcdnnogroup == "",
args: bind(env.svcdnnogroup, "mysecret", "-bdc=glauth,dc=com", "cn=hackers"),
want: env.expectedaccount,
msg: inSuperheros,
},
{
Name: "searching for the 'hackers' user after binding using the account's UPN",
Check: func(t testing.TB) {
if !env.checkbindUPN {
t.SkipNow()
}
out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", "serviceuser@example.com", "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "cn=hackers")
if got, want := out, env.expectedaccount; got != want {
t.Fatalf("should find them in the 'superheros' group\ngot: %s\nwant: %s", got, want)
}
},
name: "searching for the 'hackers' user after binding using the account's UPN",
skip: !env.checkbindUPN,
args: bind("serviceuser@example.com", "mysecret", "-bdc=glauth,dc=com", "cn=hackers"),
want: env.expectedaccount,
msg: inSuperheros,
},
{
Name: "querying the root SDE",
Check: func(t testing.TB) {
out := doRunGetSecond(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-s", "base", "(objectclass=*)")
if got, want := out, env.expectedinfo; got != want {
t.Fatalf("should get some meta information\ngot: %s\nwant: %s", got, want)
}
},
name: "querying the root SDE",
args: svc("-s", "base", "(objectclass=*)"),
line: getSecond,
want: env.expectedinfo,
msg: "should get some meta information",
},
{
Name: "querying the root SDE anonymously without authorizing in config file",
Check: func(t testing.TB) {
if !env.checkanonymousrootDSE {
t.SkipNow()
}
out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-x", "-s", "base", "(objectclass=*)")
if got, want := out, "exit status 50"; got != want {
t.Fatalf("should get error 50\ngot: %s\nwant: %s", got, want)
}
},
name: "querying the root SDE anonymously without authorizing in config file",
skip: !env.checkanonymousrootDSE,
args: []string{"-LLL", "-H", addr, "-x", "-s", "base", "(objectclass=*)"},
want: "exit status 50",
msg: "should get error 50",
},
{
Name: "enumerating posix groups",
Check: func(t testing.TB) {
out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "(objectclass=posixgroup)")
if got, want := out, env.expectedgroup; got != want {
t.Fatalf("should get a list starting with the 'superheros' group\ngot: %s\nwant: %s", got, want)
}
},
name: "enumerating posix groups",
args: svc("-bdc=glauth,dc=com", "(objectclass=posixgroup)"),
want: env.expectedgroup,
msg: "should get a list starting with the 'superheros' group",
},
{
Name: "searching for members of the 'superheros' group",
Check: func(t testing.TB) {
out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "(memberOf=ou=superheros,ou=groups,dc=glauth,dc=com)")
if got, want := out, env.expectedfirstaccount; got != want {
t.Fatalf("should get a list starting with the 'hackers' user\ngot: %s\nwant: %s", got, want)
}
},
name: "searching for members of the 'superheros' group",
args: svc("-bdc=glauth,dc=com", "(memberOf=ou=superheros,ou=groups,dc=glauth,dc=com)"),
want: env.expectedfirstaccount,
msg: listFromUser,
},
{
Name: "performing a complex search for members of 'superheros' group",
Check: func(t testing.TB) {
out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", "(&(objectClass=*)(memberOf=ou=superheros,ou=groups,dc=glauth,dc=com))")
if got, want := out, env.expectedfirstaccount; got != want {
t.Fatalf("should get a list starting with the 'hackers' user\ngot: %s\nwant: %s", got, want)
}
},
name: "performing a complex search for members of 'superheros' group",
args: svc("-bdc=glauth,dc=com", "(&(objectClass=*)(memberOf=ou=superheros,ou=groups,dc=glauth,dc=com))"),
want: env.expectedfirstaccount,
msg: listFromUser,
},
{
Name: "searching for the 'hacker' user using a TOTP-enabled account",
Check: func(t testing.TB) {
if !env.checkTOTP {
t.SkipNow()
}
otpvalue := doRunGetFirst(RD, "oathtool", "--totp", "-b", "-d", "6", "3hnvnk4ycv44glzigd6s25j4dougs3rk")
out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.otpdn, "-w", "mysecret"+otpvalue, "-x", "-bou=superheros,dc=glauth,dc=com", "cn=hackers")
if got, want := out, env.scopedaccount; got != want {
t.Fatalf("should find them in in the 'superheros' group\ngot: %s\nwant: %s", got, want)
}
},
name: "searching for the 'hacker' user using a TOTP-enabled account",
skip: !env.checkTOTP,
args: otp("mysecret" + otpvalue),
want: env.scopedaccount,
msg: inSuperheros,
},
{
Name: "searching for the 'hacker' user using a TOTP-enabled account and no value",
Check: func(t testing.TB) {
if !env.checkTOTP {
t.SkipNow()
}
out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.otpdn, "-w", "mysecret", "-x", "-bou=superheros,dc=glauth,dc=com", "cn=hackers")
if got, want := out, "exit status 49"; got != want {
t.Fatalf("should get 'Invalid credentials(49)'\ngot: %s\nwant: %s", got, want)
}
},
name: "searching for the 'hacker' user using a TOTP-enabled account and no value",
skip: !env.checkTOTP,
args: otp("mysecret"),
want: "exit status 49",
msg: badCreds,
},
{
Name: "searching for the 'hacker' user using a TOTP-enabled account and the wrong value",
Check: func(t testing.TB) {
if !env.checkTOTP {
t.SkipNow()
}
out := doRunGetFirst(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.otpdn, "-w", "mysecret123456", "-x", "-bou=superheros,dc=glauth,dc=com", "cn=hackers")
if got, want := out, "exit status 49"; got != want {
t.Fatalf("should get 'Invalid credentials(49)'\ngot: %s\nwant: %s", got, want)
}
},
name: "searching for the 'hacker' user using a TOTP-enabled account and the wrong value",
skip: !env.checkTOTP,
args: otp("mysecret123456"),
want: "exit status 49",
msg: badCreds,
},
{
Name: "searching for the 'hacker' user",
Check: func(t testing.TB) {
if env.checkemployeetype == "" {
t.SkipNow()
}
out := doRunGetSecond(RD, "ldapsearch", "-LLL", "-H", "ldap://localhost:3893", "-D", env.svcdn, "-w", "mysecret", "-x", "-bdc=glauth,dc=com", env.checkemployeetype, "employeetype")
if got, want := out, "employeetype: Intern"; got != want {
t.Fatalf("type should be 'Intern'\ngot: %s\nwant: %s", got, want)
}
},
name: "searching for the 'hacker' user",
skip: env.checkemployeetype == "",
args: svc("-bdc=glauth,dc=com", env.checkemployeetype, "employeetype"),
line: getSecond,
want: "employeetype: Intern",
msg: "type should be 'Intern'",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
tc.Check(t)
t.Run(tc.name, func(t *testing.T) {
if tc.skip {
t.SkipNow()
}
line := tc.line
if line == nil {
line = getFirst
}
if got := runAndSelect(t, line, "ldapsearch", tc.args...); got != tc.want {
t.Fatalf("%s\ngot: %s\nwant: %s", tc.msg, got, tc.want)
}
})
}
}
@@ -195,14 +307,15 @@ func TestSampleSimple(t *testing.T) {
checkemployeetype: "cn=hackers",
}
svc := startSvc(SD, "bin/glauth", "-c", "sample-simple.cfg")
svc := startSvc(t, "127.0.0.1:3893", glauthBinary(), "-c", ldapOnlyConfig(t, "sample-simple.cfg"))
batteryOfTests(t, &env)
stopSvc(svc)
}
func TestSQLitePlugin(t *testing.T) {
matchingLibrary := doRunGetFirst(RD, "ls", "bin/sqlite.so")
if matchingLibrary != "bin/sqlite.so" {
_, err := os.Stat("bin/sqlite.so")
if err != nil {
t.Log("Unable to find sqlite plugin")
t.SkipNow()
}
@@ -221,14 +334,20 @@ func TestSQLitePlugin(t *testing.T) {
checkemployeetype: "",
}
svc := startSvc(SD, "bin/glauth", "-c", "pkg/plugins/glauth-sqlite/sample-database.cfg")
svc := startSvc(t, "127.0.0.1:3893", glauthBinary(), "-c", "pkg/plugins/glauth-sqlite/sample-database.cfg")
batteryOfTests(t, &env)
stopSvc(svc)
}
func TestLdapInjection(t *testing.T) {
matchingContainers := doRunGetFirst(RD, "sh", "-c", "docker ps | grep ldap-service | wc -l")
out, err := doRun("sh", "-c", "docker ps | grep ldap-service | wc -l")
if err != nil {
t.Log("Unable to find docker container:", out)
t.SkipNow()
}
matchingContainers := getFirst(out)
if matchingContainers != "1" {
t.Log("Unable to find docker container:", out)
t.SkipNow()
}
@@ -247,52 +366,89 @@ func TestLdapInjection(t *testing.T) {
checkemployeetype: "",
}
svc := startSvc(SD, "bin/glauth", "-c", "sample-ldap-injection.cfg")
svc := startSvc(t, "127.0.0.1:3893", glauthBinary(), "-c", "sample-ldap-injection.cfg")
batteryOfTests(t, &env)
stopSvc(svc)
}
// -----=============================================================================----
const SD = 3 // Start Delay
const RD = 2 // Response Delay
func startSvc(delay time.Duration, name string, arg ...string) *exec.Cmd {
cmd := exec.Command(name, arg...)
cmd.Start()
if delay != 0 {
time.Sleep(time.Second * delay)
func waitForPort(host string, timeout time.Duration) error {
start := time.Now()
for {
conn, err := ldap.Dial("tcp", host)
if err == nil {
err = conn.Bind("_", "_")
if err == nil {
conn.Close()
return fmt.Errorf("Successfully authenticated to glauth. This should not happen")
}
e := &ldap.Error{}
if errors.As(err, &e) && e.ResultCode == 49 {
conn.Close()
return nil
}
conn.Close()
// We have an error, it's probably a connection error or an error saying that ldap isn't setup yet
// try again
}
if time.Since(start) > timeout {
return fmt.Errorf("timeout waiting for port %s: %w", host, err)
}
time.Sleep(50 * time.Millisecond)
}
}
func startSvc(t *testing.T, addr string, name string, arg ...string) *exec.Cmd {
cmd := exec.Command(name, arg...)
cmd.Stderr = t.Output()
cmd.Stdout = t.Output()
if err := cmd.Start(); err != nil {
t.Fatal("Failed to start glauth:", err)
}
if err := waitForPort(addr, time.Second*2); err != nil {
t.Fatal("Failed to wait for glauth to start:", err)
}
// the port is listening but the ldap server may not be accepting connections yet
// TODO: make glauth listen as the last part of the initialization that way as soon as it listens it can process connections
return cmd
}
func stopSvc(svc *exec.Cmd) {
svc.Process.Kill()
svc.Process.Wait()
}
func doRunGetFirst(delay time.Duration, name string, arg ...string) string {
out := strings.SplitN(doRun(delay, name, arg...), "\n", 2)
if out == nil || len(out) < 1 {
func getFirst(output string) string {
out := strings.SplitN(output, "\n", 2)
if len(out) < 1 {
return "*fail*"
}
return out[0]
}
func doRunGetSecond(delay time.Duration, name string, arg ...string) string {
out := strings.SplitN(doRun(delay, name, arg...), "\n", 3)
if out == nil || len(out) < 2 {
func getSecond(output string) string {
out := strings.SplitN(output, "\n", 3)
if len(out) < 2 {
return "*fail*"
}
return out[1]
}
func doRun(delay time.Duration, name string, arg ...string) string {
out, err := exec.Command(name, arg...).Output()
func doRun(name string, arg ...string) (string, error) {
out, err := exec.Command(name, arg...).CombinedOutput()
if err != nil {
return string(bytes.TrimSpace(out)), err
}
return string(bytes.TrimSpace(out)), nil
}
func runAndSelect(t testing.TB, pick func(string) string, name string, arg ...string) string {
t.Helper()
out, err := doRun(name, arg...)
if err != nil {
t.Log(out)
return err.Error()
}
if delay != 0 {
time.Sleep(time.Second * delay)
}
return strings.TrimSpace(string(out))
return pick(out)
}
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/glauth/glauth/v2
go 1.21
go 1.25
require (
github.com/BurntSushi/toml v1.3.2
+2
View File
@@ -15,6 +15,8 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
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/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+1 -1
View File
@@ -2,4 +2,4 @@ package version
// Version is the tag of the latest release, it gets changed by the ci
// process when a release is happening
const Version = "v2.3.1" // x-release-please-version
const Version = "v2.5.2" // x-release-please-version
+69
View File
@@ -0,0 +1,69 @@
package handler
import (
"encoding/hex"
"net"
"testing"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/trace/noop"
"golang.org/x/crypto/bcrypt"
"github.com/glauth/glauth/v2/pkg/config"
"github.com/glauth/ldap"
)
type disabledBindTestMonitor struct{}
func (disabledBindTestMonitor) SetResponseTimeMetric(map[string]string, float64) error {
return nil
}
func (disabledBindTestMonitor) SetLDAPMetric(map[string]string, float64) error {
return nil
}
func TestConfigBackendDisabledUserCannotBind(t *testing.T) {
pw := "correct horse"
raw, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.MinCost)
if err != nil {
t.Fatal(err)
}
hexHash := hex.EncodeToString(raw)
cfg := &config.Config{}
cfg.Users = []config.User{
{Name: "activeuser", PrimaryGroup: 5501, PassBcrypt: hexHash, Disabled: false},
{Name: "disableduser", PrimaryGroup: 5501, PassBcrypt: hexHash, Disabled: true},
}
logger := zerolog.Nop()
tracer := noop.NewTracerProvider().Tracer("f001")
helper := NewLDAPOpsHelper(tracer)
h := configHandler{
backend: config.Backend{
BaseDN: "dc=example,dc=com",
NameFormatAsArray: []string{"cn"},
GroupFormatAsArray: []string{"ou"},
},
log: &logger,
cfg: cfg,
ldohelper: helper,
monitor: disabledBindTestMonitor{},
tracer: tracer,
}
_, server := net.Pipe()
defer server.Close()
bind := func(dn, pass string) ldap.LDAPResultCode {
code, _ := h.Bind(dn, pass, server)
return code
}
if code := bind("cn=activeuser,dc=example,dc=com", pw); code != ldap.LDAPResultSuccess {
t.Fatalf("active user with correct pw should bind, got %d", code)
}
if code := bind("cn=disableduser,dc=example,dc=com", pw); code != ldap.LDAPResultInvalidCredentials {
t.Errorf("disabled user must be rejected; got code %d, want InvalidCredentials(49)", code)
}
}
+2
View File
@@ -121,6 +121,8 @@ func (h ldapHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (result ld
validotp := false
// Local records augment proxy authentication with OTP/YubiKey checks;
// password authentication remains delegated to the upstream LDAP server.
// Find the user
// We are going to go through all backends and ask
// until we find our user or die of boredom.
+26
View File
@@ -65,6 +65,10 @@ func NewLDAPOpsHelper(tracer trace.Tracer) LDAPOpsHelper {
return helper
}
func isUnauthenticatedSimpleBind(bindDN, bindSimplePw string) bool {
return bindDN != "" && bindSimplePw == ""
}
func (l LDAPOpsHelper) Bind(ctx context.Context, h LDAPOpsHandler, bindDN, bindSimplePw string, conn net.Conn) (resultCode ldap.LDAPResultCode, err error) {
ctx, span := l.tracer.Start(ctx, "handler.LDAPOpsHelper.Bind")
defer span.End()
@@ -86,11 +90,24 @@ func (l LDAPOpsHelper) Bind(ctx context.Context, h LDAPOpsHandler, bindDN, bindS
return ldap.LDAPResultSuccess, nil
}
// A non-empty DN with an empty password is LDAP's unauthenticated bind
// mechanism. It must not establish the named user's authorization state.
if isUnauthenticatedSimpleBind(bindDN, bindSimplePw) {
h.GetLog().Info().Str("binddn", bindDN).Str("src", conn.RemoteAddr().String()).Msg("unauthenticated bind rejected")
return ldap.LDAPResultUnwillingToPerform, nil
}
user, ldapcode := l.findUser(ctx, h, bindDN, true /* checkGroup */)
if ldapcode != ldap.LDAPResultSuccess {
return ldapcode, nil
}
// a disabled account must never authenticate, on any backend
if user.Disabled {
h.GetLog().Info().Str("binddn", bindDN).Str("src", conn.RemoteAddr().String()).Msg("Bind attempt on disabled account")
return ldap.LDAPResultInvalidCredentials, nil
}
validotp := false
if len(user.Yubikey) == 0 && len(user.OTPSecret) == 0 {
@@ -175,6 +192,15 @@ func (l LDAPOpsHelper) Bind(ctx context.Context, h LDAPOpsHandler, bindDN, bindS
return ldap.LDAPResultInvalidCredentials, nil
}
// Reaching this point without a configured primary password verifier must
// not be treated as successful authentication. App-password matches and
// custom authentication return above when they succeed.
if user.PassBcrypt == "" && user.PassSHA256 == "" {
h.GetLog().Info().Str("binddn", bindDN).Str("src", conn.RemoteAddr().String()).Msg("invalid credentials")
l.maybePutInTimeout(ctx, h, conn, true)
return ldap.LDAPResultInvalidCredentials, nil
}
// Now, check the pasword hash
if user.PassBcrypt != "" {
decoded, err := hex.DecodeString(user.PassBcrypt)
+141
View File
@@ -0,0 +1,141 @@
package handler
import (
"context"
"crypto/sha256"
"encoding/hex"
"net"
"testing"
"github.com/GeertJohan/yubigo"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/trace"
"github.com/glauth/glauth/v2/pkg/config"
"github.com/glauth/ldap"
)
type bindTestHandler struct {
backend config.Backend
cfg config.Config
log zerolog.Logger
user config.User
}
func (h *bindTestHandler) GetBackend() config.Backend { return h.backend }
func (h *bindTestHandler) GetLog() *zerolog.Logger { return &h.log }
func (h *bindTestHandler) GetCfg() *config.Config { return &h.cfg }
func (h *bindTestHandler) GetYubikeyAuth() *yubigo.YubiAuth {
return nil
}
func (h *bindTestHandler) FindUser(_ context.Context, userName string, _ bool) (bool, config.User, error) {
if userName == h.user.Name {
return true, h.user, nil
}
return false, config.User{}, nil
}
func (h *bindTestHandler) FindGroup(context.Context, string) (bool, config.Group, error) {
return false, config.Group{}, nil
}
func (h *bindTestHandler) FindPosixAccounts(context.Context, string) ([]*ldap.Entry, error) {
return nil, nil
}
func (h *bindTestHandler) FindPosixGroups(context.Context, string) ([]*ldap.Entry, error) {
return nil, nil
}
func sha256Password(password string) string {
hash := sha256.Sum256([]byte(password))
return hex.EncodeToString(hash[:])
}
func TestLDAPOpsHelperBindRejectsUnauthenticatedAndPasswordlessBinds(t *testing.T) {
tests := []struct {
name string
password string
user config.User
wantResult ldap.LDAPResultCode
}{
{
name: "named empty password is unauthenticated bind",
password: "",
wantResult: ldap.LDAPResultUnwillingToPerform,
},
{
name: "arbitrary password without verifier",
password: "definitely-wrong",
wantResult: ldap.LDAPResultInvalidCredentials,
},
{
name: "failed app password does not fall through",
user: config.User{
Name: "nopass",
PassAppSHA256: []string{sha256Password("expected")},
},
password: "definitely-wrong",
wantResult: ldap.LDAPResultInvalidCredentials,
},
{
name: "matching primary password succeeds",
user: config.User{
Name: "nopass",
PassSHA256: sha256Password("correct"),
},
password: "correct",
wantResult: ldap.LDAPResultSuccess,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
user := tt.user
if user.Name == "" {
user.Name = "nopass"
}
h := &bindTestHandler{
backend: config.Backend{
BaseDN: "dc=glauth,dc=com",
NameFormatAsArray: []string{"cn"},
GroupFormatAsArray: []string{"ou"},
},
user: user,
log: zerolog.Nop(),
}
conn, peer := net.Pipe()
defer conn.Close()
defer peer.Close()
helper := NewLDAPOpsHelper(trace.NewNoopTracerProvider().Tracer("bind-test"))
got, err := helper.Bind(context.Background(), h, "cn=nopass,dc=glauth,dc=com", tt.password, conn)
if err != nil {
t.Fatalf("Bind returned error: %v", err)
}
if got != tt.wantResult {
t.Fatalf("Bind result = %v, want %v", got, tt.wantResult)
}
})
}
}
func TestLDAPOpsHelperBindAllowsAnonymousBind(t *testing.T) {
h := &bindTestHandler{
backend: config.Backend{BaseDN: "dc=glauth,dc=com"},
log: zerolog.Nop(),
}
conn, peer := net.Pipe()
defer conn.Close()
defer peer.Close()
helper := NewLDAPOpsHelper(trace.NewNoopTracerProvider().Tracer("bind-test"))
got, err := helper.Bind(context.Background(), h, "", "", conn)
if err != nil {
t.Fatalf("Bind returned error: %v", err)
}
if got != ldap.LDAPResultSuccess {
t.Fatalf("Bind result = %v, want %v", got, ldap.LDAPResultSuccess)
}
}
+19 -3
View File
@@ -24,6 +24,22 @@ echo "Fixing CI_BUILD_DIR"
# This script requires that "$CI_BUILD_DIR" is set to the repo root
GOOS="$(go env GOOS)"
GOARCH="$(go env GOARCH)"
if [[ "$GOARCH" = "arm" ]]; then
DIST_ARCH="arm-$(go env GOARM)"
else
DIST_ARCH="$GOARCH"
fi
GLAUTH_BINARY="$CI_BUILD_DIR/bin/$GOOS$GOARCH/glauth"
if [[ ! -x "$GLAUTH_BINARY" ]]; then
GLAUTH_BINARY="$CI_BUILD_DIR/bin/glauth-$GOOS-$DIST_ARCH"
fi
if [[ ! -x "$GLAUTH_BINARY" ]]; then
echo "Unable to find a runnable GLAuth binary: $GLAUTH_BINARY"
exit 1
fi
# Ensure ldap utils are installed (for example - when running this outside of CI)
if [[ ! `which ldapsearch` ]]; then
sudo apt-get -qq update && sudo apt-get -qq install -y ldap-utils || exit 1;
@@ -33,14 +49,14 @@ fi
echo "";
echo ""
echo "Version string of tested binary:"
"$CI_BUILD_DIR/bin/linuxamd64/glauth" --version
"$GLAUTH_BINARY" --version
echo ""
# Start in background, capture PID
"$CI_BUILD_DIR/bin/linuxamd64/glauth" -c "$CI_BUILD_DIR/scripts/ci/test-config.cfg" &> /dev/null &
"$GLAUTH_BINARY" -c "$CI_BUILD_DIR/scripts/ci/test-config.cfg" &> /dev/null &
# Use this instead to see glauth logs while running
# "$CI_BUILD_DIR/bin/linuxamd64/glauth" -c "$CI_BUILD_DIR/scripts/ci/test-config.cfg" &
# "$CI_BUILD_DIR/bin/$(go env GOOS)$(go env GOARCH)/glauth" -c "$CI_BUILD_DIR/scripts/ci/test-config.cfg" &
glauthPid="$!"
echo "Running glauth at PID=$glauthPid"