feat(auth): AD-Authenticator mit Zwei-Schritt-Bind, DC-Failover und verschachtelter Gruppenprüfung
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBHF4R9EAejDJUMdwr6C68
This commit is contained in:
parent
2eb69bdcfe
commit
99ee8758cc
8 changed files with 908 additions and 1 deletions
72
internal/auth/addata_test.go
Normal file
72
internal/auth/addata_test.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReasonFromLDAPError(t *testing.T) {
|
||||
// Formulierung wie sie AD in LDAP-Result-Code 49 liefert.
|
||||
const tmpl = "LDAP Result Code 49 \"Invalid Credentials\": 80090308: LdapErr: " +
|
||||
"DSID-0C0903A9, comment: AcceptSecurityContext error, data %s, v4563"
|
||||
|
||||
cases := map[string]string{
|
||||
"52e": ReasonInvalidCredentials,
|
||||
"533": ReasonAccountDisabled,
|
||||
"775": ReasonAccountLocked,
|
||||
"532": ReasonPasswordExpired,
|
||||
"773": ReasonPasswordChangeRequired,
|
||||
"525": ReasonInvalidCredentials, // user not found -> generisch, keine Enumeration
|
||||
"701": ReasonAccountDisabled, // account expired
|
||||
"999": ReasonInvalidCredentials, // unbekannter Code -> generisch
|
||||
}
|
||||
for code, want := range cases {
|
||||
err := errors.New(fmt.Sprintf(tmpl, code))
|
||||
if got := ReasonFromLDAPError(err); got != want {
|
||||
t.Errorf("data %s: got %q, want %q", code, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReasonFromLDAPErrorIsCaseInsensitive(t *testing.T) {
|
||||
err := errors.New("AcceptSecurityContext error, DATA 52E, v4563")
|
||||
if got := ReasonFromLDAPError(err); got != ReasonInvalidCredentials {
|
||||
t.Errorf("got %q, want %q", got, ReasonInvalidCredentials)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReasonFromLDAPErrorWithoutDataCode(t *testing.T) {
|
||||
if got := ReasonFromLDAPError(errors.New("connection refused")); got != ReasonBackendUnavailable {
|
||||
t.Errorf("got %q, want %q", got, ReasonBackendUnavailable)
|
||||
}
|
||||
if got := ReasonFromLDAPError(nil); got != "" {
|
||||
t.Errorf("nil-Fehler muss leeren Reason liefern, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorUserVisibleOnlyForExpiredPassword(t *testing.T) {
|
||||
if !(&Error{Reason: ReasonPasswordExpired}).UserVisible() {
|
||||
t.Error("abgelaufenes Passwort ist die einzige spezifische Meldung")
|
||||
}
|
||||
for _, r := range []string{
|
||||
ReasonInvalidCredentials, ReasonAccountDisabled, ReasonAccountLocked,
|
||||
ReasonNotInVPNGroup, ReasonPasswordChangeRequired, ReasonBackendUnavailable,
|
||||
} {
|
||||
if (&Error{Reason: r}).UserVisible() {
|
||||
t.Errorf("Reason %q darf keine spezifische Meldung erzeugen", r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorUnwraps(t *testing.T) {
|
||||
inner := errors.New("boom")
|
||||
err := &Error{Reason: ReasonBackendUnavailable, Err: inner}
|
||||
if !errors.Is(err, inner) {
|
||||
t.Error("Error muss den inneren Fehler durchreichen")
|
||||
}
|
||||
var authErr *Error
|
||||
if !errors.As(error(err), &authErr) {
|
||||
t.Error("errors.As muss *Error finden")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue