more auth logging
continuous-integration/drone/push Build is passing

This commit is contained in:
Nathan Coad
2026-04-21 10:35:10 +10:00
parent 2c3167a1a0
commit 361ba7719b
6 changed files with 204 additions and 10 deletions
+111 -5
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"net/http"
"sort"
"strings"
"time"
"vctp/internal/auth"
@@ -15,6 +16,7 @@ import (
const (
authLoginFailureMessage = "invalid username or password"
authLoginRequestTimeout = 30 * time.Second
maxDebugLogListItems = 25
)
type ldapAuthenticator interface {
@@ -78,6 +80,17 @@ func (h *Handler) AuthLogin(w http.ResponseWriter, r *http.Request) {
writeJSONError(w, http.StatusBadRequest, "username and password are required")
return
}
audit.LogAuthEvent(h.Logger, r, "login", "observe",
"reason", "ldap_authentication_start",
"username", username,
"ldap_bind_address", cfg.LDAPBindAddress,
"ldap_base_dn", cfg.LDAPBaseDN,
"ldap_group_requirements", limitStrings(cfg.LDAPGroups, maxDebugLogListItems),
"auth_group_role_mapping_keys", limitStrings(sortedStringMapKeys(cfg.AuthGroupRoleMappings), maxDebugLogListItems),
"ldap_insecure", cfg.LDAPInsecure,
"ldap_disable_validation", cfg.LDAPDisableValidation,
"ldap_trust_cert_configured", strings.TrimSpace(cfg.LDAPTrustCertFile) != "",
)
ldapAuth, err := newLDAPAuthenticator(auth.LDAPConfig{
BindAddress: cfg.LDAPBindAddress,
@@ -99,23 +112,70 @@ func (h *Handler) AuthLogin(w http.ResponseWriter, r *http.Request) {
identity, err := ldapAuth.AuthenticateAndFetchGroups(ctx, username, password)
if err != nil {
if errors.Is(err, auth.ErrLDAPInvalidCredentials) {
audit.LogAuthEvent(h.Logger, r, "login", "deny", "reason", "invalid_credentials", "username", username)
audit.LogAuthEvent(h.Logger, r, "login", "deny",
"reason", "invalid_credentials",
"username", username,
"ldap_bind_address", cfg.LDAPBindAddress,
"ldap_base_dn", cfg.LDAPBaseDN,
"error", err,
)
writeJSONError(w, http.StatusUnauthorized, authLoginFailureMessage)
return
}
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
audit.LogAuthEvent(h.Logger, r, "login", "deny", "reason", "ldap_timeout", "username", username, "error", err)
audit.LogAuthEvent(h.Logger, r, "login", "deny",
"reason", "ldap_timeout",
"username", username,
"ldap_bind_address", cfg.LDAPBindAddress,
"ldap_base_dn", cfg.LDAPBaseDN,
"timeout_seconds", authLoginRequestTimeout.Seconds(),
"error", err,
)
writeJSONError(w, http.StatusUnauthorized, authLoginFailureMessage)
return
}
audit.LogAuthEvent(h.Logger, r, "login", "deny", "reason", "ldap_authentication_failed", "username", username, "error", err)
audit.LogAuthEvent(h.Logger, r, "login", "deny",
"reason", "ldap_authentication_failed",
"username", username,
"ldap_bind_address", cfg.LDAPBindAddress,
"ldap_base_dn", cfg.LDAPBaseDN,
"error", err,
)
writeJSONError(w, http.StatusUnauthorized, authLoginFailureMessage)
return
}
audit.LogAuthEvent(h.Logger, r, "login", "observe",
"reason", "ldap_authentication_succeeded",
"username", username,
"ldap_identity_username", identity.Username,
"ldap_user_dn", identity.UserDN,
"ldap_group_count", len(identity.Groups),
"ldap_groups", limitStrings(identity.Groups, maxDebugLogListItems),
"ldap_diagnostics", limitStrings(identity.Diagnostics, maxDebugLogListItems),
)
roles := auth.ResolveRoles(identity.Groups, cfg.AuthGroupRoleMappings)
if !auth.HasAnyGroup(identity.Groups, cfg.LDAPGroups) || len(roles) == 0 {
audit.LogAuthEvent(h.Logger, r, "login", "deny", "reason", "group_or_role_denied", "username", username, "group_count", len(identity.Groups), "resolved_roles", roles)
hasRequiredGroup := auth.HasAnyGroup(identity.Groups, cfg.LDAPGroups)
audit.LogAuthEvent(h.Logger, r, "login", "observe",
"reason", "authorization_evaluation",
"username", username,
"has_required_group", hasRequiredGroup,
"required_groups", limitStrings(cfg.LDAPGroups, maxDebugLogListItems),
"user_groups", limitStrings(identity.Groups, maxDebugLogListItems),
"resolved_roles", roles,
"auth_group_role_mapping_keys", limitStrings(sortedStringMapKeys(cfg.AuthGroupRoleMappings), maxDebugLogListItems),
)
if !hasRequiredGroup || len(roles) == 0 {
audit.LogAuthEvent(h.Logger, r, "login", "deny",
"reason", "group_or_role_denied",
"username", username,
"group_count", len(identity.Groups),
"has_required_group", hasRequiredGroup,
"required_groups", limitStrings(cfg.LDAPGroups, maxDebugLogListItems),
"user_groups", limitStrings(identity.Groups, maxDebugLogListItems),
"resolved_roles", roles,
"ldap_diagnostics", limitStrings(identity.Diagnostics, maxDebugLogListItems),
)
writeJSONError(w, http.StatusUnauthorized, authLoginFailureMessage)
return
}
@@ -191,3 +251,49 @@ func (h *Handler) AuthMe(w http.ResponseWriter, r *http.Request) {
TokenID: claims.ID,
})
}
func sortedStringMapKeys(values map[string]string) []string {
if len(values) == 0 {
return nil
}
keys := make([]string, 0, len(values))
for key := range values {
key = strings.TrimSpace(key)
if key == "" {
continue
}
keys = append(keys, key)
}
if len(keys) == 0 {
return nil
}
sort.Strings(keys)
return keys
}
func limitStrings(values []string, maxItems int) []string {
if len(values) == 0 {
return nil
}
if maxItems <= 0 || len(values) <= maxItems {
out := make([]string, 0, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
out = append(out, value)
}
return out
}
out := make([]string, 0, maxItems+1)
for _, value := range values[:maxItems] {
value = strings.TrimSpace(value)
if value == "" {
continue
}
out = append(out, value)
}
out = append(out, "...")
return out
}