Raw
  1package ldap
  2
  3import (
  4	"context"
  5	"errors"
  6	"time"
  7
  8	shared "tug/internal/platform/backoff"
  9	"tug/internal/service/password"
 10	fk "tug/internal/transport/ldap/internal/glauthldap"
 11)
 12
 13const (
 14	backoffThreshold = shared.Threshold
 15	backoffBase      = shared.Base
 16	backoffCap       = shared.Cap
 17	backoffEntries   = shared.Entries
 18	operationTimeout = 10 * time.Second
 19)
 20
 21const (
 22	bindAccepted        = "accepted"
 23	bindRejected        = "rejected"
 24	bindLocked          = "locked"
 25	bindBusy            = "busy"
 26	bindUnavailable     = "unavailable"
 27	bindUnauthenticated = "unauthenticated"
 28	bindLimited         = "limited"
 29)
 30
 31var BindResults = []string{bindAccepted, bindRejected, bindLocked, bindBusy, bindUnavailable, bindUnauthenticated, bindLimited}
 32
 33var verifyAbsent = password.VerifyAbsent
 34
 35type backoff struct {
 36	*shared.Backoff
 37}
 38
 39func newBackoff(now func() time.Time, max int) *backoff {
 40	return &backoff{shared.New(now, max)}
 41}
 42
 43func (b *backoff) locked(key string) bool { return b.Locked(key) }
 44
 45func (b *backoff) fail(key string) { b.Fail(key) }
 46
 47func (b *backoff) reset(key string) { b.Reset(key) }
 48
 49func (s *Server) bindUsername(bindDN string) (string, bool) {
 50	rdns, err := parseDN(bindDN)
 51	if err != nil || len(rdns) < 3 || rdns[0].typ != "uid" {
 52		return "", false
 53	}
 54	if rdns[1] != (rdn{typ: "ou", val: "people"}) || joinDN(rdns[2:]) != s.dir.base {
 55		return "", false
 56	}
 57	return rdns[0].val, true
 58}
 59
 60func (s *Server) bind(bindDN, bindSimplePw string) (fk.LDAPResultCode, string, string) {
 61	if bindSimplePw == "" {
 62		return fk.LDAPResultInappropriateAuthentication, bindUnauthenticated, ""
 63	}
 64	ctx, cancel := context.WithTimeout(s.ctx, operationTimeout)
 65	defer cancel()
 66	username, ok := s.bindUsername(bindDN)
 67	if !ok {
 68		code, result := s.refuseBind(ctx, "", bindSimplePw, bindRejected)
 69		return code, result, ""
 70	}
 71	if s.backoff.locked(username) {
 72		return fk.LDAPResultInvalidCredentials, bindLocked, ""
 73	}
 74	u, found, err := s.dir.publishable(ctx, username)
 75	if err != nil {
 76		s.logger.Warn("ldap bind lookup failed", "reason", "store")
 77		return fk.LDAPResultUnavailable, bindUnavailable, ""
 78	}
 79	if !found {
 80		code, result := s.refuseBind(ctx, username, bindSimplePw, bindRejected)
 81		return code, result, ""
 82	}
 83	match, err := password.Verify(ctx, u.PasswordHash, bindSimplePw)
 84	switch {
 85	case errors.Is(err, password.ErrBusy):
 86		return fk.LDAPResultBusy, bindBusy, ""
 87	case err != nil:
 88		s.logger.Warn("ldap bind stored hash unusable", "uid", username)
 89	case match:
 90		s.backoff.reset(username)
 91		return fk.LDAPResultSuccess, bindAccepted, u.ID
 92	}
 93	s.backoff.fail(username)
 94	return fk.LDAPResultInvalidCredentials, bindRejected, u.ID
 95}
 96
 97func (s *Server) refuseBind(ctx context.Context, username, pw, result string) (fk.LDAPResultCode, string) {
 98	if err := verifyAbsent(ctx, pw); errors.Is(err, password.ErrBusy) {
 99		return fk.LDAPResultBusy, bindBusy
100	}
101	if username != "" {
102		s.backoff.fail(username)
103	}
104	return fk.LDAPResultInvalidCredentials, result
105}