diff --git a/docs/generated/eventlog.md b/docs/generated/eventlog.md
index b8378a02bbf8..07c045dd0e93 100644
--- a/docs/generated/eventlog.md
+++ b/docs/generated/eventlog.md
@@ -1740,7 +1740,8 @@ Events of this type are only emitted when the cluster setting
| `Network` | The network protocol for this connection: tcp4, tcp6, unix, etc. | no |
| `RemoteAddress` | The remote address of the SQL client. Note that when using a proxy or other intermediate server, this field will contain the address of the intermediate server. | yes |
| `Transport` | The connection type after transport negotiation. | no |
-| `User` | The username the session is for. This is the username passed by the client, after case-folding and Unicode normalization. | yes |
+| `User` | The database username the session is for. This username will have undergone case-folding and Unicode normalization. | yes |
+| `SystemIdentity` | The original system identity provided by the client, if an identity mapping was used per Host-Based Authentication rules. This may be a GSSAPI or X.509 principal or any other external value, so no specific assumptions should be made about the contents of this field. | yes |
### `client_authentication_info`
@@ -1767,7 +1768,8 @@ Events of this type are only emitted when the cluster setting
| `Network` | The network protocol for this connection: tcp4, tcp6, unix, etc. | no |
| `RemoteAddress` | The remote address of the SQL client. Note that when using a proxy or other intermediate server, this field will contain the address of the intermediate server. | yes |
| `Transport` | The connection type after transport negotiation. | no |
-| `User` | The username the session is for. This is the username passed by the client, after case-folding and Unicode normalization. | yes |
+| `User` | The database username the session is for. This username will have undergone case-folding and Unicode normalization. | yes |
+| `SystemIdentity` | The original system identity provided by the client, if an identity mapping was used per Host-Based Authentication rules. This may be a GSSAPI or X.509 principal or any other external value, so no specific assumptions should be made about the contents of this field. | yes |
### `client_authentication_ok`
@@ -1793,7 +1795,8 @@ Events of this type are only emitted when the cluster setting
| `Network` | The network protocol for this connection: tcp4, tcp6, unix, etc. | no |
| `RemoteAddress` | The remote address of the SQL client. Note that when using a proxy or other intermediate server, this field will contain the address of the intermediate server. | yes |
| `Transport` | The connection type after transport negotiation. | no |
-| `User` | The username the session is for. This is the username passed by the client, after case-folding and Unicode normalization. | yes |
+| `User` | The database username the session is for. This username will have undergone case-folding and Unicode normalization. | yes |
+| `SystemIdentity` | The original system identity provided by the client, if an identity mapping was used per Host-Based Authentication rules. This may be a GSSAPI or X.509 principal or any other external value, so no specific assumptions should be made about the contents of this field. | yes |
### `client_connection_end`
@@ -1866,7 +1869,8 @@ Events of this type are only emitted when the cluster setting
| `Network` | The network protocol for this connection: tcp4, tcp6, unix, etc. | no |
| `RemoteAddress` | The remote address of the SQL client. Note that when using a proxy or other intermediate server, this field will contain the address of the intermediate server. | yes |
| `Transport` | The connection type after transport negotiation. | no |
-| `User` | The username the session is for. This is the username passed by the client, after case-folding and Unicode normalization. | yes |
+| `User` | The database username the session is for. This username will have undergone case-folding and Unicode normalization. | yes |
+| `SystemIdentity` | The original system identity provided by the client, if an identity mapping was used per Host-Based Authentication rules. This may be a GSSAPI or X.509 principal or any other external value, so no specific assumptions should be made about the contents of this field. | yes |
## SQL Slow Query Log
diff --git a/docs/generated/settings/settings-for-tenants.txt b/docs/generated/settings/settings-for-tenants.txt
index 393fa0eee08f..8e121bf2e96a 100644
--- a/docs/generated/settings/settings-for-tenants.txt
+++ b/docs/generated/settings/settings-for-tenants.txt
@@ -50,6 +50,7 @@ server.consistency_check.max_rate byte size 8.0 MiB the rate limit (bytes/sec) t
server.eventlog.enabled boolean true if set, logged notable events are also stored in the table system.eventlog
server.eventlog.ttl duration 2160h0m0s if nonzero, entries in system.eventlog older than this duration are deleted every 10m0s. Should not be lowered below 24 hours.
server.host_based_authentication.configuration string host-based authentication configuration to use during connection authentication
+server.identity_map.configuration string system-identity to database-username mappings
server.oidc_authentication.autologin boolean false if true, logged-out visitors to the DB Console will be automatically redirected to the OIDC login endpoint (this feature is experimental)
server.oidc_authentication.button_text string Login with your OIDC provider text to show on button on DB Console login page to login with your OIDC provider (only shown if OIDC is enabled) (this feature is experimental)
server.oidc_authentication.claim_json_key string sets JSON key of principal to extract from payload after OIDC authentication completes (usually email or sid) (this feature is experimental)
diff --git a/docs/generated/settings/settings.html b/docs/generated/settings/settings.html
index 131b812acb88..cb5d818d2e16 100644
--- a/docs/generated/settings/settings.html
+++ b/docs/generated/settings/settings.html
@@ -55,6 +55,7 @@
server.eventlog.enabled
boolean true
if set, logged notable events are also stored in the table system.eventlog
server.eventlog.ttl
duration 2160h0m0s
if nonzero, entries in system.eventlog older than this duration are deleted every 10m0s. Should not be lowered below 24 hours.
server.host_based_authentication.configuration
string
host-based authentication configuration to use during connection authentication
+server.identity_map.configuration
string
system-identity to database-username mappings
server.oidc_authentication.autologin
boolean false
if true, logged-out visitors to the DB Console will be automatically redirected to the OIDC login endpoint (this feature is experimental)
server.oidc_authentication.button_text
string Login with your OIDC provider
text to show on button on DB Console login page to login with your OIDC provider (only shown if OIDC is enabled) (this feature is experimental)
server.oidc_authentication.claim_json_key
string
sets JSON key of principal to extract from payload after OIDC authentication completes (usually email or sid) (this feature is experimental)
diff --git a/pkg/BUILD.bazel b/pkg/BUILD.bazel
index cd510f5184e6..d528a5648f06 100644
--- a/pkg/BUILD.bazel
+++ b/pkg/BUILD.bazel
@@ -271,6 +271,7 @@ ALL_TESTS = [
"//pkg/sql/opt:opt_test",
"//pkg/sql/parser:parser_test",
"//pkg/sql/pgwire/hba:hba_test",
+ "//pkg/sql/pgwire/identmap:identmap_test",
"//pkg/sql/pgwire/pgerror:pgerror_test",
"//pkg/sql/pgwire:pgwire_test",
"//pkg/sql/physicalplan/replicaoracle:replicaoracle_test",
diff --git a/pkg/acceptance/compose/gss/psql/gss_test.go b/pkg/acceptance/compose/gss/psql/gss_test.go
index 601020501766..0a0d34be8afa 100644
--- a/pkg/acceptance/compose/gss/psql/gss_test.go
+++ b/pkg/acceptance/compose/gss/psql/gss_test.go
@@ -50,6 +50,8 @@ func TestGSS(t *testing.T) {
hbaErr string
// Error message of gss login.
gssErr string
+ // Optionally inject an HBA identity map.
+ identMap string
}{
{
conf: `host all all all gss include_realm=0 nope=1`,
@@ -59,9 +61,13 @@ func TestGSS(t *testing.T) {
conf: `host all all all gss include_realm=1`,
hbaErr: `include_realm must be set to 0`,
},
+ {
+ conf: `host all all all gss map=ignored include_realm=1`,
+ hbaErr: `include_realm must be set to 0`,
+ },
{
conf: `host all all all gss`,
- hbaErr: `missing "include_realm=0"`,
+ hbaErr: `at least one of "include_realm=0" or "map" options required`,
},
{
conf: `host all all all gss include_realm=0`,
@@ -93,6 +99,36 @@ func TestGSS(t *testing.T) {
user: "tester",
gssErr: `GSS authentication requires an enterprise license`,
},
+ // Validate that we can use the "map" option to strip the realm
+ // data. Note that the system-identity value will have been
+ // normalized into a lower-case value.
+ {
+ conf: `host all all all gss map=demo`,
+ identMap: `demo /^(.*)@my.ex$ \1`,
+ user: "tester",
+ gssErr: `GSS authentication requires an enterprise license`,
+ },
+ // Verify case-sensitivity.
+ {
+ conf: `host all all all gss map=demo`,
+ identMap: `demo /^(.*)@MY.EX$ \1`,
+ user: "tester",
+ gssErr: `system identity "tester@my.ex" did not map to a database role`,
+ },
+ // Validating the use of "map" as a filter.
+ {
+ conf: `host all all all gss map=demo`,
+ identMap: `demo /^(.*)@NOPE.EX$ \1`,
+ user: "tester",
+ gssErr: `system identity "tester@my.ex" did not map to a database role`,
+ },
+ // Check map+include_realm=0 case.
+ {
+ conf: `host all all all gss include_realm=0 map=demo`,
+ identMap: `demo tester remapped`,
+ user: "remapped",
+ gssErr: `GSS authentication requires an enterprise license`,
+ },
}
for i, tc := range tests {
t.Run(fmt.Sprint(i), func(t *testing.T) {
@@ -102,6 +138,11 @@ func TestGSS(t *testing.T) {
if tc.hbaErr != "" {
return
}
+ if tc.identMap != "" {
+ if _, err := db.Exec(`SET CLUSTER SETTING server.identity_map.configuration = $1`, tc.identMap); err != nil {
+ t.Fatalf("bad identity_map: %v", err)
+ }
+ }
if _, err := db.Exec(fmt.Sprintf(`CREATE USER IF NOT EXISTS '%s'`, tc.user)); err != nil {
t.Fatal(err)
}
diff --git a/pkg/ccl/gssapiccl/BUILD.bazel b/pkg/ccl/gssapiccl/BUILD.bazel
index 496841fb904c..fd2db9105469 100644
--- a/pkg/ccl/gssapiccl/BUILD.bazel
+++ b/pkg/ccl/gssapiccl/BUILD.bazel
@@ -4,7 +4,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "gssapiccl",
srcs = select({
- "@io_bazel_rules_go//go/platform:linux_amd64": ["gssapi.go"],
+ "@io_bazel_rules_go//go/platform:linux_amd64": ["gssapi.go", "get_user.go"],
"//conditions:default": ["empty.go"],
}),
cdeps = select({
@@ -26,6 +26,7 @@ go_library(
"//pkg/sql/sem/tree",
"//pkg/sql/pgwire",
"//pkg/sql/pgwire/hba",
+ "//pkg/sql/pgwire/identmap",
"@com_github_cockroachdb_errors//:errors",
],
"//conditions:default": [],
diff --git a/pkg/ccl/gssapiccl/get_user.go b/pkg/ccl/gssapiccl/get_user.go
new file mode 100644
index 000000000000..f29bc7c02331
--- /dev/null
+++ b/pkg/ccl/gssapiccl/get_user.go
@@ -0,0 +1,128 @@
+// Copyright 2021 The Cockroach Authors.
+//
+// Licensed as a CockroachDB Enterprise file under the Cockroach Community
+// License (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt
+
+// See comment on build tag in gssapi.go.
+
+//go:build gss
+// +build gss
+
+package gssapiccl
+
+// This file contains the code that calls out to the GSSAPI library
+// to retrieve the current user.
+
+import (
+ "unsafe"
+
+ "github.com/cockroachdb/cockroach/pkg/sql/pgwire"
+ "github.com/cockroachdb/errors"
+)
+
+// #cgo LDFLAGS: -lgssapi_krb5 -lcom_err -lkrb5 -lkrb5support -ldl -lk5crypto -lresolv
+//
+// #include
+// #include
+import "C"
+
+func getGssUser(c pgwire.AuthConn) (connClose func(), gssUser string, _ error) {
+ var (
+ majStat, minStat, lminS, gflags C.OM_uint32
+ gbuf C.gss_buffer_desc
+ contextHandle C.gss_ctx_id_t = C.GSS_C_NO_CONTEXT
+ acceptorCredHandle C.gss_cred_id_t = C.GSS_C_NO_CREDENTIAL
+ srcName C.gss_name_t
+ outputToken C.gss_buffer_desc
+ )
+
+ if err := c.SendAuthRequest(authTypeGSS, nil); err != nil {
+ return nil, "", err
+ }
+
+ // This cleanup function must be called at the
+ // "completion of a communications session", not
+ // merely at the end of an authentication init. See
+ // https://tools.ietf.org/html/rfc2744.html, section
+ // `1. Introduction`, stage `d`:
+ //
+ // At the completion of a communications session (which
+ // may extend across several transport connections),
+ // each application calls a GSS-API routine to delete
+ // the security context.
+ //
+ // See https://github.com/postgres/postgres/blob/f4d59369d2ddf0ad7850112752ec42fd115825d4/src/backend/libpq/pqcomm.c#L269
+ connClose = func() {
+ C.gss_delete_sec_context(&lminS, &contextHandle, C.GSS_C_NO_BUFFER)
+ }
+
+ for {
+ token, err := c.GetPwdData()
+ if err != nil {
+ return connClose, "", err
+ }
+
+ gbuf.length = C.ulong(len(token))
+ gbuf.value = C.CBytes([]byte(token))
+
+ majStat = C.gss_accept_sec_context(
+ &minStat,
+ &contextHandle,
+ acceptorCredHandle,
+ &gbuf,
+ C.GSS_C_NO_CHANNEL_BINDINGS,
+ &srcName,
+ nil,
+ &outputToken,
+ &gflags,
+ nil,
+ nil,
+ )
+ C.free(unsafe.Pointer(gbuf.value))
+
+ if outputToken.length != 0 {
+ outputBytes := C.GoBytes(outputToken.value, C.int(outputToken.length))
+ C.gss_release_buffer(&lminS, &outputToken)
+ if err := c.SendAuthRequest(authTypeGSSContinue, outputBytes); err != nil {
+ return connClose, "", err
+ }
+ }
+ if majStat != C.GSS_S_COMPLETE && majStat != C.GSS_S_CONTINUE_NEEDED {
+ return connClose, "", gssError("accepting GSS security context failed", majStat, minStat)
+ }
+ if majStat != C.GSS_S_CONTINUE_NEEDED {
+ break
+ }
+ }
+
+ majStat = C.gss_display_name(&minStat, srcName, &gbuf, nil)
+ if majStat != C.GSS_S_COMPLETE {
+ return connClose, "", gssError("retrieving GSS user name failed", majStat, minStat)
+ }
+ gssUser = C.GoStringN((*C.char)(gbuf.value), C.int(gbuf.length))
+ C.gss_release_buffer(&lminS, &gbuf)
+
+ return connClose, gssUser, nil
+}
+
+func gssError(msg string, majStat, minStat C.OM_uint32) error {
+ var (
+ gmsg C.gss_buffer_desc
+ lminS, msgCtx C.OM_uint32
+ )
+
+ msgCtx = 0
+ C.gss_display_status(&lminS, majStat, C.GSS_C_GSS_CODE, C.GSS_C_NO_OID, &msgCtx, &gmsg)
+ msgMajor := C.GoString((*C.char)(gmsg.value))
+ C.gss_release_buffer(&lminS, &gmsg)
+
+ msgCtx = 0
+ C.gss_display_status(&lminS, minStat, C.GSS_C_MECH_CODE, C.GSS_C_NO_OID, &msgCtx, &gmsg)
+ msgMinor := C.GoString((*C.char)(gmsg.value))
+ C.gss_release_buffer(&lminS, &gmsg)
+
+ return errors.Errorf("%s: %s: %s", msg, msgMajor, msgMinor)
+}
diff --git a/pkg/ccl/gssapiccl/gssapi.go b/pkg/ccl/gssapiccl/gssapi.go
index b1a34c67c212..8099db712133 100644
--- a/pkg/ccl/gssapiccl/gssapi.go
+++ b/pkg/ccl/gssapiccl/gssapi.go
@@ -18,23 +18,16 @@ import (
"context"
"crypto/tls"
"strings"
- "unsafe"
"github.com/cockroachdb/cockroach/pkg/ccl/utilccl"
"github.com/cockroachdb/cockroach/pkg/security"
"github.com/cockroachdb/cockroach/pkg/sql"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/hba"
- "github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
+ "github.com/cockroachdb/cockroach/pkg/sql/pgwire/identmap"
"github.com/cockroachdb/errors"
)
-// #cgo LDFLAGS: -lgssapi_krb5 -lcom_err -lkrb5 -lkrb5support -ldl -lk5crypto -lresolv
-//
-// #include
-// #include
-import "C"
-
const (
authTypeGSS int32 = 7
authTypeGSSContinue int32 = 8
@@ -43,97 +36,55 @@ const (
// authGSS performs GSS authentication. See:
// https://github.com/postgres/postgres/blob/0f9cdd7dca694d487ab663d463b308919f591c02/src/backend/libpq/auth.c#L1090
func authGSS(
- ctx context.Context,
+ _ context.Context,
c pgwire.AuthConn,
- tlsState tls.ConnectionState,
- _ pgwire.PasswordRetrievalFn,
- _ *tree.DTimestamp,
+ _ tls.ConnectionState,
execCfg *sql.ExecutorConfig,
entry *hba.Entry,
-) (security.UserAuthHook, error) {
- return func(ctx context.Context, requestedUser security.SQLUsername, clientConnection bool) (func(), error) {
- var (
- majStat, minStat, lminS, gflags C.OM_uint32
- gbuf C.gss_buffer_desc
- contextHandle C.gss_ctx_id_t = C.GSS_C_NO_CONTEXT
- acceptorCredHandle C.gss_cred_id_t = C.GSS_C_NO_CREDENTIAL
- srcName C.gss_name_t
- outputToken C.gss_buffer_desc
-
- token []byte
- err error
- )
-
- if err = c.SendAuthRequest(authTypeGSS, nil); err != nil {
- return nil, err
- }
-
- // This cleanup function must be called at the
- // "completion of a communications session", not
- // merely at the end of an authentication init. See
- // https://tools.ietf.org/html/rfc2744.html, section
- // `1. Introduction`, stage `d`:
- //
- // At the completion of a communications session (which
- // may extend across several transport connections),
- // each application calls a GSS-API routine to delete
- // the security context.
- //
- // See https://github.com/postgres/postgres/blob/f4d59369d2ddf0ad7850112752ec42fd115825d4/src/backend/libpq/pqcomm.c#L269
- connClose := func() {
- C.gss_delete_sec_context(&lminS, &contextHandle, C.GSS_C_NO_BUFFER)
- }
-
- for {
- token, err = c.GetPwdData()
- if err != nil {
- return connClose, err
- }
+ identMap *identmap.Conf,
+) (*pgwire.AuthBehaviors, error) {
+ behaviors := &pgwire.AuthBehaviors{}
+
+ connClose, gssUser, err := getGssUser(c)
+ behaviors.SetConnClose(connClose)
+ if err != nil {
+ return behaviors, err
+ }
- gbuf.length = C.ulong(len(token))
- gbuf.value = C.CBytes([]byte(token))
-
- majStat = C.gss_accept_sec_context(
- &minStat,
- &contextHandle,
- acceptorCredHandle,
- &gbuf,
- C.GSS_C_NO_CHANNEL_BINDINGS,
- &srcName,
- nil,
- &outputToken,
- &gflags,
- nil,
- nil,
- )
- C.free(unsafe.Pointer(gbuf.value))
-
- if outputToken.length != 0 {
- outputBytes := C.GoBytes(outputToken.value, C.int(outputToken.length))
- C.gss_release_buffer(&lminS, &outputToken)
- if err = c.SendAuthRequest(authTypeGSSContinue, outputBytes); err != nil {
- return connClose, err
- }
- }
- if majStat != C.GSS_S_COMPLETE && majStat != C.GSS_S_CONTINUE_NEEDED {
- return connClose, gssError("accepting GSS security context failed", majStat, minStat)
- }
- if majStat != C.GSS_S_CONTINUE_NEEDED {
- break
- }
- }
+ // Update the incoming connection with the GSS username. We'll expect
+ // to see this value come back to the mapper function below.
+ if u, err := security.MakeSQLUsernameFromUserInput(gssUser, security.UsernameValidation); err != nil {
+ return nil, err
+ } else {
+ behaviors.SetReplacementIdentity(u)
+ }
- majStat = C.gss_display_name(&minStat, srcName, &gbuf, nil)
- if majStat != C.GSS_S_COMPLETE {
- return connClose, gssError("retrieving GSS user name failed", majStat, minStat)
+ // We enforce that the "map" and/or "include_realm=0" options are set
+ // in the HBA validation function below.
+ include0 := entry.GetOption("include_realm") == "0"
+ if entry.GetOption("map") != "" {
+ mapper := pgwire.HbaMapper(entry, identMap)
+ // Per behavior in PostgreSQL, combining both map and
+ // include_realm=0 means that the incoming principal is stripped,
+ // then the map is applied. See also:
+ // https://github.com/postgres/postgres/blob/4ac0f450b698442c3273ddfe8eed0e1a7e56645f/src/backend/libpq/auth.c#L1474
+ if include0 {
+ mapper = stripAndDelegateMapper(mapper)
}
- gssUser := C.GoStringN((*C.char)(gbuf.value), C.int(gbuf.length))
- C.gss_release_buffer(&lminS, &gbuf)
-
- realms := entry.GetOptions("krb_realm")
+ behaviors.SetRoleMapper(mapper)
+ } else if include0 {
+ // Strip the trailing realm information, if any, from the gssapi username.
+ behaviors.SetRoleMapper(stripRealmMapper)
+ } else {
+ return nil, errors.New("unsupported HBA entry configuration")
+ }
- if idx := strings.IndexByte(gssUser, '@'); idx >= 0 {
- if len(realms) > 0 {
+ behaviors.SetAuthenticator(func(
+ _ context.Context, _ security.SQLUsername, _ bool, _ pgwire.PasswordRetrievalFn,
+ ) error {
+ // Enforce krb_realm option, if any.
+ if realms := entry.GetOptions("krb_realm"); len(realms) > 0 {
+ if idx := strings.IndexByte(gssUser, '@'); idx >= 0 {
realm := gssUser[idx+1:]
matched := false
for _, krbRealm := range realms {
@@ -143,50 +94,27 @@ func authGSS(
}
}
if !matched {
- return connClose, errors.Errorf("GSSAPI realm (%s) didn't match any configured realm", realm)
+ return errors.Errorf("GSSAPI realm (%s) didn't match any configured realm", realm)
}
+ } else {
+ return errors.New("GSSAPI did not return realm but realm matching was requested")
}
- if entry.GetOption("include_realm") != "1" {
- gssUser = gssUser[:idx]
- }
- } else if len(realms) > 0 {
- return connClose, errors.New("GSSAPI did not return realm but realm matching was requested")
- }
-
- gssUsername, _ := security.MakeSQLUsernameFromUserInput(gssUser, security.UsernameValidation)
- if gssUsername != requestedUser {
- return connClose, errors.Errorf("requested user is %s, but GSSAPI auth is for %s", requestedUser, gssUser)
}
// Do the license check last so that administrators are able to test whether
// their GSS configuration is correct. That is, the presence of this error
// message means they have a correctly functioning GSS/Kerberos setup,
// but now need to enable enterprise features.
- return connClose, utilccl.CheckEnterpriseEnabled(execCfg.Settings, execCfg.ClusterID(), execCfg.Organization(), "GSS authentication")
- }, nil
-}
-
-func gssError(msg string, majStat, minStat C.OM_uint32) error {
- var (
- gmsg C.gss_buffer_desc
- lminS, msgCtx C.OM_uint32
- )
-
- msgCtx = 0
- C.gss_display_status(&lminS, majStat, C.GSS_C_GSS_CODE, C.GSS_C_NO_OID, &msgCtx, &gmsg)
- msgMajor := C.GoString((*C.char)(gmsg.value))
- C.gss_release_buffer(&lminS, &gmsg)
-
- msgCtx = 0
- C.gss_display_status(&lminS, minStat, C.GSS_C_MECH_CODE, C.GSS_C_NO_OID, &msgCtx, &gmsg)
- msgMinor := C.GoString((*C.char)(gmsg.value))
- C.gss_release_buffer(&lminS, &gmsg)
-
- return errors.Errorf("%s: %s: %s", msg, msgMajor, msgMinor)
+ return utilccl.CheckEnterpriseEnabled(execCfg.Settings, execCfg.ClusterID(), execCfg.Organization(), "GSS authentication")
+ })
+ return behaviors, nil
}
+// checkEntry validates that the HBA entry contains exactly one of the
+// include_realm=0 directive or an identity-mapping configuration.
func checkEntry(entry hba.Entry) error {
hasInclude0 := false
+ hasMap := false
for _, op := range entry.Options {
switch op[0] {
case "include_realm":
@@ -196,16 +124,50 @@ func checkEntry(entry hba.Entry) error {
return errors.Errorf("include_realm must be set to 0: %s", op[1])
}
case "krb_realm":
+ // OK.
+ case "map":
+ hasMap = true
default:
return errors.Errorf("unsupported option %s", op[0])
}
}
- if !hasInclude0 {
- return errors.New(`missing "include_realm=0" option in GSS entry`)
+ if !hasMap && !hasInclude0 {
+ return errors.New(`at least one of "include_realm=0" or "map" options required`)
}
return nil
}
+// stripRealm removes the realm data, if any, from the provided username.
+func stripRealm(u security.SQLUsername) (security.SQLUsername, error) {
+ norm := u.Normalized()
+ if idx := strings.Index(norm, "@"); idx != -1 {
+ norm = norm[:idx]
+ }
+ return security.MakeSQLUsernameFromUserInput(norm, security.UsernameValidation)
+}
+
+// stripRealmMapper is a pgwire.RoleMapper that just strips the trailing
+// realm information, if any, from the gssapi username.
+func stripRealmMapper(
+ _ context.Context, systemIdentity security.SQLUsername,
+) ([]security.SQLUsername, error) {
+ ret, err := stripRealm(systemIdentity)
+ return []security.SQLUsername{ret}, err
+}
+
+// stripAndDelegateMapper wraps a delegate pgwire.RoleMapper such that
+// the incoming identity has its realm information stripped before the
+// next mapping is applied.
+func stripAndDelegateMapper(delegate pgwire.RoleMapper) pgwire.RoleMapper {
+ return func(ctx context.Context, systemIdentity security.SQLUsername) ([]security.SQLUsername, error) {
+ next, err := stripRealm(systemIdentity)
+ if err != nil {
+ return nil, err
+ }
+ return delegate(ctx, next)
+ }
+}
+
func init() {
pgwire.RegisterAuthMethod("gss", authGSS, hba.ConnHostSSL, checkEntry)
}
diff --git a/pkg/security/auth.go b/pkg/security/auth.go
index 709501757a1b..d5486ddaef10 100644
--- a/pkg/security/auth.go
+++ b/pkg/security/auth.go
@@ -28,7 +28,15 @@ var certPrincipalMap struct {
// 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.
-type UserAuthHook func(context.Context, SQLUsername, bool) (connClose func(), _ error)
+//
+// 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 :. The principal
@@ -110,35 +118,34 @@ func UserAuthCertHook(insecureMode bool, tlsState *tls.ConnectionState) (UserAut
}
}
- return func(ctx context.Context, requestedUser SQLUsername, clientConnection bool) (func(), error) {
+ return func(ctx context.Context, systemIdentity SQLUsername, clientConnection bool) error {
// TODO(marc): we may eventually need stricter user syntax rules.
- if requestedUser.Undefined() {
- return nil, errors.New("user is missing")
+ if systemIdentity.Undefined() {
+ return errors.New("user is missing")
}
- if !clientConnection && !requestedUser.IsNodeUser() {
- return nil, errors.Errorf("user %s is not allowed", requestedUser)
+ 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, nil
+ 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 nil,
- errors.Errorf("using tenant client certificate as user certificate is not allowed")
+ return errors.Errorf("using tenant client certificate as user certificate is not allowed")
}
// The client certificate user must match the requested user.
- if !Contains(certUsers, requestedUser.Normalized()) {
- return nil, errors.Errorf("requested user is %s, but certificate is for %s", requestedUser, certUsers)
+ if !Contains(certUsers, systemIdentity.Normalized()) {
+ return errors.Errorf("requested user is %s, but certificate is for %s", systemIdentity, certUsers)
}
- return nil, nil
+ return nil
}, nil
}
@@ -151,25 +158,25 @@ func IsTenantCertificate(cert *x509.Certificate) bool {
// UserAuthPasswordHook builds an authentication hook based on the security
// mode, password, and its potentially matching hash.
func UserAuthPasswordHook(insecureMode bool, password string, hashedPassword []byte) UserAuthHook {
- return func(ctx context.Context, requestedUser SQLUsername, clientConnection bool) (func(), error) {
- if requestedUser.Undefined() {
- return nil, errors.New("user is missing")
+ return func(ctx context.Context, systemIdentity SQLUsername, clientConnection bool) error {
+ if systemIdentity.Undefined() {
+ return errors.New("user is missing")
}
if !clientConnection {
- return nil, errors.New("password authentication is only available for client connections")
+ return errors.New("password authentication is only available for client connections")
}
if insecureMode {
- return nil, nil
+ return nil
}
// If the requested user has an empty password, disallow authentication.
if len(password) == 0 || CompareHashAndPassword(ctx, hashedPassword, password) != nil {
- return nil, errors.Errorf(ErrPasswordUserAuthFailed, requestedUser)
+ return errors.Errorf(ErrPasswordUserAuthFailed, systemIdentity)
}
- return nil, nil
+ return nil
}
}
diff --git a/pkg/security/auth_test.go b/pkg/security/auth_test.go
index 94c5cadf3b16..def5a48a6aa7 100644
--- a/pkg/security/auth_test.go
+++ b/pkg/security/auth_test.go
@@ -224,11 +224,11 @@ func TestAuthenticationHook(t *testing.T) {
if err != nil {
return
}
- _, err = hook(ctx, tc.username, true /* clientConnection */)
+ err = hook(ctx, tc.username, true /* clientConnection */)
if (err == nil) != tc.publicHookSuccess {
t.Fatalf("expected success=%t, got err=%v", tc.publicHookSuccess, err)
}
- _, err = hook(ctx, tc.username, false /* clientConnection */)
+ err = hook(ctx, tc.username, false /* clientConnection */)
if (err == nil) != tc.privateHookSuccess {
t.Fatalf("expected success=%t, got err=%v", tc.privateHookSuccess, err)
}
diff --git a/pkg/sql/pgwire/BUILD.bazel b/pkg/sql/pgwire/BUILD.bazel
index 38e80198ade1..effd7a262f8b 100644
--- a/pkg/sql/pgwire/BUILD.bazel
+++ b/pkg/sql/pgwire/BUILD.bazel
@@ -4,10 +4,14 @@ go_library(
name = "pgwire",
srcs = [
"auth.go",
+ "auth_behaviors.go",
"auth_methods.go",
+ "authenticator.go",
"command_result.go",
"conn.go",
"hba_conf.go",
+ "ident_map_conf.go",
+ "role_mapper.go",
"server.go",
"types.go",
"write_buffer.go",
@@ -28,6 +32,7 @@ go_library(
"//pkg/sql/lex",
"//pkg/sql/parser",
"//pkg/sql/pgwire/hba",
+ "//pkg/sql/pgwire/identmap",
"//pkg/sql/pgwire/pgcode",
"//pkg/sql/pgwire/pgerror",
"//pkg/sql/pgwire/pgnotice",
@@ -95,6 +100,7 @@ go_test(
"//pkg/sql/colconv",
"//pkg/sql/parser",
"//pkg/sql/pgwire/hba",
+ "//pkg/sql/pgwire/identmap",
"//pkg/sql/pgwire/pgcode",
"//pkg/sql/pgwire/pgerror",
"//pkg/sql/pgwire/pgwirebase",
diff --git a/pkg/sql/pgwire/auth.go b/pkg/sql/pgwire/auth.go
index 8ed79ca2c407..3c3d19d24fda 100644
--- a/pkg/sql/pgwire/auth.go
+++ b/pkg/sql/pgwire/auth.go
@@ -20,9 +20,11 @@ import (
"github.com/cockroachdb/cockroach/pkg/security"
"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/sem/tree"
"github.com/cockroachdb/cockroach/pkg/util/log"
"github.com/cockroachdb/cockroach/pkg/util/log/eventpb"
"github.com/cockroachdb/errors"
@@ -51,6 +53,10 @@ type authOptions struct {
// 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
// ie is the server-wide internal executor, used to
// retrieve entries from system.users.
ie *sql.InternalExecutor
@@ -89,60 +95,98 @@ func (c *conn) handleAuthentication(
return err
}
+ // 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, sendError(pgerror.WithCandidateCode(err, pgcode.InvalidAuthorizationSpecification))
+ }
+
+ ac.SetAuthMethod(hbaEntry.Method.String())
+ ac.LogAuthInfof(ctx, "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, sendError(pgerror.WithCandidateCode(err, pgcode.InvalidAuthorizationSpecification))
+ }
+
+ // Choose the system identity that we'll use below for mapping
+ // externally-provisioned principals to database users.
+ var systemIdentity security.SQLUsername
+ if found, ok := behaviors.ReplacementIdentity(); ok {
+ systemIdentity = found
+ ac.SetSystemIdentity(systemIdentity)
+ } else {
+ systemIdentity = c.sessionArgs.User
+ }
+
+ // Delegate to the AuthMethod's MapRole to choose the actual
+ // database user that a successful authentication will result in.
+ if err := c.chooseDbRole(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, sendError(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, canLogin, isSuperuser, validUntil, defaultSettings, pwRetrievalFn, err := sql.GetUserSessionInitInfo(
- ctx,
- execCfg,
- authOpt.ie,
- c.sessionArgs.User,
- c.sessionArgs.SessionDefaults["database"],
- )
+ exists, canLogin, isSuperuser, validUntil, defaultSettings, pwRetrievalFn, err :=
+ sql.GetUserSessionInitInfo(
+ ctx,
+ execCfg,
+ authOpt.ie,
+ dbUser,
+ c.sessionArgs.SessionDefaults["database"],
+ )
if err != nil {
- log.Warningf(ctx, "user retrieval failed for user=%q: %+v", c.sessionArgs.User, err)
+ log.Warningf(ctx, "user retrieval failed for user=%q: %+v", dbUser, err)
ac.LogAuthFailed(ctx, eventpb.AuthFailReason_USER_RETRIEVAL_ERROR, err)
- return nil, sendError(pgerror.WithCandidateCode(err, pgcode.InvalidAuthorizationSpecification))
+ return connClose, sendError(pgerror.WithCandidateCode(err, pgcode.InvalidAuthorizationSpecification))
}
c.sessionArgs.IsSuperuser = isSuperuser
if !exists {
ac.LogAuthFailed(ctx, eventpb.AuthFailReason_USER_NOT_FOUND, nil)
- return nil, sendError(pgerror.Newf(
+ return connClose, sendError(pgerror.Newf(
pgcode.InvalidAuthorizationSpecification,
security.ErrPasswordUserAuthFailed,
- c.sessionArgs.User,
+ dbUser,
))
}
if !canLogin {
ac.LogAuthFailed(ctx, eventpb.AuthFailReason_LOGIN_DISABLED, nil)
- return nil, sendError(pgerror.Newf(
+ return connClose, sendError(pgerror.Newf(
pgcode.InvalidAuthorizationSpecification,
"%s does not have login privilege",
- c.sessionArgs.User,
+ dbUser,
))
}
- // Retrieve the authentication method.
- tlsState, hbaEntry, methodFn, err := c.findAuthenticationMethod(authOpt)
if err != nil {
ac.LogAuthFailed(ctx, eventpb.AuthFailReason_METHOD_NOT_FOUND, err)
- return nil, sendError(pgerror.WithCandidateCode(err, pgcode.InvalidAuthorizationSpecification))
+ return connClose, sendError(pgerror.WithCandidateCode(err, pgcode.InvalidAuthorizationSpecification))
}
- ac.SetAuthMethod(hbaEntry.Method.String())
- ac.LogAuthInfof(ctx, "HBA rule: %s", hbaEntry.Input)
-
- // Ask the method to authenticate.
- authenticationHook, err := methodFn(ctx, ac, tlsState, pwRetrievalFn,
- validUntil, execCfg, hbaEntry)
-
- if err != nil {
- ac.LogAuthFailed(ctx, eventpb.AuthFailReason_METHOD_NOT_FOUND, err)
- return nil, sendError(pgerror.WithCandidateCode(err, pgcode.InvalidAuthorizationSpecification))
+ // Set up lazy provider for password or cert-password methods.
+ pwDataFn := func(ctx context.Context) ([]byte, *tree.DTimestamp, error) {
+ pwHash, err := pwRetrievalFn(ctx)
+ return pwHash, validUntil, err
}
- if connClose, err = authenticationHook(ctx, c.sessionArgs.User, true /* public */); err != nil {
+ // 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.
+ if err := behaviors.Authenticate(ctx, systemIdentity, true /* public */, pwDataFn); err != nil {
ac.LogAuthFailed(ctx, eventpb.AuthFailReason_CREDENTIALS_INVALID, err)
return connClose, sendError(pgerror.WithCandidateCode(err, pgcode.InvalidAuthorizationSpecification))
}
@@ -157,11 +201,11 @@ func (c *conn) handleAuthentication(
for _, setting := range settingEntry.Settings {
keyVal := strings.SplitN(setting, "=", 2)
if len(keyVal) != 2 {
- log.Ops.Warningf(ctx, "%s has malformed default setting: %q", c.sessionArgs.User, setting)
+ 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", c.sessionArgs.User, err)
+ log.Ops.Warningf(ctx, "%s has invalid default setting: %v", dbUser, err)
continue
}
if _, ok := c.sessionArgs.SessionDefaults[keyVal[0]]; !ok {
@@ -177,6 +221,29 @@ func (c *conn) handleAuthentication(
return connClose, c.msgBuilder.finishMsg(c.conn)
}
+// chooseDbRole uses the provided RoleMapper to map an incoming
+// system identity to an actual database role. If a mapping is present,
+// the sessionArgs.User field will be updated.
+//
+// TODO(#sql-security): The docs for the pg_ident.conf file state that
+// if there are multiple mappings for an incoming system-user, the
+// session should act with the union of all roles granted to the mapped
+// database users. We're going to go with a first-one-wins approach
+// until the session can have multiple roles.
+func (c *conn) chooseDbRole(
+ ctx context.Context, ac AuthConn, mapper RoleMapper, systemIdentity security.SQLUsername,
+) error {
+ if mapped, err := mapper(ctx, systemIdentity); err != nil {
+ return err
+ } else if len(mapped) == 0 {
+ return errors.Newf("system identity %q did not map to a database role", systemIdentity.Normalized())
+ } else {
+ c.sessionArgs.User = mapped[0]
+ ac.SetDbUser(mapped[0])
+ }
+ return nil
+}
+
func (c *conn) findAuthenticationMethod(
authOpt authOptions,
) (tlsState tls.ConnectionState, hbaEntry *hba.Entry, methodFn AuthMethod, err error) {
@@ -301,6 +368,13 @@ type AuthConn interface {
// 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 security.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 security.SQLUsername)
// LogAuthInfof logs details about the progress of the
// authentication.
LogAuthInfof(ctx context.Context, format string, args ...interface{})
@@ -332,14 +406,16 @@ type authRes struct {
err error
}
-func newAuthPipe(c *conn, logAuthn bool, authOpt authOptions, user security.SQLUsername) *authPipe {
+func newAuthPipe(
+ c *conn, logAuthn bool, authOpt authOptions, systemIdentity security.SQLUsername,
+) *authPipe {
ap := &authPipe{
c: c,
log: logAuthn,
connDetails: authOpt.connDetails,
authDetails: eventpb.CommonSessionDetails{
- Transport: authOpt.connType.String(),
- User: user.Normalized(),
+ SystemIdentity: systemIdentity.Normalized(),
+ Transport: authOpt.connType.String(),
},
ch: make(chan []byte),
writerDone: make(chan struct{}),
@@ -392,6 +468,14 @@ func (p *authPipe) SetAuthMethod(method string) {
p.authMethod = method
}
+func (p *authPipe) SetDbUser(dbUser security.SQLUsername) {
+ p.authDetails.User = dbUser.Normalized()
+}
+
+func (p *authPipe) SetSystemIdentity(systemIdentity security.SQLUsername) {
+ p.authDetails.SystemIdentity = systemIdentity.Normalized()
+}
+
func (p *authPipe) LogAuthOK(ctx context.Context) {
if p.log {
ev := &eventpb.ClientAuthenticationOk{
diff --git a/pkg/sql/pgwire/auth_behaviors.go b/pkg/sql/pgwire/auth_behaviors.go
new file mode 100644
index 000000000000..7cf3d370dee2
--- /dev/null
+++ b/pkg/sql/pgwire/auth_behaviors.go
@@ -0,0 +1,110 @@
+// Copyright 2021 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"
+
+ "github.com/cockroachdb/cockroach/pkg/security"
+ "github.com/cockroachdb/errors"
+)
+
+// AuthBehaviors encapsulates the per-connection behaviors that may be
+// configured. This type is returned by AuthMethod implementations.
+//
+// Callers should call the AuthBehaviors.ConnClose method once the
+// associated network connection has been terminated to allow external
+// resources to be released.
+type AuthBehaviors struct {
+ authenticator Authenticator
+ connClose func()
+ replacementIdentity security.SQLUsername
+ replacedIdentity bool
+ roleMapper RoleMapper
+}
+
+// Ensure that an AuthBehaviors is easily composable with itself.
+var _ Authenticator = (*AuthBehaviors)(nil).Authenticate
+var _ func() = (*AuthBehaviors)(nil).ConnClose
+var _ RoleMapper = (*AuthBehaviors)(nil).MapRole
+
+// This is a hack for the unused-symbols linter. These two functions
+// are, at present, only called by the GSSAPI integration. The code
+// is guarded by a build tag, which is ignored by the linter.
+// TODO(#dev-inf): Update the linter to include the "gss" build tag.
+var _ = (*AuthBehaviors)(nil).SetConnClose
+var _ = (*AuthBehaviors)(nil).SetReplacementIdentity
+
+// Authenticate delegates to the Authenticator passed to
+// SetAuthenticator or returns an error if SetAuthenticator has not been
+// called.
+func (b *AuthBehaviors) Authenticate(
+ ctx context.Context,
+ systemIdentity security.SQLUsername,
+ clientConnection bool,
+ pwRetrieveFn PasswordRetrievalFn,
+) error {
+ if found := b.authenticator; found != nil {
+ return found(ctx, systemIdentity, clientConnection, pwRetrieveFn)
+ }
+ return errors.New("no Authenticator provided to AuthBehaviors")
+}
+
+// SetAuthenticator updates the Authenticator to be used.
+func (b *AuthBehaviors) SetAuthenticator(a Authenticator) {
+ b.authenticator = a
+}
+
+// ConnClose delegates to the function passed to SetConnClose to release
+// any resources associated with the connection. This method is a no-op
+// if SetConnClose has not been called or was called with nil.
+func (b *AuthBehaviors) ConnClose() {
+ if fn := b.connClose; fn != nil {
+ fn()
+ }
+}
+
+// SetConnClose updates the connection-close callback.
+func (b *AuthBehaviors) SetConnClose(fn func()) {
+ b.connClose = fn
+}
+
+// ReplacementIdentity returns an optional replacement for the
+// client-provided identity when validating the incoming connection.
+// This allows "ambient" authentication mechanisms, such as GSSAPI, to
+// provide replacement values. This method will return ok==false if
+// SetReplacementIdentity has not been called.
+func (b *AuthBehaviors) ReplacementIdentity() (_ security.SQLUsername, ok bool) {
+ return b.replacementIdentity, b.replacedIdentity
+}
+
+// SetReplacementIdentity allows the AuthMethod to override the
+// client-reported system identity.
+func (b *AuthBehaviors) SetReplacementIdentity(id security.SQLUsername) {
+ b.replacementIdentity = id
+ b.replacedIdentity = true
+}
+
+// MapRole delegates to the RoleMapper passed to SetRoleMapper or
+// returns an error if SetRoleMapper has not been called.
+func (b *AuthBehaviors) MapRole(
+ ctx context.Context, systemIdentity security.SQLUsername,
+) ([]security.SQLUsername, error) {
+ if found := b.roleMapper; found != nil {
+ return found(ctx, systemIdentity)
+ }
+ return nil, errors.New("no RoleMapper provided to AuthBehaviors")
+}
+
+// SetRoleMapper updates the RoleMapper to be used.
+func (b *AuthBehaviors) SetRoleMapper(m RoleMapper) {
+ b.roleMapper = m
+}
diff --git a/pkg/sql/pgwire/auth_methods.go b/pkg/sql/pgwire/auth_methods.go
index a5c4437ad35e..543992a123bf 100644
--- a/pkg/sql/pgwire/auth_methods.go
+++ b/pkg/sql/pgwire/auth_methods.go
@@ -19,6 +19,7 @@ import (
"github.com/cockroachdb/cockroach/pkg/security"
"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/sem/tree"
"github.com/cockroachdb/cockroach/pkg/util/log/eventpb"
"github.com/cockroachdb/cockroach/pkg/util/timeutil"
@@ -41,7 +42,7 @@ func loadDefaultMethods() {
//
// Care should be taken by administrators to only accept this auth
// method over secure connections, e.g. those encrypted using SSL.
- RegisterAuthMethod("password", authPassword, hba.ConnAny, nil)
+ RegisterAuthMethod("password", authPassword, hba.ConnAny, NoOptionsAllowed)
// The "cert" method requires a valid client certificate for the
// user attempting to connect.
@@ -56,66 +57,70 @@ func loadDefaultMethods() {
// The "reject" method rejects any connection attempt that matches
// the current rule.
- RegisterAuthMethod("reject", authReject, hba.ConnAny, nil)
+ RegisterAuthMethod("reject", authReject, hba.ConnAny, NoOptionsAllowed)
// The "trust" method accepts any connection attempt that matches
// the current rule.
- RegisterAuthMethod("trust", authTrust, hba.ConnAny, nil)
+ RegisterAuthMethod("trust", authTrust, hba.ConnAny, NoOptionsAllowed)
}
-// AuthMethod defines a method for authentication of a connection.
-type AuthMethod func(
+// AuthMethod is a top-level factory for composing the various
+// functionality needed to authenticate an incoming connection.
+type AuthMethod = func(
ctx context.Context,
c AuthConn,
tlsState tls.ConnectionState,
- pwRetrieveFn PasswordRetrievalFn,
- pwValidUntil *tree.DTimestamp,
execCfg *sql.ExecutorConfig,
entry *hba.Entry,
-) (security.UserAuthHook, error)
-
-// PasswordRetrievalFn defines a method to retrieve the hashed
-// password for the user logging in.
-type PasswordRetrievalFn = func(context.Context) ([]byte, error)
+ identMap *identmap.Conf,
+) (*AuthBehaviors, error)
func authPassword(
- ctx context.Context,
+ _ context.Context,
c AuthConn,
_ tls.ConnectionState,
- pwRetrieveFn PasswordRetrievalFn,
- pwValidUntil *tree.DTimestamp,
_ *sql.ExecutorConfig,
_ *hba.Entry,
-) (security.UserAuthHook, error) {
- if err := c.SendAuthRequest(authCleartextPassword, nil /* data */); err != nil {
- return nil, err
- }
- pwdData, err := c.GetPwdData()
- if err != nil {
- return nil, err
- }
- password, err := passwordString(pwdData)
- if err != nil {
- return nil, err
- }
- hashedPassword, err := pwRetrieveFn(ctx)
- if err != nil {
- return nil, err
- }
- if len(hashedPassword) == 0 {
- c.LogAuthInfof(ctx, "user has no password defined")
- }
-
- if pwValidUntil != nil {
- if pwValidUntil.Sub(timeutil.Now()) < 0 {
- c.LogAuthFailed(ctx, eventpb.AuthFailReason_CREDENTIALS_EXPIRED, nil)
- return nil, errors.New("password is expired")
+ _ *identmap.Conf,
+) (*AuthBehaviors, error) {
+ b := &AuthBehaviors{}
+ b.SetRoleMapper(UseProvidedIdentity)
+ b.SetAuthenticator(func(
+ ctx context.Context,
+ systemIdentity security.SQLUsername,
+ clientConnection bool,
+ pwRetrieveFn PasswordRetrievalFn,
+ ) error {
+ if err := c.SendAuthRequest(authCleartextPassword, nil /* data */); err != nil {
+ return err
+ }
+ pwdData, err := c.GetPwdData()
+ if err != nil {
+ return err
+ }
+ password, err := passwordString(pwdData)
+ if err != nil {
+ return err
+ }
+ hashedPassword, pwValidUntil, err := pwRetrieveFn(ctx)
+ if err != nil {
+ return err
+ }
+ if len(hashedPassword) == 0 {
+ c.LogAuthInfof(ctx, "user has no password defined")
}
- }
- return security.UserAuthPasswordHook(
- false /*insecure*/, password, hashedPassword,
- ), nil
+ if pwValidUntil != nil {
+ if pwValidUntil.Sub(timeutil.Now()) < 0 {
+ c.LogAuthFailed(ctx, eventpb.AuthFailReason_CREDENTIALS_EXPIRED, nil)
+ return errors.New("password is expired")
+ }
+ }
+ return security.UserAuthPasswordHook(
+ false /*insecure*/, password, hashedPassword,
+ )(ctx, systemIdentity, clientConnection)
+ })
+ return b, nil
}
func passwordString(pwdData []byte) (string, error) {
@@ -130,30 +135,42 @@ func authCert(
_ context.Context,
_ AuthConn,
tlsState tls.ConnectionState,
- _ PasswordRetrievalFn,
- _ *tree.DTimestamp,
_ *sql.ExecutorConfig,
- _ *hba.Entry,
-) (security.UserAuthHook, error) {
- if len(tlsState.PeerCertificates) == 0 {
- return nil, errors.New("no TLS peer certificates, but required for auth")
- }
- // Normalize the username contained in the certificate.
- tlsState.PeerCertificates[0].Subject.CommonName = tree.Name(
- tlsState.PeerCertificates[0].Subject.CommonName,
- ).Normalize()
- return security.UserAuthCertHook(false /*insecure*/, &tlsState)
+ hbaEntry *hba.Entry,
+ identMap *identmap.Conf,
+) (*AuthBehaviors, error) {
+ b := &AuthBehaviors{}
+ b.SetRoleMapper(HbaMapper(hbaEntry, identMap))
+ b.SetAuthenticator(func(
+ ctx context.Context,
+ systemIdentity security.SQLUsername,
+ clientConnection bool,
+ pwRetrieveFn PasswordRetrievalFn,
+ ) error {
+ if len(tlsState.PeerCertificates) == 0 {
+ return errors.New("no TLS peer certificates, but required for auth")
+ }
+ // Normalize the username contained in the certificate.
+ tlsState.PeerCertificates[0].Subject.CommonName = tree.Name(
+ tlsState.PeerCertificates[0].Subject.CommonName,
+ ).Normalize()
+ hook, err := security.UserAuthCertHook(false /*insecure*/, &tlsState)
+ if err != nil {
+ return err
+ }
+ return hook(ctx, systemIdentity, clientConnection)
+ })
+ return b, nil
}
func authCertPassword(
ctx context.Context,
c AuthConn,
tlsState tls.ConnectionState,
- pwRetrieveFn PasswordRetrievalFn,
- pwValidUntil *tree.DTimestamp,
execCfg *sql.ExecutorConfig,
entry *hba.Entry,
-) (security.UserAuthHook, error) {
+ identMap *identmap.Conf,
+) (*AuthBehaviors, error) {
var fn AuthMethod
if len(tlsState.PeerCertificates) == 0 {
c.LogAuthInfof(ctx, "no client certificate, proceeding with password authentication")
@@ -162,31 +179,37 @@ func authCertPassword(
c.LogAuthInfof(ctx, "client presented certificate, proceeding with certificate validation")
fn = authCert
}
- return fn(ctx, c, tlsState, pwRetrieveFn, pwValidUntil, execCfg, entry)
+ return fn(ctx, c, tlsState, execCfg, entry, identMap)
}
func authTrust(
_ context.Context,
_ AuthConn,
_ tls.ConnectionState,
- _ PasswordRetrievalFn,
- _ *tree.DTimestamp,
_ *sql.ExecutorConfig,
_ *hba.Entry,
-) (security.UserAuthHook, error) {
- return func(_ context.Context, _ security.SQLUsername, _ bool) (func(), error) { return nil, nil }, nil
+ _ *identmap.Conf,
+) (*AuthBehaviors, error) {
+ b := &AuthBehaviors{}
+ b.SetRoleMapper(UseProvidedIdentity)
+ b.SetAuthenticator(func(_ context.Context, _ security.SQLUsername, _ bool, _ PasswordRetrievalFn) error {
+ return nil
+ })
+ return b, nil
}
func authReject(
_ context.Context,
_ AuthConn,
_ tls.ConnectionState,
- _ PasswordRetrievalFn,
- _ *tree.DTimestamp,
_ *sql.ExecutorConfig,
_ *hba.Entry,
-) (security.UserAuthHook, error) {
- return func(_ context.Context, _ security.SQLUsername, _ bool) (func(), error) {
- return nil, errors.New("authentication rejected by configuration")
- }, nil
+ _ *identmap.Conf,
+) (*AuthBehaviors, error) {
+ b := &AuthBehaviors{}
+ b.SetRoleMapper(UseProvidedIdentity)
+ b.SetAuthenticator(func(_ context.Context, _ security.SQLUsername, _ bool, _ PasswordRetrievalFn) error {
+ return errors.New("authentication rejected by configuration")
+ })
+ return b, nil
}
diff --git a/pkg/sql/pgwire/auth_test.go b/pkg/sql/pgwire/auth_test.go
index 19727a69c68f..bb8d80b3c6f8 100644
--- a/pkg/sql/pgwire/auth_test.go
+++ b/pkg/sql/pgwire/auth_test.go
@@ -32,6 +32,7 @@ import (
"github.com/cockroachdb/cockroach/pkg/security"
"github.com/cockroachdb/cockroach/pkg/server"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire"
+ "github.com/cockroachdb/cockroach/pkg/sql/pgwire/identmap"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
"github.com/cockroachdb/cockroach/pkg/testutils"
"github.com/cockroachdb/cockroach/pkg/testutils/serverutils"
@@ -69,6 +70,13 @@ import (
// The expected output is the configuration after parsing
// and reloading in the server.
//
+// set_identity_map
+//
+// Load the provided identity map via the cluster setting
+// server.identity_map.configuration.
+// The expected output is the configuration after parsing
+// and reloading in the server.
+//
// sql
//
// Execute the specified SQL statement using the default root
@@ -88,6 +96,7 @@ import (
// password - the password
// host - the server name/address
// port - the server port
+// force_certs - force the use of baked-in certificates
// sslmode, sslrootcert, sslcert, sslkey - SSL parameters.
//
// The order of k/v pairs matters: if the same key is specified
@@ -207,6 +216,13 @@ func hbaRunTest(t *testing.T, insecure bool) {
t.Fatal(err)
}
+ // This counter ensures that new log messages have become available
+ // between calls to the authlog command. It avoids the case where
+ // the last line from the previous authlog block also happens to
+ // match the regex for the current authlog block, leading to a
+ // desynchronization between the test script and the logfiles.
+ lastAuthLogCounter := uint64(0)
+
datadriven.RunTest(t, path, func(t *testing.T, td *datadriven.TestData) string {
resultString, err := func() (string, error) {
switch td.Cmd {
@@ -248,7 +264,7 @@ func hbaRunTest(t *testing.T, insecure bool) {
}
}
testutils.SucceedsSoon(t, func() error {
- curConf := pgServer.GetAuthenticationConfiguration()
+ curConf, _ := pgServer.GetAuthenticationConfiguration()
if expConf.String() != curConf.String() {
return errors.Newf(
"HBA config not yet loaded\ngot:\n%s\nexpected:\n%s",
@@ -270,6 +286,47 @@ func hbaRunTest(t *testing.T, insecure bool) {
}
return string(body), nil
+ case "set_identity_map":
+ _, err := conn.ExecContext(context.Background(),
+ `SET CLUSTER SETTING server.identity_map.configuration = $1`, td.Input)
+ if err != nil {
+ return "", err
+ }
+
+ // Wait until the configuration has propagated back to the
+ // test client. We need to wait because the cluster setting
+ // change propagates asynchronously.
+ expConf := identmap.Empty()
+ if td.Input != "" {
+ expConf, err = identmap.From(strings.NewReader(td.Input))
+ if err != nil {
+ // The SET above succeeded so we don't expect a problem here.
+ t.Fatal(err)
+ }
+ }
+ testutils.SucceedsSoon(t, func() error {
+ _, curConf := pgServer.GetAuthenticationConfiguration()
+ if expConf.String() != curConf.String() {
+ return errors.Newf(
+ "identity map not yet loaded\ngot:\n%s\nexpected:\n%s",
+ curConf, expConf)
+ }
+ return nil
+ })
+
+ // Verify the HBA configuration was processed properly by
+ // reporting the resulting cached configuration.
+ resp, err := httpClient.Get(httpHBAUrl)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+ return string(body), nil
+
case "sql":
_, err := conn.ExecContext(context.Background(), td.Input)
return "ok", err
@@ -342,6 +399,10 @@ func hbaRunTest(t *testing.T, insecure bool) {
if !re.MatchString(lastLogMsg) {
return errors.Newf("last entry does not match: %q", lastLogMsg)
}
+ if lastAuthLogCounter == entries[0].Counter {
+ return errors.Newf("log counter has not advanced beyond: %d", lastAuthLogCounter)
+ }
+ lastAuthLogCounter = entries[0].Counter
}
return nil
}); err != nil {
@@ -362,11 +423,18 @@ func hbaRunTest(t *testing.T, insecure bool) {
td.ScanArgs(t, "user", &user)
}
+ // Allow connections for non-root, non-testuser to force the
+ // use of client certificates.
+ forceCerts := false
+ if td.HasArg("force_certs") {
+ forceCerts = true
+ }
+
// We want the certs to be present in the filesystem for this test.
// However, certs are only generated for users "root" and "testuser" specifically.
sqlURL, cleanupFn := sqlutils.PGUrlWithOptionalClientCerts(
t, s.ServingSQLAddr(), t.Name(), url.User(user),
- user == security.RootUser || user == security.TestUser /* withClientCerts */)
+ forceCerts || user == security.RootUser || user == security.TestUser /* withClientCerts */)
defer cleanupFn()
var host, port string
@@ -545,7 +613,7 @@ func TestClientAddrOverride(t *testing.T) {
t.Fatal(err)
}
testutils.SucceedsSoon(t, func() error {
- curConf := pgServer.GetAuthenticationConfiguration()
+ curConf, _ := pgServer.GetAuthenticationConfiguration()
if expConf.String() != curConf.String() {
return errors.Newf(
"HBA config not yet loaded\ngot:\n%s\nexpected:\n%s",
diff --git a/pkg/sql/pgwire/authenticator.go b/pkg/sql/pgwire/authenticator.go
new file mode 100644
index 000000000000..d0f05e087628
--- /dev/null
+++ b/pkg/sql/pgwire/authenticator.go
@@ -0,0 +1,37 @@
+// Copyright 2021 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"
+
+ "github.com/cockroachdb/cockroach/pkg/security"
+ "github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
+)
+
+// Authenticator is a component of an AuthMethod that determines if the
+// given system identity (e.g.: Kerberos or X.509 principal, plain-old
+// username, etc) is who it claims to be.
+type Authenticator = func(
+ ctx context.Context,
+ systemIdentity security.SQLUsername,
+ clientConnection bool,
+ pwRetrieveFn PasswordRetrievalFn,
+) error
+
+// PasswordRetrievalFn defines a method to retrieve a hashed password
+// and expiration time for a user logging in with password-based
+// authentication.
+type PasswordRetrievalFn = func(context.Context) (
+ pwHash []byte,
+ pwExpiration *tree.DTimestamp,
+ _ error,
+)
diff --git a/pkg/sql/pgwire/hba/hba.go b/pkg/sql/pgwire/hba/hba.go
index f802b6f1ba04..30953d9d44cd 100644
--- a/pkg/sql/pgwire/hba/hba.go
+++ b/pkg/sql/pgwire/hba/hba.go
@@ -205,7 +205,7 @@ func (h Entry) ConnMatches(clientConn ConnType, ip net.IP) (bool, error) {
return true, nil
}
-// UserMatches returns true iff the provided username matches the an
+// UserMatches returns true iff the provided username matches an
// entry in the User list or if the user list is empty (the entry
// matches all).
//
diff --git a/pkg/sql/pgwire/hba_conf.go b/pkg/sql/pgwire/hba_conf.go
index 337a70655875..082c4704ccd1 100644
--- a/pkg/sql/pgwire/hba_conf.go
+++ b/pkg/sql/pgwire/hba_conf.go
@@ -21,6 +21,7 @@ import (
"github.com/cockroachdb/cockroach/pkg/settings"
"github.com/cockroachdb/cockroach/pkg/settings/cluster"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/hba"
+ "github.com/cockroachdb/cockroach/pkg/sql/pgwire/identmap"
"github.com/cockroachdb/cockroach/pkg/util/errorutil/unimplemented"
"github.com/cockroachdb/cockroach/pkg/util/log"
"github.com/cockroachdb/errors"
@@ -80,30 +81,31 @@ var connAuthConf = func() *settings.StringSetting {
return s
}()
-// loadLocalAuthConfigUponRemoteSettingChange initializes the local
-// node's cache of the auth configuration each time the cluster
-// setting is updated.
-func loadLocalAuthConfigUponRemoteSettingChange(
+// loadLocalHBAConfigUponRemoteSettingChange initializes the local
+// node's cache of the HBA configuration each time the cluster setting
+// is updated.
+func loadLocalHBAConfigUponRemoteSettingChange(
ctx context.Context, server *Server, st *cluster.Settings,
) {
val := connAuthConf.Get(&st.SV)
// An empty HBA configuration is special and means "use the
// default".
- conf := DefaultHBAConfig
+ hbaConfig := DefaultHBAConfig
if val != "" {
var err error
- conf, err = ParseAndNormalize(val)
+ hbaConfig, err = ParseAndNormalize(val)
if err != nil {
// The default is also used if the node is unable to load the
// config from the cluster setting.
log.Ops.Warningf(ctx, "invalid %s: %v", serverHBAConfSetting, err)
- conf = DefaultHBAConfig
+ hbaConfig = DefaultHBAConfig
}
}
+
server.auth.Lock()
defer server.auth.Unlock()
- server.auth.conf = conf
+ server.auth.conf = hbaConfig
}
// checkHBASyntaxBeforeUpdatingSetting is run by the SQL gateway each
@@ -260,9 +262,10 @@ local all all password # built-in CockroachDB default
//
// The data returned by this method is also observable via the debug
// endpoint /debug/hba_conf.
-func (s *Server) GetAuthenticationConfiguration() *hba.Conf {
+func (s *Server) GetAuthenticationConfiguration() (*hba.Conf, *identmap.Conf) {
s.auth.RLock()
auth := s.auth.conf
+ idMap := s.auth.identityMap
s.auth.RUnlock()
if auth == nil {
@@ -270,7 +273,10 @@ func (s *Server) GetAuthenticationConfiguration() *hba.Conf {
// the cluster setting has ever been set.
auth = DefaultHBAConfig
}
- return auth
+ if idMap == nil {
+ idMap = identmap.Empty()
+ }
+ return auth, idMap
}
// RegisterAuthMethod registers an AuthMethod for pgwire
@@ -326,15 +332,28 @@ type methodInfo struct {
// configuration of the cluster setting by a SQL client.
type CheckHBAEntry func(hba.Entry) error
+// NoOptionsAllowed is a CheckHBAEntry that returns an error if any
+// options are present in the entry.
+var NoOptionsAllowed CheckHBAEntry = func(e hba.Entry) error {
+ if len(e.Options) != 0 {
+ return errors.Newf("the HBA method %q does not accept options", e.Method)
+ }
+ return nil
+}
+
// HBADebugFn exposes the computed HBA configuration via the debug
// interface, for inspection by tests.
func (s *Server) HBADebugFn() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
- auth := s.GetAuthenticationConfiguration()
+ auth, usernames := s.GetAuthenticationConfiguration()
_, _ = w.Write([]byte("# Active authentication configuration on this node:\n"))
_, _ = w.Write([]byte(auth.String()))
+ if !usernames.Empty() {
+ _, _ = w.Write([]byte("# Active identity mapping on this node:\n"))
+ _, _ = w.Write([]byte(usernames.String()))
+ }
}
}
diff --git a/pkg/sql/pgwire/ident_map_conf.go b/pkg/sql/pgwire/ident_map_conf.go
new file mode 100644
index 000000000000..b264def06f1a
--- /dev/null
+++ b/pkg/sql/pgwire/ident_map_conf.go
@@ -0,0 +1,57 @@
+// Copyright 2021 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"
+ "strings"
+
+ "github.com/cockroachdb/cockroach/pkg/settings"
+ "github.com/cockroachdb/cockroach/pkg/settings/cluster"
+ "github.com/cockroachdb/cockroach/pkg/sql/pgwire/identmap"
+ "github.com/cockroachdb/cockroach/pkg/util/log"
+)
+
+// serverIdentityMapSetting is the name of the cluster setting that
+// holds the pg_ident configuration.
+const serverIdentityMapSetting = "server.identity_map.configuration"
+
+var connIdentityMapConf = func() *settings.StringSetting {
+ s := settings.RegisterValidatedStringSetting(
+ serverIdentityMapSetting,
+ "system-identity to database-username mappings",
+ "",
+ func(values *settings.Values, s string) error {
+ _, err := identmap.From(strings.NewReader(s))
+ return err
+ },
+ )
+ s.SetVisibility(settings.Public)
+ return s
+}()
+
+// loadLocalIdentityMapUponRemoteSettingChange initializes the local
+// node's cache of the identity map configuration each time the cluster
+// setting is updated.
+func loadLocalIdentityMapUponRemoteSettingChange(
+ ctx context.Context, server *Server, st *cluster.Settings,
+) {
+ val := connIdentityMapConf.Get(&st.SV)
+ idMap, err := identmap.From(strings.NewReader(val))
+ if err != nil {
+ log.Ops.Warningf(ctx, "invalid %s: %v", serverIdentityMapSetting, err)
+ idMap = identmap.Empty()
+ }
+
+ server.auth.Lock()
+ defer server.auth.Unlock()
+ server.auth.identityMap = idMap
+}
diff --git a/pkg/sql/pgwire/identmap/BUILD.bazel b/pkg/sql/pgwire/identmap/BUILD.bazel
new file mode 100644
index 000000000000..248a7c55082a
--- /dev/null
+++ b/pkg/sql/pgwire/identmap/BUILD.bazel
@@ -0,0 +1,20 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+ name = "identmap",
+ srcs = ["ident_map.go"],
+ importpath = "github.com/cockroachdb/cockroach/pkg/sql/pgwire/identmap",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//pkg/security",
+ "@com_github_cockroachdb_errors//:errors",
+ "@com_github_olekukonko_tablewriter//:tablewriter",
+ ],
+)
+
+go_test(
+ name = "identmap_test",
+ srcs = ["ident_map_test.go"],
+ embed = [":identmap"],
+ deps = ["@com_github_stretchr_testify//assert"],
+)
diff --git a/pkg/sql/pgwire/identmap/ident_map.go b/pkg/sql/pgwire/identmap/ident_map.go
new file mode 100644
index 000000000000..98a52b574ee9
--- /dev/null
+++ b/pkg/sql/pgwire/identmap/ident_map.go
@@ -0,0 +1,232 @@
+// Copyright 2021 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 identmap contains the code for parsing a pg_ident.conf file,
+// which allows a database operator to create some number of mappings
+// between system identities (e.g.: GSSAPI or X.509 principals) and
+// database usernames.
+package identmap
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "regexp"
+ "sort"
+ "strings"
+
+ "github.com/cockroachdb/cockroach/pkg/security"
+ "github.com/cockroachdb/errors"
+ "github.com/olekukonko/tablewriter"
+)
+
+// Conf provides a multi-level, user-configurable mapping between an
+// external system identity (e.g.: GSSAPI or X.509 principals) and zero
+// or more database usernames which the external principal may act as.
+//
+// The Conf supports being initialized from a file format that
+// is compatible with Postgres's pg_ident.conf file:
+//
+// # Comments
+// map-name system-identity database-username
+// # Convert "carl@example.com" ==> "example-carl"
+// map-name /^(.*)@example.com$ example-\1
+//
+// If the system-identity field starts with a slash, it will be
+// interpreted as a regular expression. The system-identity expression
+// may include a single capturing group, which may be substituted into
+// database-username with the character sequence \1 (backslash one). The
+// regular expression will be un-anchored for compatibility; users are
+// therefore encouraged to always specify anchors to eliminate ambiguity.
+//
+// See also: https://www.postgresql.org/docs/13/auth-username-maps.html
+type Conf struct {
+ // data is keyed by the map-map of a pg_ident entry.
+ data map[string][]element
+ // originalLines is for debugging use.
+ originalLines []string
+ // sortedKeys is for debugging use, to make String() deterministic.
+ sortedKeys []string
+}
+
+// Empty returns an empty configuration.
+func Empty() *Conf {
+ return &Conf{}
+}
+
+// linePattern is just three columns of data, so let's be
+// lazy and use a regexp instead of a full-blown parser.
+var linePattern = regexp.MustCompile(`^(\S+)\s+(\S+)\s+(\S+)$`)
+
+// From parses a reader containing a pg_ident.conf file.
+func From(r io.Reader) (*Conf, error) {
+ ret := &Conf{data: make(map[string][]element)}
+ scanner := bufio.NewScanner(r)
+ lineNo := 0
+
+ for scanner.Scan() {
+ lineNo++
+ line := scanner.Text()
+ ret.originalLines = append(ret.originalLines, line)
+
+ line = tidyIdentMapLine(line)
+ if line == "" {
+ continue
+ }
+
+ parts := linePattern.FindStringSubmatch(line)
+ if len(parts) != 4 {
+ return nil, errors.Errorf("unable to parse line %d: %q", lineNo, line)
+ }
+ mapName := parts[1]
+
+ var sysPattern *regexp.Regexp
+ var err error
+ if sysName := parts[2]; sysName[0] == '/' {
+ sysPattern, err = regexp.Compile(sysName[1:])
+ } else {
+ sysPattern, err = regexp.Compile("^" + regexp.QuoteMeta(sysName) + "$")
+ }
+ if err != nil {
+ return nil, errors.Wrapf(err, "unable to parse line %d", lineNo)
+ }
+
+ dbUser := parts[3]
+ subIdx := strings.Index(dbUser, `\1`)
+ if subIdx >= 0 {
+ if sysPattern.NumSubexp() == 0 {
+ return nil, errors.Errorf(
+ `saw \1 substitution on line %d, but pattern contains no subexpressions`, lineNo)
+ }
+ }
+
+ elt := element{
+ dbUser: dbUser,
+ pattern: sysPattern,
+ substituteAt: subIdx,
+ }
+ if existing, ok := ret.data[mapName]; ok {
+ ret.data[mapName] = append(existing, elt)
+ } else {
+ ret.sortedKeys = append(ret.sortedKeys, mapName)
+ ret.data[mapName] = []element{elt}
+ }
+ }
+ sort.Strings(ret.sortedKeys)
+ return ret, nil
+}
+
+// Empty returns true if no mappings have been defined.
+func (c *Conf) Empty() bool {
+ return c.data == nil || len(c.data) == 0
+}
+
+// Map returns the database usernames that a system identity maps to
+// within the named mapping. If there are no matching usernames, or if
+// mapName is unknown, nil will be returned. The returned list will be
+// ordered based on the order in which the rules were defined. If there
+// are rules which generate identical mappings, only the first one will
+// be returned. That is, the returned list will be deduplicated,
+// preferring the first instance of any given username.
+func (c *Conf) Map(mapName, systemIdentity string) ([]security.SQLUsername, error) {
+ if c.data == nil {
+ return nil, nil
+ }
+ elts := c.data[mapName]
+ if elts == nil {
+ return nil, nil
+ }
+ var names []security.SQLUsername
+ seen := make(map[string]bool)
+ for _, elt := range elts {
+ if n := elt.substitute(systemIdentity); n != "" && !seen[n] {
+ // We're returning this as a for-validation username since a
+ // pattern-based mapping could still result in invalid characters
+ // being incorporated into the input.
+ u, err := security.MakeSQLUsernameFromUserInput(n, security.UsernameValidation)
+ if err != nil {
+ return nil, err
+ }
+ names = append(names, u)
+ seen[n] = true
+ }
+ }
+ return names, nil
+}
+
+func (c *Conf) String() string {
+ if len(c.data) == 0 {
+ return "# (empty configuration)"
+ }
+ var sb strings.Builder
+ sb.WriteString("# Original configuration:\n")
+ for _, l := range c.originalLines {
+ fmt.Fprintf(&sb, "# %s\n", l)
+ }
+ sb.WriteString("# Active configuration:\n")
+ table := tablewriter.NewWriter(&sb)
+ table.SetAutoWrapText(false)
+ table.SetReflowDuringAutoWrap(false)
+ table.SetAlignment(tablewriter.ALIGN_LEFT)
+ table.SetBorder(false)
+ table.SetNoWhiteSpace(true)
+ table.SetTrimWhiteSpaceAtEOL(true)
+ table.SetTablePadding(" ")
+
+ row := []string{"# map-name", "system-username", "database-username", ""}
+ table.Append(row)
+ for _, k := range c.sortedKeys {
+ row[0] = k
+ for _, elt := range c.data[k] {
+ row[1] = elt.pattern.String()
+ row[2] = elt.dbUser
+ if elt.substituteAt == -1 {
+ row[3] = ""
+ } else {
+ row[3] = fmt.Sprintf("# substituteAt=%d", elt.substituteAt)
+ }
+ table.Append(row)
+ }
+ }
+ table.Render()
+ return sb.String()
+}
+
+type element struct {
+ dbUser string
+ // pattern may just be a literal match.
+ pattern *regexp.Regexp
+ // If substituteAt is non-negative, it indicates the index at which
+ // the \1 substitution token occurs. This also implies that pattern
+ // has at least one submatch in it.
+ substituteAt int
+}
+
+// substitute returns a non-empty string if the map element matches the
+// external system username.
+func (e element) substitute(systemUsername string) string {
+ m := e.pattern.FindStringSubmatch(systemUsername)
+ if m == nil {
+ return ""
+ }
+ if e.substituteAt == -1 {
+ return e.dbUser
+ }
+ return e.dbUser[0:e.substituteAt] + m[1] + e.dbUser[e.substituteAt+2:]
+}
+
+// tidyIdentMapLine removes # comments and trims whitespace.
+func tidyIdentMapLine(line string) string {
+ if commentIdx := strings.IndexByte(line, '#'); commentIdx != -1 {
+ line = line[0:commentIdx]
+ }
+
+ return strings.TrimSpace(line)
+}
diff --git a/pkg/sql/pgwire/identmap/ident_map_test.go b/pkg/sql/pgwire/identmap/ident_map_test.go
new file mode 100644
index 000000000000..a030c743ea92
--- /dev/null
+++ b/pkg/sql/pgwire/identmap/ident_map_test.go
@@ -0,0 +1,112 @@
+// Copyright 2021 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 identmap
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestIdentityMapElement(t *testing.T) {
+ exactMatch := func(sysName, dbname string) element {
+ return element{
+ dbUser: dbname,
+ pattern: regexp.MustCompile("^" + regexp.QuoteMeta(sysName) + "$"),
+ substituteAt: -1,
+ }
+ }
+ regexMatch := func(sysName, dbName string) element {
+ return element{
+ dbUser: dbName,
+ pattern: regexp.MustCompile(sysName),
+ substituteAt: strings.Index(dbName, `\1`),
+ }
+ }
+
+ tcs := []struct {
+ elt element
+ principal string
+ expected string
+ }{
+ {
+ elt: exactMatch("carlito", "carl"),
+ principal: "carlito",
+ expected: "carl",
+ },
+ {
+ elt: exactMatch("carlito", "carl"),
+ principal: "nope",
+ expected: "",
+ },
+ {
+ elt: regexMatch("^(.*)@cockroachlabs.com$", `\1`),
+ principal: "carl@cockroachlabs.com",
+ expected: "carl",
+ },
+ {
+ elt: regexMatch("^(.*)@cockroachlabs.com$", `\11`),
+ principal: "carl@cockroachlabs.com",
+ expected: "carl1",
+ },
+ {
+ elt: regexMatch("^(.*)@cockroachlabs.com$", `1\1`),
+ principal: "carl@cockroachlabs.com",
+ expected: "1carl",
+ },
+ {
+ elt: regexMatch("^(.*)@cockroachlabs.com$", `\1`),
+ principal: "carl@example.com",
+ expected: "",
+ },
+ }
+
+ for idx, tc := range tcs {
+ t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) {
+ a := assert.New(t)
+ a.Equal(tc.expected, tc.elt.substitute(tc.principal))
+ })
+ }
+}
+
+func TestIdentityMap(t *testing.T) {
+ a := assert.New(t)
+ data := `
+# This is a comment
+map-name system-username database-username
+foo /^(.*)@cockroachlabs.com$ \1
+foo carl@cockroachlabs.com also_carl # Trailing comment
+foo carl@cockroachlabs.com carl # Duplicate behavior
+`
+
+ m, err := From(strings.NewReader(data))
+ if !a.NoError(err) {
+ return
+ }
+ t.Log(m.String())
+ a.Len(m.data, 2)
+
+ a.Nil(m.Map("missing", "carl"))
+
+ if elts := m.data["map-name"]; a.Len(elts, 1) {
+ a.Equal("database-username", elts[0].substitute("system-username"))
+ }
+
+ if elts, err := m.Map("foo", "carl@cockroachlabs.com"); a.NoError(err) && a.Len(elts, 2) {
+ a.Equal("carl", elts[0].Normalized())
+ a.Equal("also_carl", elts[1].Normalized())
+ }
+
+ a.Nil(m.Map("foo", "carl@example.com"))
+}
diff --git a/pkg/sql/pgwire/role_mapper.go b/pkg/sql/pgwire/role_mapper.go
new file mode 100644
index 000000000000..38d824691d78
--- /dev/null
+++ b/pkg/sql/pgwire/role_mapper.go
@@ -0,0 +1,71 @@
+// Copyright 2021 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"
+
+ "github.com/cockroachdb/cockroach/pkg/security"
+ "github.com/cockroachdb/cockroach/pkg/sql/pgwire/hba"
+ "github.com/cockroachdb/cockroach/pkg/sql/pgwire/identmap"
+ "github.com/cockroachdb/errors"
+)
+
+// RoleMapper defines a mechanism by which an AuthMethod associated
+// with an incoming connection may replace the caller-provided system
+// identity (e.g.: GSSAPI or X.509 principal, LDAP DN, etc.) with zero
+// or more SQLUsernames that will be subsequently validated against the
+// SQL roles defined within the database. The mapping from system
+// identity to database roles may be derived from the host-based
+// authentication mechanism built into CockroachDB, or it could
+// conceivably be implemented by an external directory service which
+// maps groups of users onto database roles.
+type RoleMapper = func(
+ ctx context.Context,
+ systemIdentity security.SQLUsername,
+) ([]security.SQLUsername, error)
+
+// UseProvidedIdentity is a trivial implementation of RoleMapper which always
+// returns its input.
+func UseProvidedIdentity(
+ _ context.Context, id security.SQLUsername,
+) ([]security.SQLUsername, error) {
+ return []security.SQLUsername{id}, nil
+}
+
+var _ RoleMapper = UseProvidedIdentity
+
+// HbaMapper implements the "map" option that may be defined in a
+// host-based authentication rule. If the HBA entry does not define a
+// "map" option, this function will return UseProvidedIdentity.
+//
+// This mapper will return an error if an applied mapping rule results
+// in the root user or a reserved user, which includes the node,
+// "public", and various other magic prefixes.
+func HbaMapper(hbaEntry *hba.Entry, identMap *identmap.Conf) RoleMapper {
+ mapName := hbaEntry.GetOption("map")
+ if mapName == "" {
+ return UseProvidedIdentity
+ }
+ return func(_ context.Context, id security.SQLUsername) ([]security.SQLUsername, error) {
+ users, err := identMap.Map(mapName, id.Normalized())
+ if err != nil {
+ return nil, err
+ }
+ for _, user := range users {
+ if user.IsRootUser() || user.IsNodeUser() || user.IsReserved() {
+ return nil, errors.Newf("system identity %q mapped to reserved database role %q",
+ id.Normalized(), user.Normalized())
+ }
+ }
+ return users, nil
+ }
+}
diff --git a/pkg/sql/pgwire/server.go b/pkg/sql/pgwire/server.go
index ec6450a411a8..6bbd0c148ac7 100644
--- a/pkg/sql/pgwire/server.go
+++ b/pkg/sql/pgwire/server.go
@@ -32,6 +32,7 @@ import (
"github.com/cockroachdb/cockroach/pkg/sql/catalog/catalogkeys"
"github.com/cockroachdb/cockroach/pkg/sql/catalog/catconstants"
"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"
@@ -187,7 +188,8 @@ type Server struct {
auth struct {
syncutil.RWMutex
- conf *hba.Conf
+ conf *hba.Conf
+ identityMap *identmap.Conf
}
sqlMemoryPool *mon.BytesMonitor
@@ -300,11 +302,14 @@ func MakeServer(
server.mu.connCancelMap = make(cancelChanMap)
server.mu.Unlock()
- connAuthConf.SetOnChange(&st.SV,
- func(ctx context.Context) {
- loadLocalAuthConfigUponRemoteSettingChange(
- ambientCtx.AnnotateCtx(context.Background()), server, st)
- })
+ connAuthConf.SetOnChange(&st.SV, func(ctx context.Context) {
+ loadLocalHBAConfigUponRemoteSettingChange(
+ ambientCtx.AnnotateCtx(context.Background()), server, st)
+ })
+ connIdentityMapConf.SetOnChange(&st.SV, func(ctx context.Context) {
+ loadLocalIdentityMapUponRemoteSettingChange(
+ ambientCtx.AnnotateCtx(context.Background()), server, st)
+ })
return server
}
@@ -695,6 +700,7 @@ func (s *Server) ServeConn(ctx context.Context, conn net.Conn, socketType Socket
testingAuthHook = k.AuthHook
}
+ hbaConf, identMap := s.GetAuthenticationConfiguration()
// Defer the rest of the processing to the connection handler.
// This includes authentication.
s.serveConn(
@@ -706,7 +712,8 @@ func (s *Server) ServeConn(ctx context.Context, conn net.Conn, socketType Socket
connDetails: connDetails,
insecure: s.cfg.Insecure,
ie: s.execCfg.InternalExecutor,
- auth: s.GetAuthenticationConfiguration(),
+ auth: hbaConf,
+ identMap: identMap,
testingAuthHook: testingAuthHook,
})
return nil
diff --git a/pkg/sql/pgwire/testdata/auth/conn_log b/pkg/sql/pgwire/testdata/auth/conn_log
index 05c9b277f686..b748bc5006a8 100644
--- a/pkg/sql/pgwire/testdata/auth/conn_log
+++ b/pkg/sql/pgwire/testdata/auth/conn_log
@@ -47,9 +47,9 @@ authlog 6
.*client_connection_end
----
5 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
-6 {"EventType":"client_authentication_info","Info":"HBA rule: host all root all cert-password # CockroachDB mandatory rule","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"root"}
-7 {"EventType":"client_authentication_info","Info":"client presented certificate, proceeding with certificate validation","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"root"}
-8 {"EventType":"client_authentication_ok","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"root"}
+6 {"EventType":"client_authentication_info","Info":"HBA rule: host all root all cert-password # CockroachDB mandatory rule","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"root","Timestamp":"XXX","Transport":"hostssl"}
+7 {"EventType":"client_authentication_info","Info":"client presented certificate, proceeding with certificate validation","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"root","Timestamp":"XXX","Transport":"hostssl"}
+8 {"EventType":"client_authentication_ok","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"root","Timestamp":"XXX","Transport":"hostssl","User":"root"}
9 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
10 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
@@ -61,9 +61,9 @@ authlog 6
.*client_connection_end
----
11 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
-12 {"EventType":"client_authentication_info","Info":"HBA rule: host all root all cert-password # CockroachDB mandatory rule","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"root"}
-13 {"EventType":"client_authentication_info","Info":"no client certificate, proceeding with password authentication","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"root"}
-14 {"EventType":"client_authentication_ok","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"root"}
+12 {"EventType":"client_authentication_info","Info":"HBA rule: host all root all cert-password # CockroachDB mandatory rule","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"root","Timestamp":"XXX","Transport":"hostssl"}
+13 {"EventType":"client_authentication_info","Info":"no client certificate, proceeding with password authentication","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"root","Timestamp":"XXX","Transport":"hostssl"}
+14 {"EventType":"client_authentication_ok","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"root","Timestamp":"XXX","Transport":"hostssl","User":"root"}
15 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
16 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
@@ -75,9 +75,9 @@ authlog 6
.*client_connection_end
----
17 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
-18 {"EventType":"client_authentication_info","Info":"HBA rule: host all root all cert-password # CockroachDB mandatory rule","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"root"}
-19 {"EventType":"client_authentication_info","Info":"no client certificate, proceeding with password authentication","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"root"}
-20 {"Detail":"password authentication failed for user root","EventType":"client_authentication_failed","InstanceID":1,"Method":"cert-password","Network":"tcp","Reason":6,"RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"root"}
+18 {"EventType":"client_authentication_info","Info":"HBA rule: host all root all cert-password # CockroachDB mandatory rule","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"root","Timestamp":"XXX","Transport":"hostssl"}
+19 {"EventType":"client_authentication_info","Info":"no client certificate, proceeding with password authentication","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"root","Timestamp":"XXX","Transport":"hostssl"}
+20 {"Detail":"password authentication failed for user root","EventType":"client_authentication_failed","InstanceID":1,"Method":"cert-password","Network":"tcp","Reason":6,"RemoteAddress":"XXX","SystemIdentity":"root","Timestamp":"XXX","Transport":"hostssl","User":"root"}
21 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
22 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
@@ -94,8 +94,8 @@ authlog 5
.*client_connection_end
----
23 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
-24 {"EventType":"client_authentication_info","Info":"HBA rule: host all trusted all trust # custom","InstanceID":1,"Method":"trust","Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"trusted"}
-25 {"EventType":"client_authentication_ok","InstanceID":1,"Method":"trust","Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"trusted"}
+24 {"EventType":"client_authentication_info","Info":"HBA rule: host all trusted all trust # custom","InstanceID":1,"Method":"trust","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"trusted","Timestamp":"XXX","Transport":"hostssl"}
+25 {"EventType":"client_authentication_ok","InstanceID":1,"Method":"trust","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"trusted","Timestamp":"XXX","Transport":"hostssl","User":"trusted"}
26 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
27 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
@@ -111,9 +111,9 @@ authlog 6
.*client_connection_end
----
28 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
-29 {"EventType":"client_authentication_info","Info":"HBA rule: host all all all cert-password # built-in CockroachDB default","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"userpw"}
-30 {"EventType":"client_authentication_info","Info":"no client certificate, proceeding with password authentication","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"userpw"}
-31 {"EventType":"client_authentication_ok","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"userpw"}
+29 {"EventType":"client_authentication_info","Info":"HBA rule: host all all all cert-password # built-in CockroachDB default","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"userpw","Timestamp":"XXX","Transport":"hostssl"}
+30 {"EventType":"client_authentication_info","Info":"no client certificate, proceeding with password authentication","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"userpw","Timestamp":"XXX","Transport":"hostssl"}
+31 {"EventType":"client_authentication_ok","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"userpw","Timestamp":"XXX","Transport":"hostssl","User":"userpw"}
32 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
33 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
@@ -125,9 +125,9 @@ authlog 6
.*client_connection_end
----
34 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
-35 {"EventType":"client_authentication_info","Info":"HBA rule: host all all all cert-password # built-in CockroachDB default","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"userpw"}
-36 {"EventType":"client_authentication_info","Info":"no client certificate, proceeding with password authentication","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"userpw"}
-37 {"Detail":"password authentication failed for user userpw","EventType":"client_authentication_failed","InstanceID":1,"Method":"cert-password","Network":"tcp","Reason":6,"RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"userpw"}
+35 {"EventType":"client_authentication_info","Info":"HBA rule: host all all all cert-password # built-in CockroachDB default","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"userpw","Timestamp":"XXX","Transport":"hostssl"}
+36 {"EventType":"client_authentication_info","Info":"no client certificate, proceeding with password authentication","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"userpw","Timestamp":"XXX","Transport":"hostssl"}
+37 {"Detail":"password authentication failed for user userpw","EventType":"client_authentication_failed","InstanceID":1,"Method":"cert-password","Network":"tcp","Reason":6,"RemoteAddress":"XXX","SystemIdentity":"userpw","Timestamp":"XXX","Transport":"hostssl","User":"userpw"}
38 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
39 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
@@ -143,10 +143,10 @@ authlog 7
.*client_connection_end
----
40 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
-41 {"EventType":"client_authentication_info","Info":"HBA rule: host all all all cert-password # built-in CockroachDB default","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"usernopw"}
-42 {"EventType":"client_authentication_info","Info":"no client certificate, proceeding with password authentication","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"usernopw"}
-43 {"EventType":"client_authentication_info","Info":"user has no password defined","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"usernopw"}
-44 {"Detail":"password authentication failed for user usernopw","EventType":"client_authentication_failed","InstanceID":1,"Method":"cert-password","Network":"tcp","Reason":6,"RemoteAddress":"XXX","Timestamp":"XXX","Transport":"hostssl","User":"usernopw"}
+41 {"EventType":"client_authentication_info","Info":"HBA rule: host all all all cert-password # built-in CockroachDB default","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"usernopw","Timestamp":"XXX","Transport":"hostssl"}
+42 {"EventType":"client_authentication_info","Info":"no client certificate, proceeding with password authentication","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"usernopw","Timestamp":"XXX","Transport":"hostssl"}
+43 {"EventType":"client_authentication_info","Info":"user has no password defined","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"usernopw","Timestamp":"XXX","Transport":"hostssl","User":"usernopw"}
+44 {"Detail":"password authentication failed for user usernopw","EventType":"client_authentication_failed","InstanceID":1,"Method":"cert-password","Network":"tcp","Reason":6,"RemoteAddress":"XXX","SystemIdentity":"usernopw","Timestamp":"XXX","Transport":"hostssl","User":"usernopw"}
45 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
46 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
@@ -168,8 +168,8 @@ authlog 5
.*client_connection_end
----
47 {"EventType":"client_connection_start","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
-48 {"EventType":"client_authentication_info","Info":"HBA rule: local all all password # built-in CockroachDB default","InstanceID":1,"Method":"password","Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"local","User":"root"}
-49 {"EventType":"client_authentication_ok","InstanceID":1,"Method":"password","Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"local","User":"root"}
+48 {"EventType":"client_authentication_info","Info":"HBA rule: local all all password # built-in CockroachDB default","InstanceID":1,"Method":"password","Network":"unix","RemoteAddress":"XXX","SystemIdentity":"root","Timestamp":"XXX","Transport":"local"}
+49 {"EventType":"client_authentication_ok","InstanceID":1,"Method":"password","Network":"unix","RemoteAddress":"XXX","SystemIdentity":"root","Timestamp":"XXX","Transport":"local","User":"root"}
50 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
51 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
@@ -181,8 +181,8 @@ authlog 5
.*client_connection_end
----
52 {"EventType":"client_connection_start","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
-53 {"EventType":"client_authentication_info","Info":"HBA rule: local all all password # built-in CockroachDB default","InstanceID":1,"Method":"password","Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"local","User":"root"}
-54 {"Detail":"password authentication failed for user root","EventType":"client_authentication_failed","InstanceID":1,"Method":"password","Network":"unix","Reason":6,"RemoteAddress":"XXX","Timestamp":"XXX","Transport":"local","User":"root"}
+53 {"EventType":"client_authentication_info","Info":"HBA rule: local all all password # built-in CockroachDB default","InstanceID":1,"Method":"password","Network":"unix","RemoteAddress":"XXX","SystemIdentity":"root","Timestamp":"XXX","Transport":"local"}
+54 {"Detail":"password authentication failed for user root","EventType":"client_authentication_failed","InstanceID":1,"Method":"password","Network":"unix","Reason":6,"RemoteAddress":"XXX","SystemIdentity":"root","Timestamp":"XXX","Transport":"local","User":"root"}
55 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
56 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
@@ -199,8 +199,8 @@ authlog 5
.*client_connection_end
----
57 {"EventType":"client_connection_start","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
-58 {"EventType":"client_authentication_info","Info":"HBA rule: local all trusted reject # custom","InstanceID":1,"Method":"reject","Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"local","User":"trusted"}
-59 {"Detail":"authentication rejected by configuration","EventType":"client_authentication_failed","InstanceID":1,"Method":"reject","Network":"unix","Reason":6,"RemoteAddress":"XXX","Timestamp":"XXX","Transport":"local","User":"trusted"}
+58 {"EventType":"client_authentication_info","Info":"HBA rule: local all trusted reject # custom","InstanceID":1,"Method":"reject","Network":"unix","RemoteAddress":"XXX","SystemIdentity":"trusted","Timestamp":"XXX","Transport":"local"}
+59 {"Detail":"authentication rejected by configuration","EventType":"client_authentication_failed","InstanceID":1,"Method":"reject","Network":"unix","Reason":6,"RemoteAddress":"XXX","SystemIdentity":"trusted","Timestamp":"XXX","Transport":"local","User":"trusted"}
60 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
61 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
@@ -216,8 +216,8 @@ authlog 5
.*client_connection_end
----
62 {"EventType":"client_connection_start","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
-63 {"EventType":"client_authentication_info","Info":"HBA rule: local all all password # built-in CockroachDB default","InstanceID":1,"Method":"password","Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"local","User":"userpw"}
-64 {"EventType":"client_authentication_ok","InstanceID":1,"Method":"password","Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"local","User":"userpw"}
+63 {"EventType":"client_authentication_info","Info":"HBA rule: local all all password # built-in CockroachDB default","InstanceID":1,"Method":"password","Network":"unix","RemoteAddress":"XXX","SystemIdentity":"userpw","Timestamp":"XXX","Transport":"local"}
+64 {"EventType":"client_authentication_ok","InstanceID":1,"Method":"password","Network":"unix","RemoteAddress":"XXX","SystemIdentity":"userpw","Timestamp":"XXX","Transport":"local","User":"userpw"}
65 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
66 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
@@ -229,8 +229,8 @@ authlog 5
.*client_connection_end
----
67 {"EventType":"client_connection_start","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
-68 {"EventType":"client_authentication_info","Info":"HBA rule: local all all password # built-in CockroachDB default","InstanceID":1,"Method":"password","Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"local","User":"userpw"}
-69 {"Detail":"password authentication failed for user userpw","EventType":"client_authentication_failed","InstanceID":1,"Method":"password","Network":"unix","Reason":6,"RemoteAddress":"XXX","Timestamp":"XXX","Transport":"local","User":"userpw"}
+68 {"EventType":"client_authentication_info","Info":"HBA rule: local all all password # built-in CockroachDB default","InstanceID":1,"Method":"password","Network":"unix","RemoteAddress":"XXX","SystemIdentity":"userpw","Timestamp":"XXX","Transport":"local"}
+69 {"Detail":"password authentication failed for user userpw","EventType":"client_authentication_failed","InstanceID":1,"Method":"password","Network":"unix","Reason":6,"RemoteAddress":"XXX","SystemIdentity":"userpw","Timestamp":"XXX","Transport":"local","User":"userpw"}
70 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
71 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
@@ -245,10 +245,10 @@ ERROR: usernologin does not have login privilege (SQLSTATE 28000)
authlog 4
.*client_connection_end
----
-72 {"EventType":"client_connection_start","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
-73 {"EventType":"client_authentication_failed","InstanceID":1,"Network":"unix","Reason":3,"RemoteAddress":"XXX","Timestamp":"XXX","Transport":"local","User":"usernologin"}
-74 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
-75 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
+73 {"EventType":"client_authentication_info","Info":"HBA rule: local all all password # built-in CockroachDB default","InstanceID":1,"Method":"password","Network":"unix","RemoteAddress":"XXX","SystemIdentity":"usernologin","Timestamp":"XXX","Transport":"local"}
+74 {"EventType":"client_authentication_failed","InstanceID":1,"Method":"password","Network":"unix","Reason":3,"RemoteAddress":"XXX","SystemIdentity":"usernologin","Timestamp":"XXX","Transport":"local","User":"usernologin"}
+75 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
+76 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
connect_unix user=userexpired password=123
@@ -258,12 +258,12 @@ ERROR: password is expired (SQLSTATE 28000)
authlog 6
.*client_connection_end
----
-76 {"EventType":"client_connection_start","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
-77 {"EventType":"client_authentication_info","Info":"HBA rule: local all all password # built-in CockroachDB default","InstanceID":1,"Method":"password","Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX","Transport":"local","User":"userexpired"}
-78 {"EventType":"client_authentication_failed","InstanceID":1,"Method":"password","Network":"unix","Reason":7,"RemoteAddress":"XXX","Timestamp":"XXX","Transport":"local","User":"userexpired"}
-79 {"Detail":"password is expired","EventType":"client_authentication_failed","InstanceID":1,"Method":"password","Network":"unix","Reason":4,"RemoteAddress":"XXX","Timestamp":"XXX","Transport":"local","User":"userexpired"}
-80 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
-81 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
+77 {"EventType":"client_connection_start","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
+78 {"EventType":"client_authentication_info","Info":"HBA rule: local all all password # built-in CockroachDB default","InstanceID":1,"Method":"password","Network":"unix","RemoteAddress":"XXX","SystemIdentity":"userexpired","Timestamp":"XXX","Transport":"local"}
+79 {"EventType":"client_authentication_failed","InstanceID":1,"Method":"password","Network":"unix","Reason":7,"RemoteAddress":"XXX","SystemIdentity":"userexpired","Timestamp":"XXX","Transport":"local","User":"userexpired"}
+80 {"Detail":"password is expired","EventType":"client_authentication_failed","InstanceID":1,"Method":"password","Network":"unix","Reason":6,"RemoteAddress":"XXX","SystemIdentity":"userexpired","Timestamp":"XXX","Transport":"local","User":"userexpired"}
+81 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
+82 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"unix","RemoteAddress":"XXX","Timestamp":"XXX"}
subtest end
diff --git a/pkg/sql/pgwire/testdata/auth/identity_map b/pkg/sql/pgwire/testdata/auth/identity_map
new file mode 100644
index 000000000000..0d9e0e0f023f
--- /dev/null
+++ b/pkg/sql/pgwire/testdata/auth/identity_map
@@ -0,0 +1,262 @@
+# Verify system-identity to database-user substitutions.
+
+config secure
+----
+
+
+# Set HBA to add an ident map.
+set_hba
+host all all all cert-password map=testing
+----
+# Active authentication configuration on this node:
+# Original configuration:
+# host all root all cert-password # CockroachDB mandatory rule
+# host all all all cert-password map=testing
+#
+# Interpreted configuration:
+# TYPE DATABASE USER ADDRESS METHOD OPTIONS
+host all root all cert-password
+host all all all cert-password map=testing
+
+set_identity_map
+testing testuser carl # Exact remapping
+testing /(.*)@cockroachlabs.com \1 # Generalized domain mapping
+testing testuser another_carl # Verify first-one-wins
+testing will_be_carl carl # Another user for password testing
+testing testuser2 carl # Cert that doesn't correspond to a db user
+testing testuser@example.com carl # Cert with a non-SQL principal baked in
+----
+# Active authentication configuration on this node:
+# Original configuration:
+# host all root all cert-password # CockroachDB mandatory rule
+# host all all all cert-password map=testing
+#
+# Interpreted configuration:
+# TYPE DATABASE USER ADDRESS METHOD OPTIONS
+host all root all cert-password
+host all all all cert-password map=testing
+# Active identity mapping on this node:
+# Original configuration:
+# testing testuser carl # Exact remapping
+# testing /(.*)@cockroachlabs.com \1 # Generalized domain mapping
+# testing testuser another_carl # Verify first-one-wins
+# testing will_be_carl carl # Another user for password testing
+# testing testuser2 carl # Cert that doesn't correspond to a db user
+# testing testuser@example.com carl # Cert with a non-SQL principal baked in
+# Active configuration:
+# map-name system-username database-username
+testing ^testuser$ carl
+testing (.*)@cockroachlabs.com \1 # substituteAt=0
+testing ^testuser$ another_carl
+testing ^will_be_carl$ carl
+testing ^testuser2$ carl
+testing ^testuser@example\.com$ carl
+
+sql
+CREATE USER carl WITH PASSWORD 'doggo';
+CREATE USER will_be_carl WITH PASSWORD 'oggod';
+----
+ok
+
+
+subtest password_still_works_with_db_username
+
+# Sanity-check the database user
+connect user=carl database=mydb password=doggo
+----
+ok mydb
+
+authlog 6
+.*client_connection_end
+----
+5 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+6 {"EventType":"client_authentication_info","Info":"HBA rule: host all all all cert-password map=testing","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"carl","Timestamp":"XXX","Transport":"hostssl"}
+7 {"EventType":"client_authentication_info","Info":"no client certificate, proceeding with password authentication","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"carl","Timestamp":"XXX","Transport":"hostssl"}
+8 {"EventType":"client_authentication_ok","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"carl","Timestamp":"XXX","Transport":"hostssl","User":"carl"}
+9 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+10 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+
+subtest end
+
+
+# This test verifies that we validate the password of an incoming
+# username-based request before any remapping occurs.
+subtest password_evaluated_before_remapping
+
+connect user=carl@cockroachlabs.com database=mydb password=doggo
+----
+ERROR: password authentication failed for user carl@cockroachlabs.com (SQLSTATE 28000)
+
+authlog 6
+.*client_connection_end
+----
+11 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+12 {"EventType":"client_authentication_info","Info":"HBA rule: host all all all cert-password map=testing","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"carl@cockroachlabs.com","Timestamp":"XXX","Transport":"hostssl"}
+13 {"EventType":"client_authentication_info","Info":"no client certificate, proceeding with password authentication","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"carl@cockroachlabs.com","Timestamp":"XXX","Transport":"hostssl"}
+14 {"EventType":"client_authentication_failed","InstanceID":1,"Method":"cert-password","Network":"tcp","Reason":2,"RemoteAddress":"XXX","SystemIdentity":"carl@cockroachlabs.com","Timestamp":"XXX","Transport":"hostssl","User":"carl@cockroachlabs.com"}
+15 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+16 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+
+subtest end
+
+# Verify the good path, where we verify the password against the
+# system identity and get a remapping.
+subtest password_remapped_user_ok
+
+connect user=will_be_carl database=mydb password=oggod
+----
+ok mydb
+
+authlog 6
+.*client_connection_end
+----
+17 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+18 {"EventType":"client_authentication_info","Info":"HBA rule: host all all all cert-password map=testing","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"will_be_carl","Timestamp":"XXX","Transport":"hostssl"}
+19 {"EventType":"client_authentication_info","Info":"no client certificate, proceeding with password authentication","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"will_be_carl","Timestamp":"XXX","Transport":"hostssl"}
+20 {"EventType":"client_authentication_ok","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"will_be_carl","Timestamp":"XXX","Transport":"hostssl","User":"will_be_carl"}
+21 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+22 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+
+subtest end
+
+# Connect as the magic "testuser" since that comes pre-equipped with a cert.
+subtest certificate_good
+
+connect user=testuser database=mydb
+----
+ok mydb
+
+authlog 6
+.*client_connection_end
+----
+23 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+24 {"EventType":"client_authentication_info","Info":"HBA rule: host all all all cert-password map=testing","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"testuser","Timestamp":"XXX","Transport":"hostssl"}
+25 {"EventType":"client_authentication_info","Info":"client presented certificate, proceeding with certificate validation","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"testuser","Timestamp":"XXX","Transport":"hostssl"}
+26 {"EventType":"client_authentication_ok","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"testuser","Timestamp":"XXX","Transport":"hostssl","User":"carl"}
+27 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+28 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+
+subtest end
+
+# There's a certificate on disk for a "testuser2" principal that doesn't
+# correspond to an actual SQL user. We want to test the case where
+# arbitrary system identities in a certificate must be mapped onto a
+# database username.
+subtest cert_with_principal_not_in_users
+
+connect user=testuser2 database=mydb force_certs
+----
+ok mydb
+
+authlog 6
+.*client_connection_end
+----
+29 {"EventType":"client_connection_start","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+30 {"EventType":"client_authentication_info","Info":"HBA rule: host all all all cert-password map=testing","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"testuser2","Timestamp":"XXX","Transport":"hostssl"}
+31 {"EventType":"client_authentication_info","Info":"client presented certificate, proceeding with certificate validation","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"testuser2","Timestamp":"XXX","Transport":"hostssl"}
+32 {"EventType":"client_authentication_ok","InstanceID":1,"Method":"cert-password","Network":"tcp","RemoteAddress":"XXX","SystemIdentity":"testuser2","Timestamp":"XXX","Transport":"hostssl","User":"carl"}
+33 {"Duration":"NNN","EventType":"client_session_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+34 {"Duration":"NNN","EventType":"client_connection_end","InstanceID":1,"Network":"tcp","RemoteAddress":"XXX","Timestamp":"XXX"}
+
+subtest end
+
+subtest password_should_not_accept_map
+
+set_hba
+host all all all password map=testing
+----
+ERROR: the HBA method "password" does not accept options
+
+subtest end
+
+subtest trust_should_not_accept_map
+
+set_hba
+host all all all trust map=testing
+----
+ERROR: the HBA method "trust" does not accept options
+
+subtest end
+
+subtest verify_root_mapping_fails
+
+set_identity_map
+testing testuser root # Exact remapping
+----
+# Active authentication configuration on this node:
+# Original configuration:
+# host all root all cert-password # CockroachDB mandatory rule
+# host all all all cert-password map=testing
+#
+# Interpreted configuration:
+# TYPE DATABASE USER ADDRESS METHOD OPTIONS
+host all root all cert-password
+host all all all cert-password map=testing
+# Active identity mapping on this node:
+# Original configuration:
+# testing testuser root # Exact remapping
+# Active configuration:
+# map-name system-username database-username
+testing ^testuser$ root
+
+connect user=testuser database=mydb
+----
+ERROR: system identity "testuser" mapped to reserved database role "root" (SQLSTATE 28000)
+
+subtest end
+
+subtest verify_node_mapping_fails
+
+set_identity_map
+testing testuser node # Exact remapping
+----
+# Active authentication configuration on this node:
+# Original configuration:
+# host all root all cert-password # CockroachDB mandatory rule
+# host all all all cert-password map=testing
+#
+# Interpreted configuration:
+# TYPE DATABASE USER ADDRESS METHOD OPTIONS
+host all root all cert-password
+host all all all cert-password map=testing
+# Active identity mapping on this node:
+# Original configuration:
+# testing testuser node # Exact remapping
+# Active configuration:
+# map-name system-username database-username
+testing ^testuser$ node
+
+connect user=testuser database=mydb
+----
+ERROR: system identity "testuser" mapped to reserved database role "node" (SQLSTATE 28000)
+
+subtest end
+
+
+# Clean up
+
+set_identity_map
+----
+# Active authentication configuration on this node:
+# Original configuration:
+# host all root all cert-password # CockroachDB mandatory rule
+# host all all all cert-password map=testing
+#
+# Interpreted configuration:
+# TYPE DATABASE USER ADDRESS METHOD OPTIONS
+host all root all cert-password
+host all all all cert-password map=testing
+
+set_hba
+----
+# Active authentication configuration on this node:
+# Original configuration:
+# host all root all cert-password # CockroachDB mandatory rule
+# host all all all cert-password # built-in CockroachDB default
+# local all all password # built-in CockroachDB default
+#
+# Interpreted configuration:
+# TYPE DATABASE USER ADDRESS METHOD OPTIONS
+host all root all cert-password
+host all all all cert-password
+local all all password
diff --git a/pkg/util/log/eventpb/json_encode_generated.go b/pkg/util/log/eventpb/json_encode_generated.go
index 30724df8b24c..fb9e9d468c6e 100644
--- a/pkg/util/log/eventpb/json_encode_generated.go
+++ b/pkg/util/log/eventpb/json_encode_generated.go
@@ -1543,6 +1543,18 @@ func (m *CommonSessionDetails) AppendJSONFields(printComma bool, b redact.Redact
b = append(b, '"')
}
+ if m.SystemIdentity != "" {
+ if printComma {
+ b = append(b, ',')
+ }
+ printComma = true
+ b = append(b, "\"SystemIdentity\":\""...)
+ b = append(b, redact.StartMarker()...)
+ b = redact.RedactableBytes(jsonbytes.EncodeString([]byte(b), string(redact.EscapeMarkers([]byte(m.SystemIdentity)))))
+ b = append(b, redact.EndMarker()...)
+ b = append(b, '"')
+ }
+
return printComma, b
}
diff --git a/pkg/util/log/eventpb/session_events.pb.go b/pkg/util/log/eventpb/session_events.pb.go
index a0ba46f7d776..0cfab03842de 100644
--- a/pkg/util/log/eventpb/session_events.pb.go
+++ b/pkg/util/log/eventpb/session_events.pb.go
@@ -129,9 +129,15 @@ var xxx_messageInfo_CommonConnectionDetails proto.InternalMessageInfo
type CommonSessionDetails struct {
// The connection type after transport negotiation.
Transport string `protobuf:"bytes,1,opt,name=transport,proto3" json:",omitempty" redact:"nonsensitive"`
- // The username the session is for. This is the username passed by
- // the client, after case-folding and Unicode normalization.
+ // The database username the session is for. This username will have
+ // undergone case-folding and Unicode normalization.
User string `protobuf:"bytes,2,opt,name=user,proto3" json:",omitempty"`
+ // The original system identity provided by the client, if an identity
+ // mapping was used per Host-Based Authentication rules. This may be a
+ // GSSAPI or X.509 principal or any other external value, so no
+ // specific assumptions should be made about the contents of this
+ // field.
+ SystemIdentity string `protobuf:"bytes,3,opt,name=system_identity,json=systemIdentity,proto3" json:",omitempty"`
}
func (m *CommonSessionDetails) Reset() { *m = CommonSessionDetails{} }
@@ -436,54 +442,55 @@ func init() {
}
var fileDescriptor_aef88ca0e95f42d6 = []byte{
- // 745 bytes of a gzipped FileDescriptorProto
- 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x96, 0xb1, 0x4f, 0xdb, 0x5a,
- 0x14, 0xc6, 0xe3, 0x24, 0x24, 0x8f, 0x9b, 0xf7, 0x22, 0xeb, 0x12, 0x1e, 0x51, 0xf4, 0x9e, 0x83,
- 0x2c, 0xb5, 0xa5, 0xa8, 0x4a, 0xaa, 0xa0, 0x2e, 0x95, 0xaa, 0xca, 0x89, 0x4d, 0xb1, 0x48, 0x6d,
- 0xe4, 0x24, 0x14, 0x75, 0xb1, 0x8c, 0x7d, 0x09, 0x56, 0x92, 0x7b, 0x23, 0xfb, 0x86, 0xaa, 0x63,
- 0x97, 0xce, 0xfd, 0x7f, 0xba, 0xb6, 0x12, 0x23, 0xea, 0xc4, 0x14, 0xd1, 0xb0, 0x31, 0x74, 0xe8,
- 0xd6, 0xad, 0xf2, 0xb5, 0x53, 0x48, 0x80, 0x52, 0xaa, 0x2e, 0x48, 0x6c, 0xb1, 0xcf, 0x39, 0x9f,
- 0xcf, 0xf7, 0x3b, 0xbe, 0x27, 0x06, 0x77, 0x06, 0xd4, 0xed, 0x96, 0xbb, 0xa4, 0x5d, 0x46, 0x7b,
- 0x08, 0xd3, 0xfe, 0x76, 0xd9, 0x47, 0xbe, 0xef, 0x12, 0x6c, 0xb2, 0x6b, 0xbf, 0xd4, 0xf7, 0x08,
- 0x25, 0xb0, 0x60, 0x13, 0xbb, 0xe3, 0x11, 0xcb, 0xde, 0x2d, 0x05, 0x05, 0xa5, 0x2e, 0x69, 0x97,
- 0xa2, 0x82, 0x42, 0xae, 0x4d, 0xda, 0x84, 0xa5, 0x95, 0x83, 0x5f, 0x61, 0x45, 0xe1, 0xff, 0x73,
- 0xc2, 0x67, 0x05, 0xc5, 0x4f, 0x1c, 0x58, 0xa8, 0x91, 0x5e, 0x8f, 0xe0, 0x1a, 0xc1, 0x18, 0xd9,
- 0xd4, 0x25, 0x58, 0x46, 0xd4, 0x72, 0xbb, 0x3e, 0x7c, 0x02, 0x32, 0x2e, 0xf6, 0xa9, 0x85, 0x6d,
- 0x64, 0xba, 0x4e, 0x9e, 0x5b, 0xe4, 0x96, 0x66, 0xaa, 0xff, 0x8d, 0x86, 0x45, 0xa0, 0x46, 0xb7,
- 0x55, 0xf9, 0x64, 0x58, 0x04, 0x0f, 0x48, 0xcf, 0xa5, 0xa8, 0xd7, 0xa7, 0xaf, 0x0d, 0x30, 0x2e,
- 0x50, 0x1d, 0x28, 0x81, 0x34, 0x46, 0xf4, 0x15, 0xf1, 0x3a, 0xf9, 0xf8, 0x22, 0xb7, 0x34, 0x5b,
- 0xbd, 0x37, 0x99, 0xfc, 0x75, 0x58, 0x9c, 0xf7, 0x90, 0x63, 0xd9, 0xf4, 0xb1, 0x88, 0x09, 0xf6,
- 0x11, 0xf6, 0x5d, 0xea, 0xee, 0x21, 0xd1, 0x18, 0xd7, 0xc1, 0x47, 0x20, 0xeb, 0xa1, 0x1e, 0xa1,
- 0xc8, 0xb4, 0x1c, 0xc7, 0x43, 0xbe, 0x9f, 0x4f, 0x30, 0xa5, 0xec, 0xd4, 0x63, 0xff, 0x09, 0xb3,
- 0xa4, 0x30, 0x49, 0x7c, 0xc3, 0x81, 0x5c, 0x68, 0xaa, 0x11, 0x42, 0x1c, 0x3b, 0x52, 0xc0, 0x2c,
- 0xf5, 0x2c, 0xec, 0xf7, 0x89, 0x47, 0x99, 0x9f, 0x6b, 0x34, 0x75, 0x5a, 0x09, 0x45, 0x90, 0x1c,
- 0xf8, 0xc8, 0x8b, 0x6c, 0x4d, 0x37, 0xc3, 0x62, 0xe2, 0x07, 0x0e, 0xcc, 0xd7, 0xba, 0x2e, 0xc2,
- 0xf4, 0x14, 0x6c, 0x83, 0x5a, 0x1e, 0x85, 0x4d, 0x90, 0xb2, 0x59, 0x73, 0xac, 0x83, 0x4c, 0xa5,
- 0x54, 0xba, 0x7c, 0xa8, 0xa5, 0xd0, 0x86, 0x12, 0x5c, 0x45, 0x26, 0xaa, 0x7f, 0xef, 0x0f, 0x8b,
- 0xb1, 0x83, 0x61, 0x91, 0x3b, 0x19, 0x16, 0x63, 0x46, 0xa4, 0x05, 0x5b, 0x20, 0x69, 0x13, 0x8c,
- 0x59, 0x4f, 0x99, 0xca, 0xca, 0xd5, 0x9a, 0xe7, 0xe6, 0x3d, 0x25, 0xcc, 0xe4, 0xc4, 0x2f, 0x1c,
- 0x98, 0x9b, 0xb6, 0xa1, 0x60, 0xe7, 0x46, 0x99, 0x80, 0xcb, 0xe0, 0x2f, 0x67, 0xe0, 0x59, 0x41,
- 0x1a, 0x7b, 0x81, 0x12, 0xe7, 0x66, 0xf6, 0x23, 0x2e, 0x7e, 0x8c, 0x03, 0x3e, 0x34, 0x1c, 0xbd,
- 0x3b, 0x37, 0xce, 0xed, 0x16, 0x48, 0x47, 0xbb, 0x83, 0x99, 0xcd, 0x54, 0x1e, 0x5e, 0xad, 0x3c,
- 0x79, 0x4e, 0xa6, 0x64, 0xc7, 0x72, 0x13, 0x1c, 0x93, 0x57, 0x70, 0xfc, 0x96, 0x00, 0x85, 0x90,
- 0xa3, 0x34, 0xa0, 0xbb, 0x08, 0x53, 0xd7, 0x66, 0x81, 0x55, 0xcb, 0xed, 0xa2, 0x5b, 0xa2, 0x11,
- 0xd1, 0x0e, 0x48, 0x79, 0xc8, 0xf2, 0x23, 0x9e, 0xd9, 0xca, 0xf2, 0xcf, 0x84, 0x03, 0x90, 0x01,
- 0x3e, 0x83, 0x55, 0xfc, 0xfa, 0xe6, 0x8a, 0x1e, 0x01, 0xef, 0x82, 0x94, 0xc3, 0xda, 0xc9, 0xcf,
- 0x5c, 0xb8, 0xb8, 0xa2, 0x28, 0x7c, 0x0a, 0x52, 0x3d, 0x44, 0x77, 0x89, 0x93, 0x4f, 0x5d, 0x6f,
- 0x45, 0x46, 0x65, 0xe2, 0x51, 0x1c, 0xfc, 0x7b, 0xd1, 0xec, 0xf5, 0xce, 0xed, 0xdc, 0xc3, 0xb9,
- 0x9f, 0x22, 0x4e, 0xfe, 0x1e, 0xe2, 0xb7, 0x09, 0x90, 0xbf, 0x08, 0xb1, 0x8a, 0x77, 0xc8, 0x2d,
- 0xe4, 0x3f, 0x03, 0x39, 0xf8, 0x9f, 0x77, 0xf1, 0x0e, 0xb9, 0xe4, 0xb8, 0xb0, 0xd8, 0xf2, 0x7b,
- 0x0e, 0x64, 0x27, 0x0f, 0x26, 0xcc, 0x80, 0x74, 0x4b, 0x5b, 0xd7, 0xf4, 0x17, 0x1a, 0x1f, 0x83,
- 0x79, 0x90, 0x6b, 0x35, 0x14, 0xc3, 0x34, 0x94, 0xa6, 0xa1, 0x2a, 0x9b, 0x52, 0xdd, 0x54, 0x0c,
- 0x43, 0x37, 0x78, 0x0e, 0x42, 0x90, 0x65, 0x11, 0x4d, 0x6f, 0x9a, 0xab, 0x7a, 0x4b, 0x93, 0xf9,
- 0x78, 0x70, 0xaf, 0xae, 0x3f, 0x53, 0x35, 0x53, 0x56, 0x1b, 0x52, 0xb5, 0xae, 0xc8, 0x7c, 0x02,
- 0xe6, 0x00, 0xff, 0x5c, 0x69, 0xae, 0xe9, 0xf2, 0x99, 0xcc, 0x64, 0x90, 0xb9, 0x61, 0x28, 0xe6,
- 0x9a, 0xae, 0xaf, 0x47, 0x8a, 0x33, 0x70, 0x01, 0xcc, 0xd5, 0x0c, 0x45, 0x56, 0xb4, 0xa6, 0x2a,
- 0xd5, 0x1b, 0xa6, 0xaa, 0x6d, 0x4a, 0x75, 0x55, 0xe6, 0x53, 0xd3, 0x01, 0x65, 0x6b, 0x43, 0x35,
- 0x14, 0x99, 0x4f, 0x57, 0xef, 0xef, 0x7f, 0x16, 0x62, 0xfb, 0x23, 0x81, 0x3b, 0x18, 0x09, 0xdc,
- 0xe1, 0x48, 0xe0, 0x8e, 0x46, 0x02, 0xf7, 0xee, 0x58, 0x88, 0x1d, 0x1c, 0x0b, 0xb1, 0xc3, 0x63,
- 0x21, 0xf6, 0x32, 0x1d, 0x91, 0xdf, 0x4e, 0xb1, 0x0f, 0xc6, 0x95, 0xef, 0x01, 0x00, 0x00, 0xff,
- 0xff, 0xe7, 0x55, 0xe5, 0x76, 0xaa, 0x0a, 0x00, 0x00,
+ // 766 bytes of a gzipped FileDescriptorProto
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x56, 0x41, 0x6f, 0xd3, 0x4a,
+ 0x18, 0x8c, 0x93, 0x34, 0x79, 0xdd, 0xbc, 0x97, 0x67, 0x6d, 0x53, 0x1a, 0x45, 0xe0, 0x54, 0x96,
+ 0x80, 0x52, 0xa1, 0x04, 0xa5, 0x42, 0x48, 0x48, 0x08, 0x25, 0xb1, 0x4b, 0xad, 0x06, 0xbb, 0x72,
+ 0x92, 0x52, 0x71, 0xb1, 0x5c, 0x7b, 0x9b, 0x5a, 0x49, 0x76, 0x23, 0x7b, 0x53, 0xd4, 0x3f, 0xc0,
+ 0x99, 0xdf, 0x03, 0x57, 0x90, 0x7a, 0xac, 0x38, 0xf5, 0x14, 0x95, 0xf4, 0xd6, 0x03, 0x07, 0x6e,
+ 0xdc, 0x90, 0xd7, 0x0e, 0x6d, 0xd2, 0x96, 0x52, 0xc4, 0xa5, 0x52, 0x6f, 0xb6, 0xbf, 0x99, 0xf1,
+ 0x37, 0xf3, 0xed, 0xae, 0x16, 0xdc, 0xed, 0x53, 0xa7, 0x53, 0xec, 0x90, 0x56, 0x11, 0xed, 0x20,
+ 0x4c, 0x7b, 0x9b, 0x45, 0x0f, 0x79, 0x9e, 0x43, 0xb0, 0xc1, 0xde, 0xbd, 0x42, 0xcf, 0x25, 0x94,
+ 0xc0, 0x9c, 0x45, 0xac, 0xb6, 0x4b, 0x4c, 0x6b, 0xbb, 0xe0, 0x13, 0x0a, 0x1d, 0xd2, 0x2a, 0x84,
+ 0x84, 0x5c, 0xa6, 0x45, 0x5a, 0x84, 0xc1, 0x8a, 0xfe, 0x53, 0xc0, 0xc8, 0xdd, 0x39, 0x23, 0x7c,
+ 0x5a, 0x50, 0xfc, 0xcc, 0x81, 0xb9, 0x2a, 0xe9, 0x76, 0x09, 0xae, 0x12, 0x8c, 0x91, 0x45, 0x1d,
+ 0x82, 0x25, 0x44, 0x4d, 0xa7, 0xe3, 0xc1, 0x67, 0x20, 0xe5, 0x60, 0x8f, 0x9a, 0xd8, 0x42, 0x86,
+ 0x63, 0x67, 0xb9, 0x79, 0x6e, 0x61, 0xaa, 0x72, 0x7b, 0x38, 0xc8, 0x03, 0x25, 0xfc, 0xac, 0x48,
+ 0xc7, 0x83, 0x3c, 0x78, 0x48, 0xba, 0x0e, 0x45, 0xdd, 0x1e, 0xdd, 0xd5, 0xc1, 0x88, 0xa0, 0xd8,
+ 0xb0, 0x0c, 0x92, 0x18, 0xd1, 0x37, 0xc4, 0x6d, 0x67, 0xa3, 0xf3, 0xdc, 0xc2, 0x74, 0xe5, 0xfe,
+ 0x38, 0xf8, 0xdb, 0x20, 0x3f, 0xeb, 0x22, 0xdb, 0xb4, 0xe8, 0x53, 0x11, 0x13, 0xec, 0x21, 0xec,
+ 0x39, 0xd4, 0xd9, 0x41, 0xa2, 0x3e, 0xe2, 0xc1, 0xc7, 0x20, 0xed, 0xa2, 0x2e, 0xa1, 0xc8, 0x30,
+ 0x6d, 0xdb, 0x45, 0x9e, 0x97, 0x8d, 0x31, 0xa5, 0xf4, 0xc4, 0x6f, 0xff, 0x0b, 0x50, 0xe5, 0x00,
+ 0x24, 0xbe, 0xe7, 0x40, 0x26, 0x30, 0x55, 0x0f, 0x42, 0x1c, 0x39, 0x92, 0xc1, 0x34, 0x75, 0x4d,
+ 0xec, 0xf5, 0x88, 0x4b, 0x99, 0x9f, 0x2b, 0x34, 0x75, 0xc2, 0x84, 0x22, 0x88, 0xf7, 0x3d, 0xe4,
+ 0x86, 0xb6, 0x26, 0x9b, 0x61, 0x35, 0xf8, 0x04, 0xfc, 0xef, 0xed, 0x7a, 0x14, 0x75, 0x0d, 0xc7,
+ 0x46, 0x98, 0x3a, 0x74, 0xf7, 0x82, 0xde, 0xd3, 0x01, 0x4c, 0x09, 0x51, 0xe2, 0x47, 0x0e, 0xcc,
+ 0x56, 0x3b, 0x0e, 0xc2, 0xf4, 0x64, 0x22, 0x75, 0x6a, 0xba, 0x14, 0x36, 0x40, 0xc2, 0x62, 0xae,
+ 0x58, 0xeb, 0xa9, 0x52, 0xa1, 0x70, 0xf1, 0x6a, 0x28, 0x04, 0xfe, 0x65, 0xff, 0x2d, 0x74, 0x5f,
+ 0xf9, 0x77, 0x6f, 0x90, 0x8f, 0xec, 0x0f, 0xf2, 0xdc, 0xf1, 0x20, 0x1f, 0xd1, 0x43, 0x2d, 0xd8,
+ 0x04, 0x71, 0x8b, 0x60, 0xcc, 0xcc, 0xa4, 0x4a, 0x4b, 0x97, 0x6b, 0x9e, 0x59, 0x28, 0x13, 0xc2,
+ 0x4c, 0x4e, 0xfc, 0xca, 0x81, 0x99, 0x49, 0x1b, 0x32, 0xb6, 0xaf, 0x95, 0x09, 0xb8, 0x08, 0xfe,
+ 0xb1, 0xfb, 0xae, 0xe9, 0xc3, 0xd8, 0xf4, 0x62, 0x67, 0xa6, 0xf7, 0xb3, 0x2e, 0x7e, 0x8a, 0x02,
+ 0x3e, 0x30, 0x1c, 0x2e, 0xba, 0x6b, 0xe7, 0x76, 0x03, 0x24, 0xc3, 0x43, 0x87, 0x99, 0x4d, 0x95,
+ 0x1e, 0x5d, 0xae, 0x3c, 0xbe, 0xc1, 0x26, 0x64, 0x47, 0x72, 0x63, 0x39, 0xc6, 0x2f, 0xc9, 0xf1,
+ 0x7b, 0x0c, 0xe4, 0x82, 0x1c, 0xcb, 0x7d, 0xba, 0xed, 0x6f, 0x0a, 0x8b, 0x15, 0x96, 0x4d, 0xa7,
+ 0x83, 0x6e, 0x12, 0x0d, 0x13, 0x6d, 0x83, 0x84, 0x8b, 0x4c, 0x2f, 0xcc, 0x33, 0x5d, 0x5a, 0xfc,
+ 0x95, 0xb0, 0x1f, 0xa4, 0x1f, 0x9f, 0xce, 0x18, 0xbf, 0x7f, 0xe4, 0x85, 0xbf, 0x80, 0xf7, 0x40,
+ 0xc2, 0x66, 0xed, 0x64, 0xa7, 0xce, 0x3d, 0xc2, 0xc2, 0x2a, 0x7c, 0x0e, 0x12, 0x5d, 0x44, 0xb7,
+ 0x89, 0x9d, 0x4d, 0x5c, 0xed, 0x6c, 0x0d, 0x69, 0xe2, 0x61, 0x14, 0xdc, 0x3a, 0x6f, 0xf6, 0x5a,
+ 0xfb, 0x66, 0xee, 0xc1, 0xdc, 0x4f, 0x22, 0x8e, 0xff, 0x59, 0xc4, 0x6f, 0x63, 0x20, 0x7b, 0x5e,
+ 0xc4, 0x0a, 0xde, 0x22, 0x37, 0x21, 0xff, 0x9d, 0x90, 0xfd, 0x0b, 0x82, 0x83, 0xb7, 0xc8, 0x05,
+ 0xdb, 0x85, 0xd5, 0x16, 0x3f, 0x70, 0x20, 0x3d, 0xbe, 0x31, 0x61, 0x0a, 0x24, 0x9b, 0xea, 0xaa,
+ 0xaa, 0xbd, 0x52, 0xf9, 0x08, 0xcc, 0x82, 0x4c, 0xb3, 0x2e, 0xeb, 0x86, 0x2e, 0x37, 0x74, 0x45,
+ 0x5e, 0x2f, 0xd7, 0x0c, 0x59, 0xd7, 0x35, 0x9d, 0xe7, 0x20, 0x04, 0x69, 0x56, 0x51, 0xb5, 0x86,
+ 0xb1, 0xac, 0x35, 0x55, 0x89, 0x8f, 0xfa, 0xdf, 0x6a, 0xda, 0x0b, 0x45, 0x35, 0x24, 0xa5, 0x5e,
+ 0xae, 0xd4, 0x64, 0x89, 0x8f, 0xc1, 0x0c, 0xe0, 0x5f, 0xca, 0x8d, 0x15, 0x4d, 0x3a, 0x85, 0x8c,
+ 0xfb, 0xc8, 0x35, 0x5d, 0x36, 0x56, 0x34, 0x6d, 0x35, 0x54, 0x9c, 0x82, 0x73, 0x60, 0xa6, 0xaa,
+ 0xcb, 0x92, 0xac, 0x36, 0x94, 0x72, 0xad, 0x6e, 0x28, 0xea, 0x7a, 0xb9, 0xa6, 0x48, 0x7c, 0x62,
+ 0xb2, 0x20, 0x6f, 0xac, 0x29, 0xba, 0x2c, 0xf1, 0xc9, 0xca, 0x83, 0xbd, 0x2f, 0x42, 0x64, 0x6f,
+ 0x28, 0x70, 0xfb, 0x43, 0x81, 0x3b, 0x18, 0x0a, 0xdc, 0xe1, 0x50, 0xe0, 0xde, 0x1d, 0x09, 0x91,
+ 0xfd, 0x23, 0x21, 0x72, 0x70, 0x24, 0x44, 0x5e, 0x27, 0xc3, 0xe4, 0x37, 0x13, 0xec, 0xa6, 0xb9,
+ 0xf4, 0x23, 0x00, 0x00, 0xff, 0xff, 0xf2, 0x98, 0xcf, 0xc2, 0xe3, 0x0a, 0x00, 0x00,
}
func (m *CommonConnectionDetails) Marshal() (dAtA []byte, err error) {
@@ -548,6 +555,13 @@ func (m *CommonSessionDetails) MarshalToSizedBuffer(dAtA []byte) (int, error) {
_ = i
var l int
_ = l
+ if len(m.SystemIdentity) > 0 {
+ i -= len(m.SystemIdentity)
+ copy(dAtA[i:], m.SystemIdentity)
+ i = encodeVarintSessionEvents(dAtA, i, uint64(len(m.SystemIdentity)))
+ i--
+ dAtA[i] = 0x1a
+ }
if len(m.User) > 0 {
i -= len(m.User)
copy(dAtA[i:], m.User)
@@ -958,6 +972,10 @@ func (m *CommonSessionDetails) Size() (n int) {
if l > 0 {
n += 1 + l + sovSessionEvents(uint64(l))
}
+ l = len(m.SystemIdentity)
+ if l > 0 {
+ n += 1 + l + sovSessionEvents(uint64(l))
+ }
return n
}
@@ -1308,6 +1326,38 @@ func (m *CommonSessionDetails) Unmarshal(dAtA []byte) error {
}
m.User = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
+ case 3:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field SystemIdentity", wireType)
+ }
+ var stringLen uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return ErrIntOverflowSessionEvents
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ stringLen |= uint64(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ intStringLen := int(stringLen)
+ if intStringLen < 0 {
+ return ErrInvalidLengthSessionEvents
+ }
+ postIndex := iNdEx + intStringLen
+ if postIndex < 0 {
+ return ErrInvalidLengthSessionEvents
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.SystemIdentity = string(dAtA[iNdEx:postIndex])
+ iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipSessionEvents(dAtA[iNdEx:])
diff --git a/pkg/util/log/eventpb/session_events.proto b/pkg/util/log/eventpb/session_events.proto
index 757ced205e8e..387478e849a2 100644
--- a/pkg/util/log/eventpb/session_events.proto
+++ b/pkg/util/log/eventpb/session_events.proto
@@ -54,9 +54,15 @@ message CommonConnectionDetails {
message CommonSessionDetails {
// The connection type after transport negotiation.
string transport = 1 [(gogoproto.jsontag) = ",omitempty", (gogoproto.moretags) = "redact:\"nonsensitive\""];
- // The username the session is for. This is the username passed by
- // the client, after case-folding and Unicode normalization.
+ // The database username the session is for. This username will have
+ // undergone case-folding and Unicode normalization.
string user = 2 [(gogoproto.jsontag) = ",omitempty"];
+ // The original system identity provided by the client, if an identity
+ // mapping was used per Host-Based Authentication rules. This may be a
+ // GSSAPI or X.509 principal or any other external value, so no
+ // specific assumptions should be made about the contents of this
+ // field.
+ string system_identity = 3 [(gogoproto.jsontag) = ",omitempty"];
}
// ClientConnectionStart is reported when a client connection