-
Notifications
You must be signed in to change notification settings - Fork 3.8k
/
Copy pathauth.go
246 lines (220 loc) · 8.19 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
// 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"
"github.com/cockroachdb/cockroach/pkg/roachpb"
"strconv"
"strings"
"github.com/cockroachdb/cockroach/pkg/util/syncutil"
"github.com/cockroachdb/errors"
)
var certPrincipalMap struct {
syncutil.RWMutex
m map[string]string
}
// 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 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
}
// GetCertificateUsers extract the users from a client certificate.
func GetCertificateUsers(tlsState *tls.ConnectionState) ([]string, error) {
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")
}
// The go server handshake code verifies the first certificate, using
// any following certificates as intermediates. See:
// https://github.com/golang/go/blob/go1.8.1/src/crypto/tls/handshake_server.go#L723:L742
peerCert := tlsState.PeerCertificates[0]
return getCertificatePrincipals(peerCert), 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 certUsers []string
var certTenantID roachpb.TenantID
if !insecureMode {
var err error
certUsers, err = GetCertificateUsers(tlsState)
if err != nil {
return nil, err
}
certTenantID, err = GetTenantIfTenantScopedClientCert(tlsState)
if err != nil {
return nil, err
}
}
return func(ctx context.Context, systemIdentity 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 %s 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 the current server is a non system tenant SQL server, we should use a tenant scoped
// client certificate.
// TODO(rima): Should we enforce always using tenant scoped client cert for non-system tenants?
if tenantID != roachpb.SystemTenantID {
// Enforce that the tenant ID *and* user matches the certificate
if tenantID != certTenantID {
return errors.Errorf("certificate is for tenant ID %s, but current tenant ID is %s", certTenantID, tenantID)
}
if !Contains(certUsers, systemIdentity.Normalized()) {
return errors.Errorf("requested user is %s, but certificate is for %s", systemIdentity, certUsers)
}
}
// TODO(rima): If we always want to enforce using tenant scoped client cert for non system tenants, we will
// need to put the below in an else block.
// The client certificate user must match the requested user.
if !Contains(certUsers, systemIdentity.Normalized()) {
return errors.Errorf("requested user is %s, but certificate is for %s", systemIdentity, certUsers)
}
return nil
}, nil
}
// 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, password string, hashedPassword PasswordHash,
) UserAuthHook {
return func(ctx context.Context, systemIdentity 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(password) == 0 {
return NewErrPasswordUserAuthFailed(systemIdentity)
}
ok, err := CompareHashAndCleartextPassword(ctx, hashedPassword, password)
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(username SQLUsername) error {
return errors.Newf("password authentication failed for user %s", username)
}
func GetTenantIfTenantScopedClientCert(tlsState *tls.ConnectionState) (roachpb.TenantID, error) {
if tlsState == nil {
return roachpb.TenantID{}, errors.Errorf("request is not using TLS")
}
if len(tlsState.PeerCertificates) == 0 {
return roachpb.TenantID{}, errors.Errorf("no client certificates in request")
}
// The go server handshake code verifies the first certificate, using
// any following certificates as intermediates. See:
// https://github.com/golang/go/blob/go1.8.1/src/crypto/tls/handshake_server.go#L723:L742
peerCert := tlsState.PeerCertificates[0]
uris := peerCert.URIs
var tenantID uint64
var err error
for _, uri := range uris {
if uri.Host == "tenant" {
tenantInfo := strings.TrimPrefix(uri.Path, "/")
tenantID, err = strconv.ParseUint(tenantInfo, 10, 64)
if err != nil {
return roachpb.TenantID{}, errors.Wrapf(err, "tenant ID: %s contained in cert is invalid", tenantInfo)
}
return roachpb.MakeTenantID(tenantID), nil
}
}
// No tenant info contained within cert, return default system tenant
return roachpb.SystemTenantID, nil
}