-
Notifications
You must be signed in to change notification settings - Fork 332
/
authenticator.go
251 lines (219 loc) · 7.82 KB
/
authenticator.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
package auth
import (
"context"
"crypto/subtle"
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/go-ldap/ldap/v3"
"github.com/hashicorp/go-multierror"
"github.com/treeverse/lakefs/pkg/auth/model"
"github.com/treeverse/lakefs/pkg/logging"
)
// Authenticator authenticates users returning an identifier for the user.
// (Currently it handles only username+password single-step authentication.
// This interface will need to change significantly in order to support
// challenge-response protocols.)
type Authenticator interface {
// AuthenticateUser authenticates a user matching username and
// password and returns their ID.
AuthenticateUser(ctx context.Context, username, password string) (string, error)
}
// Credentialler fetches S3-style credentials for access keys.
type Credentialler interface {
GetCredentials(ctx context.Context, accessKeyID string) (*model.Credential, error)
}
// NewChainAuthenticator returns an Authenticator that authenticates users
// by trying each auth in order.
func NewChainAuthenticator(auth ...Authenticator) Authenticator {
return ChainAuthenticator(auth)
}
// ChainAuthenticator authenticates users by trying each Authenticator in
// order, returning the last error in case all fail.
type ChainAuthenticator []Authenticator
func (ca ChainAuthenticator) AuthenticateUser(ctx context.Context, username, password string) (string, error) {
var merr *multierror.Error
logger := logging.FromContext(ctx).WithField("username", username)
for _, a := range ca {
id, err := a.AuthenticateUser(ctx, username, password)
if err == nil {
return id, nil
}
// TODO(ariels): Add authenticator ID here.
merr = multierror.Append(merr, fmt.Errorf("%s: %w", a, err))
}
logger.WithError(merr).Info("Failed to authenticate user")
return InvalidUserID, merr
}
type EmailAuthenticator struct {
AuthService Service
}
func NewEmailAuthenticator(service Service) *EmailAuthenticator {
return &EmailAuthenticator{AuthService: service}
}
func (e EmailAuthenticator) AuthenticateUser(ctx context.Context, username, password string) (string, error) {
user, err := e.AuthService.GetUserByEmail(ctx, username)
if err != nil {
return InvalidUserID, err
}
if err := user.Authenticate(password); err != nil {
return InvalidUserID, err
}
return user.Username, nil
}
func (e EmailAuthenticator) String() string {
return "email authenticator"
}
// BuiltinAuthenticator authenticates users by their access key IDs and
// passwords stored in the auth service.
type BuiltinAuthenticator struct {
creds Credentialler
}
func NewBuiltinAuthenticator(service Service) *BuiltinAuthenticator {
return &BuiltinAuthenticator{creds: service}
}
func (ba *BuiltinAuthenticator) AuthenticateUser(ctx context.Context, username, password string) (string, error) {
// Look user up in DB. username is really the access key ID.
cred, err := ba.creds.GetCredentials(ctx, username)
if err != nil {
return InvalidUserID, err
}
if subtle.ConstantTimeCompare([]byte(password), []byte(cred.SecretAccessKey)) != 1 {
return InvalidUserID, ErrInvalidSecretAccessKey
}
return cred.Username, nil
}
func (ba *BuiltinAuthenticator) String() string {
return "built in authenticator"
}
// LDAPAuthenticator authenticates users on an LDAP server. It currently
// supports only simple authentication.
type LDAPAuthenticator struct {
AuthService Service
MakeLDAPConn func(ctx context.Context) (*ldap.Conn, error)
BindDN string
BindPassword string
BaseSearchRequest ldap.SearchRequest
UsernameAttribute string
DefaultUserGroup string
// control is bound to the operator (BindDN) and is used to query
// LDAP about users.
control *ldap.Conn
}
func (la *LDAPAuthenticator) getControlConnection(ctx context.Context) (*ldap.Conn, error) {
// LDAP connections are "closing" even after they've closed.
if la.control != nil && !la.control.IsClosing() {
return la.control, nil
}
control, err := la.MakeLDAPConn(ctx)
if err != nil {
return nil, fmt.Errorf("open control connection: %w", err)
}
if la.BindPassword == "" {
err = control.UnauthenticatedBind(la.BindDN)
} else {
err = control.Bind(la.BindDN, la.BindPassword)
}
if err != nil {
return nil, fmt.Errorf("bind control connection to %s: %w", la.BindDN, err)
}
la.control = control
return la.control, nil
}
func (la *LDAPAuthenticator) resetControlConnection() {
go la.control.Close() // Don't wait for dead connection to shut itself down
la.control = nil
}
// inBrackets returns filter (which should already be properly escaped)
// enclosed in brackets if it does not start with open brackets.
func inBrackets(filter string) string {
filter = strings.TrimLeft(filter, " \t\n\r")
if filter == "" {
return filter
}
if filter[0] == '(' {
return filter
}
return fmt.Sprintf("(%s)", filter)
}
func (la *LDAPAuthenticator) AuthenticateUser(ctx context.Context, username, password string) (string, error) {
// There may be multiple authenticators. Log everything to allow debugging.
logger := logging.FromContext(ctx).WithField("username", username)
controlConn, err := la.getControlConnection(ctx)
if err != nil {
logger.WithError(err).Error("Failed to bind LDAP control connection")
return InvalidUserID, fmt.Errorf("LDAP bind control: %w", err)
}
searchRequest := la.BaseSearchRequest
searchRequest.Filter = fmt.Sprintf("(&(%s=%s)%s)", la.UsernameAttribute, ldap.EscapeFilter(username), inBrackets(searchRequest.Filter))
res, err := controlConn.SearchWithPaging(&searchRequest, 2) //nolint: gomnd
if err != nil {
logger.WithError(err).Error("Failed to search for DN by username")
if errors.Is(err, os.ErrDeadlineExceeded) {
la.resetControlConnection()
}
return InvalidUserID, fmt.Errorf("LDAP find user %s: %w", username, err)
}
if logger.IsTracing() {
for _, e := range res.Entries {
logger.WithField("entry", fmt.Sprintf("%+v", e)).Trace("LDAP entry")
}
}
if len(res.Entries) == 0 {
logger.WithError(err).Debug("No users found")
return InvalidUserID, fmt.Errorf("LDAP find user %s: %w", username, ErrNotFound)
}
if len(res.Entries) > 1 {
logger.WithError(err).Error("Too many users found")
return InvalidUserID, fmt.Errorf("LDAP find user %s: %w", username, ErrNonUnique)
}
dn := res.Entries[0].DN
// TODO(ariels): This should be on the audit log.
logger = logger.WithField("dn", dn)
logger.Trace("Authenticate user")
userConn, err := la.MakeLDAPConn(ctx)
if err != nil {
logger.WithError(err).Error("Open per-user LDAP connection")
return InvalidUserID, fmt.Errorf("LDAP connect for user auth: %w", err) //nolint:stylecheck
}
defer userConn.Close()
err = userConn.Bind(dn, password)
if err != nil {
logger.WithError(err).Debug("Failed to bind user")
return InvalidUserID, fmt.Errorf("user auth failed: %w", err)
}
// TODO(ariels): Should users be stored by their DNs or by their
// usernames? (Also below in user passed to CreateUser).
user, err := la.AuthService.GetUser(ctx, dn)
if err == nil {
logger.WithField("user", fmt.Sprintf("%+v", user)).Debug("Got existing user")
return user.Username, nil
}
if !errors.Is(err, ErrNotFound) {
logger.WithError(err).Info("Could not get user; create them")
}
newUser := &model.User{
CreatedAt: time.Now(),
Username: dn,
FriendlyName: &username,
Source: "ldap",
}
_, err = la.AuthService.CreateUser(ctx, newUser)
if err != nil {
return InvalidUserID, fmt.Errorf("create backing user for LDAP user %s: %w", dn, err)
}
_, err = la.AuthService.CreateCredentials(ctx, dn)
if err != nil {
return InvalidUserID, fmt.Errorf("create credentials for LDAP user %s: %w", dn, err)
}
err = la.AuthService.AddUserToGroup(ctx, dn, la.DefaultUserGroup)
if err != nil {
return InvalidUserID, fmt.Errorf("add newly created LDAP user %s to %s: %w", dn, la.DefaultUserGroup, err)
}
return newUser.Username, nil
}
func (la *LDAPAuthenticator) String() string {
return "LDAP authenticator"
}