-
Notifications
You must be signed in to change notification settings - Fork 3.8k
/
auth.go
600 lines (540 loc) · 21.2 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
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
// Copyright 2019 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 pgwire
import (
"context"
"crypto/tls"
"net"
"strings"
"sync"
"time"
"github.com/cockroachdb/cockroach/pkg/security"
"github.com/cockroachdb/cockroach/pkg/security/username"
"github.com/cockroachdb/cockroach/pkg/sql"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/hba"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/identmap"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgwirebase"
"github.com/cockroachdb/cockroach/pkg/sql/sessiondatapb"
"github.com/cockroachdb/cockroach/pkg/util/log"
"github.com/cockroachdb/cockroach/pkg/util/log/eventpb"
"github.com/cockroachdb/cockroach/pkg/util/log/logpb"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/redact"
)
const (
// authOK is the pgwire auth response code for successful authentication
// during the connection handshake.
authOK int32 = 0
// authCleartextPassword is the pgwire auth response code to request
// a plaintext password during the connection handshake.
authCleartextPassword int32 = 3
// authReqSASL is the begin request for a SCRAM handshake.
authReqSASL int32 = 10
// authReqSASLContinue is the continue request for a SCRAM handshake.
authReqSASLContinue int32 = 11
// authReqSASLFin is the final message for a SCRAM handshake.
authReqSASLFin int32 = 12
)
type authOptions struct {
// insecure indicates that all connections for existing users must
// be allowed to go through. A password, if presented, must be
// accepted.
insecure bool
// connType is the actual type of client connection (e.g. local,
// hostssl, hostnossl).
connType hba.ConnType
// connDetails is the event payload common to all auth/session events.
connDetails eventpb.CommonConnectionDetails
// auth is the current HBA configuration as returned by
// (*Server).GetAuthenticationConfiguration().
auth *hba.Conf
// identMap is used in conjunction with the HBA configuration to
// allow system usernames (e.g. GSSAPI principals or X.509 CN's) to
// be dynamically mapped to database usernames.
identMap *identmap.Conf
// The following fields are only used by tests.
// testingSkipAuth requires to skip authentication, not even
// allowing a password exchange.
// Note that this different from insecure auth: with no auth, no
// password is accepted (a protocol error is given if one is
// presented); with insecure auth; _any_ is accepted.
testingSkipAuth bool
// testingAuthHook, if provided, replaces the logic in
// handleAuthentication().
testingAuthHook func(ctx context.Context) error
}
// handleAuthentication checks the connection's user. Errors are sent to the
// client and also returned.
//
// TODO(knz): handleAuthentication should discuss with the client to arrange
// authentication and update c.sessionArgs with the authenticated user's name,
// if different from the one given initially.
func (c *conn) handleAuthentication(
ctx context.Context, ac AuthConn, authOpt authOptions, execCfg *sql.ExecutorConfig,
) (connClose func(), _ error) {
if authOpt.testingSkipAuth {
return nil, nil
}
if authOpt.testingAuthHook != nil {
return nil, authOpt.testingAuthHook(ctx)
}
// Retrieve the authentication method.
tlsState, hbaEntry, authMethod, err := c.findAuthenticationMethod(authOpt)
if err != nil {
ac.LogAuthFailed(ctx, eventpb.AuthFailReason_METHOD_NOT_FOUND, err)
return nil, c.sendError(ctx, pgerror.WithCandidateCode(err, pgcode.InvalidAuthorizationSpecification))
}
ac.SetAuthMethod(hbaEntry.Method.String())
ac.LogAuthInfof(ctx, redact.Sprintf("HBA rule: %s", hbaEntry.Input))
// Populate the AuthMethod with per-connection information so that it
// can compose the next layer of behaviors that we're going to apply
// to the incoming connection.
behaviors, err := authMethod(ctx, ac, tlsState, execCfg, hbaEntry, authOpt.identMap)
connClose = behaviors.ConnClose
if err != nil {
ac.LogAuthFailed(ctx, eventpb.AuthFailReason_UNKNOWN, err)
return connClose, c.sendError(ctx, pgerror.WithCandidateCode(err, pgcode.InvalidAuthorizationSpecification))
}
// Choose the system identity that we'll use below for mapping
// externally-provisioned principals to database users.
var systemIdentity username.SQLUsername
if found, ok := behaviors.ReplacementIdentity(); ok {
systemIdentity = found
ac.SetSystemIdentity(systemIdentity)
} else {
if !c.sessionArgs.SystemIdentity.Undefined() {
// This case is used in tests, which pass a system_identity
// option directly.
systemIdentity = c.sessionArgs.SystemIdentity
} else {
systemIdentity = c.sessionArgs.User
}
}
c.sessionArgs.SystemIdentity = systemIdentity
// Delegate to the AuthMethod's MapRole to verify that the
// client-provided username matches one of the mappings.
if err := c.checkClientUsernameMatchesMapping(ctx, ac, behaviors.MapRole, systemIdentity); err != nil {
log.Warningf(ctx, "unable to map incoming identity %q to any database user: %+v", systemIdentity, err)
ac.LogAuthFailed(ctx, eventpb.AuthFailReason_USER_NOT_FOUND, err)
return connClose, c.sendError(ctx, pgerror.WithCandidateCode(err, pgcode.InvalidAuthorizationSpecification))
}
// Once chooseDbRole() returns, we know that the actual DB username
// will be present in c.sessionArgs.User.
dbUser := c.sessionArgs.User
// Check that the requested user exists and retrieve the hashed
// password in case password authentication is needed.
exists, canLoginSQL, _, canUseReplicationMode, isSuperuser, defaultSettings, roleSubject, pwRetrievalFn, err :=
sql.GetUserSessionInitInfo(
ctx,
execCfg,
dbUser,
c.sessionArgs.SessionDefaults["database"],
)
if err != nil {
log.Warningf(ctx, "user retrieval failed for user=%q: %+v", dbUser, err)
ac.LogAuthFailed(ctx, eventpb.AuthFailReason_USER_RETRIEVAL_ERROR, err)
return connClose, c.sendError(ctx, pgerror.WithCandidateCode(err, pgcode.InvalidAuthorizationSpecification))
}
c.sessionArgs.IsSuperuser = isSuperuser
if !exists {
ac.LogAuthFailed(ctx, eventpb.AuthFailReason_USER_NOT_FOUND, nil)
// If the user does not exist, we show the same error used for invalid
// passwords, to make it harder for an attacker to determine if a user
// exists.
return connClose, c.sendError(ctx, pgerror.WithCandidateCode(security.NewErrPasswordUserAuthFailed(dbUser), pgcode.InvalidPassword))
}
if !canLoginSQL {
ac.LogAuthFailed(ctx, eventpb.AuthFailReason_LOGIN_DISABLED, nil)
return connClose, c.sendError(ctx, pgerror.Newf(pgcode.InvalidAuthorizationSpecification, "%s does not have login privilege", dbUser))
}
// At this point, we know that the requested user exists and is allowed to log
// in. Now we can delegate to the selected AuthMethod implementation to
// complete the authentication.
// TODO(souravcrl): Verify whether to use systemIdentity or dbUser here since
// systemIdentity refers to external system name, which is same as dbUser name
// incase ReplacementIdentity is not set.
if err := behaviors.Authenticate(ctx, systemIdentity, true /* public */, pwRetrievalFn, roleSubject); err != nil {
ac.LogAuthFailed(ctx, eventpb.AuthFailReason_UNKNOWN, err)
if pErr := (*security.PasswordUserAuthError)(nil); errors.As(err, &pErr) {
err = pgerror.WithCandidateCode(err, pgcode.InvalidPassword)
} else {
err = pgerror.WithCandidateCode(err, pgcode.InvalidAuthorizationSpecification)
}
return connClose, c.sendError(ctx, err)
}
// Add all the defaults to this session's defaults. If there is an
// error (e.g., a setting that no longer exists, or bad input),
// log a warning instead of preventing login.
// The defaultSettings array is ordered by precedence. This means that if
// SessionDefaults already has an entry for a given setting name, then
// it should not be replaced.
for _, settingEntry := range defaultSettings {
for _, setting := range settingEntry.Settings {
keyVal := strings.SplitN(setting, "=", 2)
if len(keyVal) != 2 {
log.Ops.Warningf(ctx, "%s has malformed default setting: %q", dbUser, setting)
continue
}
if err := sql.CheckSessionVariableValueValid(ctx, execCfg.Settings, keyVal[0], keyVal[1]); err != nil {
log.Ops.Warningf(ctx, "%s has invalid default setting: %v", dbUser, err)
continue
}
if _, ok := c.sessionArgs.SessionDefaults[keyVal[0]]; !ok {
c.sessionArgs.SessionDefaults[keyVal[0]] = keyVal[1]
}
}
}
// Check replication privilege.
if c.sessionArgs.SessionDefaults["replication"] != "" {
m, err := sql.ReplicationModeFromString(c.sessionArgs.SessionDefaults["replication"])
if err == nil && m != sessiondatapb.ReplicationMode_REPLICATION_MODE_DISABLED && !canUseReplicationMode {
ac.LogAuthFailed(ctx, eventpb.AuthFailReason_NO_REPLICATION_ROLEOPTION, nil)
return connClose, c.sendError(
ctx,
pgerror.Newf(
pgcode.InsufficientPrivilege,
"must be superuser or have REPLICATION role to start streaming replication",
),
)
}
}
return connClose, nil
}
func (c *conn) authOKMessage() error {
c.msgBuilder.initMsg(pgwirebase.ServerMsgAuth)
c.msgBuilder.putInt32(authOK)
return c.msgBuilder.finishMsg(c.conn)
}
// checkClientUsernameMatchesMapping uses the provided RoleMapper to
// verify that the client-provided username matches one of the
// mappings for the system identity.
// See: https://www.postgresql.org/docs/15/auth-username-maps.html
// "There is no restriction regarding how many database users a given
// operating system user can correspond to, nor vice versa. Thus,
// entries in a map should be thought of as meaning “this operating
// system user is allowed to connect as this database user”, rather
// than implying that they are equivalent. The connection will be
// allowed if there is any map entry that pairs the user name obtained
// from the external authentication system with the database user name
// that the user has requested to connect as."
func (c *conn) checkClientUsernameMatchesMapping(
ctx context.Context, ac AuthConn, mapper RoleMapper, systemIdentity username.SQLUsername,
) error {
mapped, err := mapper(ctx, systemIdentity)
if err != nil {
return err
}
if len(mapped) == 0 {
return errors.Newf("system identity %q did not map to a database role", systemIdentity.Normalized())
}
for _, m := range mapped {
if m == c.sessionArgs.User {
ac.SetDbUser(m)
return nil
}
}
return errors.Newf("requested user identity %q does not correspond to any mapping for system identity %q",
c.sessionArgs.User, systemIdentity.Normalized())
}
func (c *conn) findAuthenticationMethod(
authOpt authOptions,
) (tlsState tls.ConnectionState, hbaEntry *hba.Entry, methodFn AuthMethod, err error) {
if authOpt.insecure {
// Insecure connections always use "trust" no matter what, and the
// remaining of the configuration is ignored.
methodFn = authTrust
hbaEntry = &insecureEntry
return
}
if c.sessionArgs.SessionRevivalToken != nil {
methodFn = authSessionRevivalToken(c.sessionArgs.SessionRevivalToken)
c.sessionArgs.SessionRevivalToken = nil
hbaEntry = &sessionRevivalEntry
return
}
if c.sessionArgs.JWTAuthEnabled {
methodFn = authJwtToken
hbaEntry = &jwtAuthEntry
return
}
// Look up the method from the HBA configuration.
var mi methodInfo
mi, hbaEntry, err = c.lookupAuthenticationMethodUsingRules(authOpt.connType, authOpt.auth)
if err != nil {
return
}
methodFn = mi.fn
// Check that this method can be used over this connection type.
if authOpt.connType&mi.validConnTypes == 0 {
err = errors.Newf("method %q required for this user, but unusable over this connection type",
hbaEntry.Method.Value)
return
}
// If the client is using SSL, retrieve the TLS state to provide as
// input to the method.
if authOpt.connType == hba.ConnHostSSL {
tlsConn, ok := c.conn.(*tls.Conn)
if !ok {
err = errors.AssertionFailedf("server reports hostssl conn without TLS state")
return
}
tlsState = tlsConn.ConnectionState()
}
return
}
func (c *conn) lookupAuthenticationMethodUsingRules(
connType hba.ConnType, auth *hba.Conf,
) (mi methodInfo, entry *hba.Entry, err error) {
var ip net.IP
if connType != hba.ConnLocal && connType != hba.ConnInternalLoopback {
// Extract the IP address of the client.
tcpAddr, ok := c.sessionArgs.RemoteAddr.(*net.TCPAddr)
if !ok {
err = errors.AssertionFailedf("client address type %T unsupported", c.sessionArgs.RemoteAddr)
return
}
ip = tcpAddr.IP
}
// Look up the method.
for i := range auth.Entries {
entry = &auth.Entries[i]
var connMatch bool
connMatch, err = entry.ConnMatches(connType, ip)
if err != nil {
// TODO(knz): Determine if an error should be reported
// upon unknown address formats.
// See: https://github.com/cockroachdb/cockroach/issues/43716
return
}
if !connMatch {
// The address does not match.
continue
}
if !entry.UserMatches(c.sessionArgs.User) {
// The user does not match.
continue
}
return entry.MethodFn.(methodInfo), entry, nil
}
// No match.
err = errors.Errorf("no %s entry for host %q, user %q", serverHBAConfSetting, ip, c.sessionArgs.User)
return
}
// authenticatorIO is the interface used by the connection to pass password data
// to the authenticator and expect an authentication decision from it.
type authenticatorIO interface {
// sendPwdData is used to push authentication data into the authenticator.
// This call is blocking; authenticators are supposed to consume data hastily
// once they've requested it.
sendPwdData(data []byte) error
// noMorePwdData is used to inform the authenticator that the client is not
// sending any more authentication data. This method can be called multiple
// times.
noMorePwdData()
// authResult blocks for an authentication decision. This call also informs
// the authenticator that no more auth data is coming from the client;
// noMorePwdData() is called internally.
authResult() error
}
// AuthConn is the interface used by the authenticator for interacting with the
// pgwire connection.
type AuthConn interface {
// SendAuthRequest send a request for authentication information. After
// calling this, the authenticator needs to call GetPwdData() quickly, as the
// connection's goroutine will be blocked on providing us the requested data.
SendAuthRequest(authType int32, data []byte) error
// GetPwdData returns authentication info that was previously requested with
// SendAuthRequest. The call blocks until such data is available.
// An error is returned if the client connection dropped or if the client
// didn't respect the protocol. After an error has been returned, GetPwdData()
// cannot be called any more.
GetPwdData() ([]byte, error)
// AuthOK declares that authentication succeeded and provides a
// unqualifiedIntSizer, to be returned by authenticator.authResult(). Future
// authenticator.sendPwdData() calls fail.
AuthOK(context.Context)
// AuthFail declares that authentication has failed and provides an error to
// be returned by authenticator.authResult(). Future
// authenticator.sendPwdData() calls fail. The error has already been written
// to the client connection.
AuthFail(err error)
// SetAuthMethod sets the authentication method for subsequent
// logging messages.
SetAuthMethod(method string)
// SetDbUser updates the AuthConn with the actual database username
// the connection has authenticated to.
SetDbUser(dbUser username.SQLUsername)
// SetSystemIdentity updates the AuthConn with an externally-defined
// identity for the connection. This is useful for "ambient"
// authentication mechanisms, such as GSSAPI.
SetSystemIdentity(systemIdentity username.SQLUsername)
// LogAuthInfof logs details about the progress of the
// authentication.
LogAuthInfof(ctx context.Context, msg redact.RedactableString)
// LogAuthFailed logs details about an authentication failure.
LogAuthFailed(ctx context.Context, reason eventpb.AuthFailReason, err error)
// LogAuthOK logs when the authentication handshake has completed.
LogAuthOK(ctx context.Context)
// LogSessionEnd logs when the session is ended.
LogSessionEnd(ctx context.Context, endTime time.Time)
// GetTenantSpecificMetrics returns the tenant-specific metrics for the connection.
GetTenantSpecificMetrics() *tenantSpecificMetrics
}
// authPipe is the implementation for the authenticator and AuthConn interfaces.
// A single authPipe will serve as both an AuthConn and an authenticator; the
// two represent the two "ends" of the pipe and we'll pass data between them.
type authPipe struct {
c *conn // Only used for writing, not for reading.
log bool
loggedFailure bool
connDetails eventpb.CommonConnectionDetails
authDetails eventpb.CommonSessionDetails
authMethod string
ch chan []byte
// closeWriterDoneOnce wraps close(writerDone) to prevent a panic if
// noMorePwdData is called multiple times.
closeWriterDoneOnce sync.Once
// writerDone is a channel closed by noMorePwdData().
writerDone chan struct{}
readerDone chan authRes
}
type authRes struct {
err error
}
func newAuthPipe(
c *conn, logAuthn bool, authOpt authOptions, systemIdentity username.SQLUsername,
) *authPipe {
ap := &authPipe{
c: c,
log: logAuthn,
connDetails: authOpt.connDetails,
authDetails: eventpb.CommonSessionDetails{
SystemIdentity: systemIdentity.Normalized(),
Transport: authOpt.connType.String(),
},
ch: make(chan []byte),
writerDone: make(chan struct{}),
readerDone: make(chan authRes, 1),
}
return ap
}
var _ authenticatorIO = &authPipe{}
var _ AuthConn = &authPipe{}
func (p *authPipe) sendPwdData(data []byte) error {
select {
case p.ch <- data:
return nil
case <-p.readerDone:
return pgwirebase.NewProtocolViolationErrorf("unexpected auth data")
}
}
func (p *authPipe) noMorePwdData() {
p.closeWriterDoneOnce.Do(func() {
// A reader blocked in GetPwdData() gets unblocked with an error.
close(p.writerDone)
})
}
const writerDoneError = "client didn't send required auth data"
// GetPwdData is part of the AuthConn interface.
func (p *authPipe) GetPwdData() ([]byte, error) {
select {
case data := <-p.ch:
return data, nil
case <-p.writerDone:
return nil, pgwirebase.NewProtocolViolationErrorf(writerDoneError)
}
}
// AuthOK is part of the AuthConn interface.
func (p *authPipe) AuthOK(ctx context.Context) {
p.readerDone <- authRes{err: nil}
}
func (p *authPipe) AuthFail(err error) {
p.readerDone <- authRes{err: err}
}
func (p *authPipe) SetAuthMethod(method string) {
p.authMethod = method
}
func (p *authPipe) SetDbUser(dbUser username.SQLUsername) {
p.authDetails.User = dbUser.Normalized()
}
func (p *authPipe) SetSystemIdentity(systemIdentity username.SQLUsername) {
p.authDetails.SystemIdentity = systemIdentity.Normalized()
}
func (p *authPipe) LogAuthOK(ctx context.Context) {
// Logged unconditionally.
ev := &eventpb.ClientAuthenticationOk{
CommonConnectionDetails: p.connDetails,
CommonSessionDetails: p.authDetails,
Method: p.authMethod,
}
log.StructuredEvent(ctx, ev)
}
func (p *authPipe) LogAuthInfof(ctx context.Context, msg redact.RedactableString) {
if p.log {
ev := &eventpb.ClientAuthenticationInfo{
CommonConnectionDetails: p.connDetails,
CommonSessionDetails: p.authDetails,
Info: msg,
Method: p.authMethod,
}
log.StructuredEvent(ctx, ev)
}
}
func (p *authPipe) LogSessionEnd(ctx context.Context, endTime time.Time) {
// Logged unconditionally.
ev := &eventpb.ClientSessionEnd{
CommonEventDetails: logpb.CommonEventDetails{Timestamp: endTime.UnixNano()},
CommonConnectionDetails: p.connDetails,
Duration: endTime.Sub(p.c.startTime).Nanoseconds(),
}
log.StructuredEvent(ctx, ev)
}
func (p *authPipe) LogAuthFailed(
ctx context.Context, reason eventpb.AuthFailReason, detailedErr error,
) {
if p.log && !p.loggedFailure {
// If a failure was already logged, then don't log another one. The
// assumption is that if an error is logged deeper in the call stack, the
// reason is likely to be more specific than at a higher point in the stack.
p.loggedFailure = true
var errStr redact.RedactableString
if detailedErr != nil {
errStr = redact.Sprint(detailedErr)
}
ev := &eventpb.ClientAuthenticationFailed{
CommonConnectionDetails: p.connDetails,
CommonSessionDetails: p.authDetails,
Reason: reason,
Detail: errStr,
Method: p.authMethod,
}
log.StructuredEvent(ctx, ev)
}
}
// authResult is part of the authenticator interface.
func (p *authPipe) authResult() error {
p.noMorePwdData()
res := <-p.readerDone
return res.err
}
// SendAuthRequest is part of the AuthConn interface.
func (p *authPipe) SendAuthRequest(authType int32, data []byte) error {
c := p.c
c.msgBuilder.initMsg(pgwirebase.ServerMsgAuth)
c.msgBuilder.putInt32(authType)
c.msgBuilder.write(data)
return c.msgBuilder.finishMsg(c.conn)
}
// GetTenantSpecificMetrics is part of the AuthConn interface.
func (p *authPipe) GetTenantSpecificMetrics() *tenantSpecificMetrics {
return p.c.metrics
}