-
Notifications
You must be signed in to change notification settings - Fork 3.9k
/
Copy pathauth.go
302 lines (269 loc) · 9.65 KB
/
auth.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
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
// Copyright 2015 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
package security
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"strings"
"github.com/cockroachdb/cockroach/pkg/roachpb"
"github.com/cockroachdb/cockroach/pkg/security/password"
"github.com/cockroachdb/cockroach/pkg/security/username"
"github.com/cockroachdb/cockroach/pkg/util/syncutil"
"github.com/cockroachdb/errors"
)
var certPrincipalMap struct {
syncutil.RWMutex
m map[string]string
}
// CertificateUserScope indicates the scope of a user certificate i.e.
// which tenant the user is allowed to authenticate on. Older client certificates
// without a tenant scope are treated as global certificates which can
// authenticate on any tenant strictly for backward compatibility with the
// older certificates.
type CertificateUserScope struct {
Username string
TenantID roachpb.TenantID
// global is set to true to indicate that the certificate unscoped to
// any tenant is a global client certificate which can authenticate
// on any tenant. This is ONLY for backward compatibility with old
// client certificates without a tenant scope.
Global bool
}
// UserAuthHook authenticates a user based on their username and whether their
// connection originates from a client or another node in the cluster. It
// returns an optional func that is run at connection close.
//
// The systemIdentity is the external identity, from GSSAPI or an X.509
// certificate, while databaseUsername reflects any username mappings
// that may have been applied to the given connection.
type UserAuthHook func(
ctx context.Context,
systemIdentity username.SQLUsername,
clientConnection bool,
) error
// SetCertPrincipalMap sets the global principal map. Each entry in the mapping
// list must either be empty or have the format <source>:<dest>. The principal
// map is used to transform principal names found in the Subject.CommonName or
// DNS-type SubjectAlternateNames fields of certificates. This function splits
// each list entry on the final colon, allowing <source> to contain colons.
func SetCertPrincipalMap(mappings []string) error {
m := make(map[string]string, len(mappings))
for _, v := range mappings {
if v == "" {
continue
}
idx := strings.LastIndexByte(v, ':')
if idx == -1 {
return errors.Errorf("invalid <cert-principal>:<db-principal> mapping: %q", v)
}
m[v[:idx]] = v[idx+1:]
}
certPrincipalMap.Lock()
certPrincipalMap.m = m
certPrincipalMap.Unlock()
return nil
}
func transformPrincipal(commonName string) string {
certPrincipalMap.RLock()
mappedName, ok := certPrincipalMap.m[commonName]
certPrincipalMap.RUnlock()
if !ok {
return commonName
}
return mappedName
}
func getCertificatePrincipals(cert *x509.Certificate) []string {
results := make([]string, 0, 1+len(cert.DNSNames))
results = append(results, transformPrincipal(cert.Subject.CommonName))
for _, name := range cert.DNSNames {
results = append(results, transformPrincipal(name))
}
return results
}
// GetCertificateUserScope extracts the certificate scopes from a client certificate.
func GetCertificateUserScope(
peerCert *x509.Certificate,
) (userScopes []CertificateUserScope, _ error) {
for _, uri := range peerCert.URIs {
uriString := uri.String()
if URISANHasCRDBPrefix(uriString) {
tenantID, user, err := ParseTenantURISAN(uriString)
if err != nil {
return nil, err
}
scope := CertificateUserScope{
Username: user,
TenantID: tenantID,
}
userScopes = append(userScopes, scope)
}
}
if len(userScopes) == 0 {
users := getCertificatePrincipals(peerCert)
for _, user := range users {
scope := CertificateUserScope{
Username: user,
Global: true,
}
userScopes = append(userScopes, scope)
}
}
return userScopes, nil
}
// Contains returns true if the specified string is present in the given slice.
func Contains(sl []string, s string) bool {
for i := range sl {
if sl[i] == s {
return true
}
}
return false
}
// UserAuthCertHook builds an authentication hook based on the security
// mode and client certificate.
func UserAuthCertHook(
insecureMode bool, tlsState *tls.ConnectionState, tenantID roachpb.TenantID,
) (UserAuthHook, error) {
var certUserScope []CertificateUserScope
if !insecureMode {
if tlsState == nil {
return nil, errors.Errorf("request is not using TLS")
}
if len(tlsState.PeerCertificates) == 0 {
return nil, errors.Errorf("no client certificates in request")
}
peerCert := tlsState.PeerCertificates[0]
var err error
certUserScope, err = GetCertificateUserScope(peerCert)
if err != nil {
return nil, err
}
}
return func(ctx context.Context, systemIdentity username.SQLUsername, clientConnection bool) error {
// TODO(marc): we may eventually need stricter user syntax rules.
if systemIdentity.Undefined() {
return errors.New("user is missing")
}
if !clientConnection && !systemIdentity.IsNodeUser() {
return errors.Errorf("user %q is not allowed", systemIdentity)
}
// If running in insecure mode, we have nothing to verify it against.
if insecureMode {
return nil
}
// The client certificate should not be a tenant client type. For now just
// check that it doesn't have OU=Tenants. It would make sense to add
// explicit OU=Users to all client certificates and to check for match.
if IsTenantCertificate(tlsState.PeerCertificates[0]) {
return errors.Errorf("using tenant client certificate as user certificate is not allowed")
}
if ValidateUserScope(certUserScope, systemIdentity.Normalized(), tenantID) {
return nil
}
return errors.WithDetailf(errors.Errorf("certificate authentication failed for user %q", systemIdentity),
"You are connecting to tenant %v. The client cert is valid for %s.", tenantID, FormatUserScopes(certUserScope))
}, nil
}
// FormatUserScopes formats a list of scopes in a human-readable way,
// suitable for e.g. inclusion in error messages.
func FormatUserScopes(certUserScope []CertificateUserScope) string {
var buf strings.Builder
comma := ""
for _, scope := range certUserScope {
fmt.Fprintf(&buf, "%s%q on ", comma, scope.Username)
if scope.Global {
buf.WriteString("all tenants")
} else {
fmt.Fprintf(&buf, "tenant %v", scope.TenantID)
}
comma = ", "
}
return buf.String()
}
// IsTenantCertificate returns true if the passed certificate indicates an
// inbound Tenant connection.
func IsTenantCertificate(cert *x509.Certificate) bool {
return Contains(cert.Subject.OrganizationalUnit, TenantsOU)
}
// UserAuthPasswordHook builds an authentication hook based on the security
// mode, password, and its potentially matching hash.
func UserAuthPasswordHook(
insecureMode bool, passwordStr string, hashedPassword password.PasswordHash,
) UserAuthHook {
return func(ctx context.Context, systemIdentity username.SQLUsername, clientConnection bool) error {
if systemIdentity.Undefined() {
return errors.New("user is missing")
}
if !clientConnection {
return errors.New("password authentication is only available for client connections")
}
if insecureMode {
return nil
}
// If the requested user has an empty password, disallow authentication.
if len(passwordStr) == 0 {
return NewErrPasswordUserAuthFailed(systemIdentity)
}
ok, err := password.CompareHashAndCleartextPassword(ctx,
hashedPassword, passwordStr, GetExpensiveHashComputeSem(ctx))
if err != nil {
return err
}
if !ok {
return NewErrPasswordUserAuthFailed(systemIdentity)
}
return nil
}
}
// NewErrPasswordUserAuthFailed constructs an error that represents
// failed password authentication for a user. It should be used when
// the password is incorrect or the user does not exist.
func NewErrPasswordUserAuthFailed(user username.SQLUsername) error {
return &PasswordUserAuthError{errors.Newf("password authentication failed for user %s", user)}
}
// PasswordUserAuthError indicates that an error was encountered
// during the initial set-up of a SQL connection.
type PasswordUserAuthError struct {
err error
}
// Error implements the error interface.
func (i *PasswordUserAuthError) Error() string { return i.err.Error() }
// Cause implements causer for compatibility with pkg/errors.
// NB: this is obsolete. Use Unwrap() instead.
func (i *PasswordUserAuthError) Cause() error { return i.err }
// Unwrap implements errors.Wrapper.
func (i *PasswordUserAuthError) Unwrap() error { return i.err }
// Format implements fmt.Formatter.
func (i *PasswordUserAuthError) Format(s fmt.State, verb rune) { errors.FormatError(i, s, verb) }
// FormatError implements errors.Formatter.
func (i *PasswordUserAuthError) FormatError(p errors.Printer) error {
return i.err
}
// ValidateUserScope returns true if the user is a valid user for the tenant based on the certificate
// user scope. It also returns true if the certificate is a global certificate. A client certificate
// is considered global only when it doesn't contain a tenant SAN which is only possible for older
// client certificates created prior to introducing tenant based scoping for the client.
func ValidateUserScope(
certUserScope []CertificateUserScope, user string, tenantID roachpb.TenantID,
) bool {
for _, scope := range certUserScope {
if scope.Username == user {
// If username matches, allow authentication to succeed if the tenantID is a match
// or if the certificate scope is global.
if scope.TenantID == tenantID || scope.Global {
return true
}
}
}
// No user match, return false.
return false
}