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}