Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

release-21.2: pgwire: Add dynamic user identity mapping #74459

Merged
merged 1 commit into from
Jan 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions docs/generated/eventlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand All @@ -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`

Expand All @@ -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`

Expand Down Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions docs/generated/settings/settings-for-tenants.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions docs/generated/settings/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
<tr><td><code>server.eventlog.enabled</code></td><td>boolean</td><td><code>true</code></td><td>if set, logged notable events are also stored in the table system.eventlog</td></tr>
<tr><td><code>server.eventlog.ttl</code></td><td>duration</td><td><code>2160h0m0s</code></td><td>if nonzero, entries in system.eventlog older than this duration are deleted every 10m0s. Should not be lowered below 24 hours.</td></tr>
<tr><td><code>server.host_based_authentication.configuration</code></td><td>string</td><td><code></code></td><td>host-based authentication configuration to use during connection authentication</td></tr>
<tr><td><code>server.identity_map.configuration</code></td><td>string</td><td><code></code></td><td>system-identity to database-username mappings</td></tr>
<tr><td><code>server.oidc_authentication.autologin</code></td><td>boolean</td><td><code>false</code></td><td>if true, logged-out visitors to the DB Console will be automatically redirected to the OIDC login endpoint (this feature is experimental)</td></tr>
<tr><td><code>server.oidc_authentication.button_text</code></td><td>string</td><td><code>Login with your OIDC provider</code></td><td>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)</td></tr>
<tr><td><code>server.oidc_authentication.claim_json_key</code></td><td>string</td><td><code></code></td><td>sets JSON key of principal to extract from payload after OIDC authentication completes (usually email or sid) (this feature is experimental)</td></tr>
Expand Down
1 change: 1 addition & 0 deletions pkg/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
43 changes: 42 additions & 1 deletion pkg/acceptance/compose/gss/psql/gss_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand All @@ -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`,
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/ccl/gssapiccl/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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": [],
Expand Down
128 changes: 128 additions & 0 deletions pkg/ccl/gssapiccl/get_user.go
Original file line number Diff line number Diff line change
@@ -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 <gssapi/gssapi.h>
// #include <stdlib.h>
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)
}
Loading