package ldap import ( "context" "errors" "time" shared "tug/internal/platform/backoff" "tug/internal/service/password" fk "tug/internal/transport/ldap/internal/glauthldap" ) const ( backoffThreshold = shared.Threshold backoffBase = shared.Base backoffCap = shared.Cap backoffEntries = shared.Entries operationTimeout = 10 * time.Second ) const ( bindAccepted = "accepted" bindRejected = "rejected" bindLocked = "locked" bindBusy = "busy" bindUnavailable = "unavailable" bindUnauthenticated = "unauthenticated" bindLimited = "limited" ) var BindResults = []string{bindAccepted, bindRejected, bindLocked, bindBusy, bindUnavailable, bindUnauthenticated, bindLimited} var verifyAbsent = password.VerifyAbsent type backoff struct { *shared.Backoff } func newBackoff(now func() time.Time, max int) *backoff { return &backoff{shared.New(now, max)} } func (b *backoff) locked(key string) bool { return b.Locked(key) } func (b *backoff) fail(key string) { b.Fail(key) } func (b *backoff) reset(key string) { b.Reset(key) } func (s *Server) bindUsername(bindDN string) (string, bool) { rdns, err := parseDN(bindDN) if err != nil || len(rdns) < 3 || rdns[0].typ != "uid" { return "", false } if rdns[1] != (rdn{typ: "ou", val: "people"}) || joinDN(rdns[2:]) != s.dir.base { return "", false } return rdns[0].val, true } func (s *Server) bind(bindDN, bindSimplePw string) (fk.LDAPResultCode, string, string) { if bindSimplePw == "" { return fk.LDAPResultInappropriateAuthentication, bindUnauthenticated, "" } ctx, cancel := context.WithTimeout(s.ctx, operationTimeout) defer cancel() username, ok := s.bindUsername(bindDN) if !ok { code, result := s.refuseBind(ctx, "", bindSimplePw, bindRejected) return code, result, "" } if s.backoff.locked(username) { return fk.LDAPResultInvalidCredentials, bindLocked, "" } u, found, err := s.dir.publishable(ctx, username) if err != nil { s.logger.Warn("ldap bind lookup failed", "reason", "store") return fk.LDAPResultUnavailable, bindUnavailable, "" } if !found { code, result := s.refuseBind(ctx, username, bindSimplePw, bindRejected) return code, result, "" } match, err := password.Verify(ctx, u.PasswordHash, bindSimplePw) switch { case errors.Is(err, password.ErrBusy): return fk.LDAPResultBusy, bindBusy, "" case err != nil: s.logger.Warn("ldap bind stored hash unusable", "uid", username) case match: s.backoff.reset(username) return fk.LDAPResultSuccess, bindAccepted, u.ID } s.backoff.fail(username) return fk.LDAPResultInvalidCredentials, bindRejected, u.ID } func (s *Server) refuseBind(ctx context.Context, username, pw, result string) (fk.LDAPResultCode, string) { if err := verifyAbsent(ctx, pw); errors.Is(err, password.ErrBusy) { return fk.LDAPResultBusy, bindBusy } if username != "" { s.backoff.fail(username) } return fk.LDAPResultInvalidCredentials, result }