+111
-5
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user