Compare commits

..

1 Commits

Author SHA1 Message Date
Jörn Friedrich Dreyer c2eb84d080 Fix typo in contributing section of README 2026-06-10 07:45:59 +02:00
9 changed files with 10 additions and 367 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
{
"v2": "2.5.1"
"v2": "2.5.0"
}
-8
View File
@@ -1,13 +1,5 @@
# Changelog
## [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)
+3 -2
View File
@@ -58,9 +58,10 @@ mocks:
vet:
-go vet ./...
.PHONY: vet
#test: @ runs package, root integration, and shell integration tests
#test: @ runs the integration test on linuxamd64 (eventually allow the binary to be set)
test: mocks vet
go test -v -cover -coverprofile coverage.out ./pkg/... ./internal/... .
# also run unit tests for packages, skip glauth_test.go for now
go test -v -cover -coverprofile coverage.out ./pkg/... ./internal/...
$(MAKE) runtest
#all: @ run build process for all binaries
all: setup binaries verify
+5 -117
View File
@@ -1,12 +1,8 @@
package main
import (
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
@@ -27,22 +23,10 @@ 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
}
return "bin/glauth"
}
func TestProperBuild(t *testing.T) {
info, err := os.Stat(glauthBinary())
info, err := os.Stat("bin/glauth")
if err != nil {
t.Fatal(err)
t.Error(err)
}
mode := uint32(info.Mode())
if mode&0b001001001 == 0 {
@@ -50,102 +34,6 @@ 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), 0600); err != nil {
t.Fatal(err)
}
svc := startSvc(SD, 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 := doRunGetFirst(0, "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), 0600); err != nil {
t.Fatal(err)
}
return path
}
func batteryOfTests(t *testing.T, env *testEnv) {
tests := []struct {
Name string
@@ -307,7 +195,7 @@ func TestSampleSimple(t *testing.T) {
checkemployeetype: "cn=hackers",
}
svc := startSvc(SD, glauthBinary(), "-c", ldapOnlyConfig(t, "sample-simple.cfg"))
svc := startSvc(SD, "bin/glauth", "-c", "sample-simple.cfg")
batteryOfTests(t, &env)
stopSvc(svc)
}
@@ -333,7 +221,7 @@ func TestSQLitePlugin(t *testing.T) {
checkemployeetype: "",
}
svc := startSvc(SD, glauthBinary(), "-c", "pkg/plugins/glauth-sqlite/sample-database.cfg")
svc := startSvc(SD, "bin/glauth", "-c", "pkg/plugins/glauth-sqlite/sample-database.cfg")
batteryOfTests(t, &env)
stopSvc(svc)
}
@@ -359,7 +247,7 @@ func TestLdapInjection(t *testing.T) {
checkemployeetype: "",
}
svc := startSvc(SD, glauthBinary(), "-c", "sample-ldap-injection.cfg")
svc := startSvc(SD, "bin/glauth", "-c", "sample-ldap-injection.cfg")
batteryOfTests(t, &env)
stopSvc(svc)
}
+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.5.1" // x-release-please-version
const Version = "v2.5.0" // x-release-please-version
-69
View File
@@ -1,69 +0,0 @@
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,8 +121,6 @@ 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,10 +65,6 @@ 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()
@@ -90,24 +86,11 @@ 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 {
@@ -192,15 +175,6 @@ 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
@@ -1,141 +0,0 @@
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)
}
}