Skip to content

Commit 21c5277

Browse files
craig[bot]souravcrl
andcommitted
Merge #148200
148200: sql,provisioning: enable provisioning during LDAP authentication r=pritesh-lahoti,spilchen a=souravcrl After successful authentication with LDAP, CRDB will now also provision the LDAP user corresponding to the db user from the connection string. The user needs to have valid credentials for binding to LDAP server and we will skip other validations for pre-existence of the user and privilege to perform cluster sql login. informs #147602 fixes #147599 Epic CRDB-21590 Release note (enterprise change): Added a new cluster setting `server.provisioning.ldap.enabled` which can be set to true to conditionally enable user provisioning during sql cluster authentication. The user authenticates with the LDAP server and CRDB will only validate that bind to the IDP was successful for provisioning the user. All roles created thus will be privileged to perform sql authentication and will mandatorily have a role option for PROVISIONING_SOURCE set to `ldap:<idp_url>`. Any group roles that are to be assigned via ldap authorization must be pre created prior to the authentication start. Co-authored-by: souravcrl <[email protected]>
2 parents 4a528ac + 1f9236d commit 21c5277

File tree

17 files changed

+398
-4
lines changed

17 files changed

+398
-4
lines changed

docs/generated/eventlog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3543,6 +3543,7 @@ authentication failure.
35433543
| 7 | CREDENTIALS_EXPIRED | occur when the credentials provided by the client are expired. |
35443544
| 8 | NO_REPLICATION_ROLEOPTION | occurs when the connection requires a replication role option, but the user does not have it. |
35453545
| 9 | AUTHORIZATION_ERROR | is used for errors during the authorization phase. For example, this would include issues with mapping LDAP groups to SQL roles and granting those roles to the user. |
3546+
| 10 | PROVISIONING_ERROR | is used for errors during the user provisioning phase. This would include errors when the transaction to provision the authenticating user failed to execute. |
35463547

35473548

35483549

docs/generated/settings/settings-for-tenants.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ schedules.backup.gc_protection.enabled boolean true enable chaining of GC protec
110110
security.client_cert.subject_required.enabled boolean false mandates a requirement for subject role to be set for db user system-visible
111111
security.ocsp.mode enumeration off use OCSP to check whether TLS certificates are revoked. If the OCSP server is unreachable, in strict mode all certificates will be rejected and in lax mode all certificates will be accepted. [off = 0, lax = 1, strict = 2] application
112112
security.ocsp.timeout duration 3s timeout before considering the OCSP server unreachable application
113+
security.provisioning.ldap.enabled boolean false enables automatic creation of SQL users upon successful LDAP login application
113114
server.auth_log.sql_connections.enabled boolean false if set, log SQL client connect and disconnect events to the SESSIONS log channel (note: may hinder performance on loaded nodes) application
114115
server.auth_log.sql_sessions.enabled boolean false if set, log verbose SQL session authentication events to the SESSIONS log channel (note: may hinder performance on loaded nodes). Session start and end events are always logged regardless of this setting; disable the SESSIONS log channel to suppress them. application
115116
server.authentication_cache.enabled boolean true enables a cache used during authentication to avoid lookups to system tables when retrieving per-user authentication-related information application

docs/generated/settings/settings.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@
141141
<tr><td><div id="setting-security-client-cert-subject-required-enabled" class="anchored"><code>security.client_cert.subject_required.enabled</code></div></td><td>boolean</td><td><code>false</code></td><td>mandates a requirement for subject role to be set for db user</td><td>Dedicated/Self-hosted (read-write); Serverless (read-only)</td></tr>
142142
<tr><td><div id="setting-security-ocsp-mode" class="anchored"><code>security.ocsp.mode</code></div></td><td>enumeration</td><td><code>off</code></td><td>use OCSP to check whether TLS certificates are revoked. If the OCSP server is unreachable, in strict mode all certificates will be rejected and in lax mode all certificates will be accepted. [off = 0, lax = 1, strict = 2]</td><td>Serverless/Dedicated/Self-Hosted</td></tr>
143143
<tr><td><div id="setting-security-ocsp-timeout" class="anchored"><code>security.ocsp.timeout</code></div></td><td>duration</td><td><code>3s</code></td><td>timeout before considering the OCSP server unreachable</td><td>Serverless/Dedicated/Self-Hosted</td></tr>
144+
<tr><td><div id="setting-security-provisioning-ldap-enabled" class="anchored"><code>security.provisioning.ldap.enabled</code></div></td><td>boolean</td><td><code>false</code></td><td>enables automatic creation of SQL users upon successful LDAP login</td><td>Serverless/Dedicated/Self-Hosted</td></tr>
144145
<tr><td><div id="setting-server-auth-log-sql-connections-enabled" class="anchored"><code>server.auth_log.sql_connections.enabled</code></div></td><td>boolean</td><td><code>false</code></td><td>if set, log SQL client connect and disconnect events to the SESSIONS log channel (note: may hinder performance on loaded nodes)</td><td>Serverless/Dedicated/Self-Hosted</td></tr>
145146
<tr><td><div id="setting-server-auth-log-sql-sessions-enabled" class="anchored"><code>server.auth_log.sql_sessions.enabled</code></div></td><td>boolean</td><td><code>false</code></td><td>if set, log verbose SQL session authentication events to the SESSIONS log channel (note: may hinder performance on loaded nodes). Session start and end events are always logged regardless of this setting; disable the SESSIONS log channel to suppress them.</td><td>Serverless/Dedicated/Self-Hosted</td></tr>
146147
<tr><td><div id="setting-server-authentication-cache-enabled" class="anchored"><code>server.authentication_cache.enabled</code></div></td><td>boolean</td><td><code>true</code></td><td>enables a cache used during authentication to avoid lookups to system tables when retrieving per-user authentication-related information</td><td>Serverless/Dedicated/Self-Hosted</td></tr>

pkg/ccl/testccl/authccl/testdata/ldap

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,3 +380,134 @@ SELECT pg_has_role('ldap_user', 'ldap-parent-unsynced', 'MEMBER')
380380
ERROR: role 'ldap-parent-unsynced' does not exist (SQLSTATE 42704)
381381

382382
subtest end
383+
384+
subtest ldap_user_provisioning_valid
385+
386+
set_hba
387+
host all all 127.0.0.1/32 ldap ldapserver=localhost ldapport=636 ldapbasedn="O=security org,DC=localhost" ldapbinddn="CN=service_account,O=security org,DC=localhost" ldapbindpasswd=ldap_pwd ldapsearchattribute=sAMAccountName ldapsearchfilter="(memberOf=*)" "ldapgrouplistfilter=(cn=*)"
388+
----
389+
# Active authentication configuration on this node:
390+
# Original configuration:
391+
# loopback all all all trust # built-in CockroachDB default
392+
# host all root all cert-password # CockroachDB mandatory rule
393+
# host all all 127.0.0.1/32 ldap ldapserver=localhost ldapport=636 ldapbasedn="O=security org,DC=localhost" ldapbinddn="CN=service_account,O=security org,DC=localhost" ldapbindpasswd=ldap_pwd ldapsearchattribute=sAMAccountName ldapsearchfilter="(memberOf=*)" "ldapgrouplistfilter=(cn=*)"
394+
#
395+
# Interpreted configuration:
396+
# TYPE DATABASE USER ADDRESS METHOD OPTIONS
397+
loopback all all all trust
398+
host all root all cert-password
399+
host all all 127.0.0.1/32 ldap ldapserver=localhost ldapport=636 "ldapbasedn=O=security org,DC=localhost" "ldapbinddn=CN=service_account,O=security org,DC=localhost" ldapbindpasswd=ldap_pwd ldapsearchattribute=sAMAccountName "ldapsearchfilter=(memberOf=*)" "ldapgrouplistfilter=(cn=*)"
400+
401+
sql
402+
CREATE ROLE IF NOT EXISTS "ldap-parent-synced";
403+
----
404+
ok
405+
406+
ldap_mock set_groups=(unprovisioned_ldap_user,cn=ldap-parent-unsynced,cn=ldap-parent-synced)
407+
----
408+
409+
ldap_mock set_groups=(provisioned_ldap_user,cn=ldap-parent-unsynced,cn=ldap-parent-synced)
410+
----
411+
412+
connect user=unprovisioned_ldap_user password="ldap_pwd"
413+
----
414+
ERROR: password authentication failed for user unprovisioned_ldap_user (SQLSTATE 28P01)
415+
416+
sql
417+
set cluster setting security.provisioning.ldap.enabled = true;
418+
----
419+
ok
420+
421+
connect user=provisioned_ldap_user password="ldap_pwd"
422+
----
423+
ok defaultdb
424+
425+
query_row
426+
SELECT pg_has_role('provisioned_ldap_user', 'ldap-parent-synced', 'MEMBER')
427+
----
428+
true
429+
430+
sql
431+
GRANT admin to provisioned_ldap_user
432+
----
433+
ok
434+
435+
query_row
436+
SELECT options FROM [SHOW ROLES] AS r WHERE EXISTS (SELECT 1 FROM unnest(r.member_of) AS m(role_name) WHERE role_name = 'ldap-parent-synced')
437+
----
438+
PROVISIONSRC=ldap:localhost
439+
440+
subtest end
441+
442+
subtest ldap_user_provisioning_no_hba_ldap_method
443+
444+
set_hba
445+
host all all 127.0.0.1/32 cert-password
446+
----
447+
# Active authentication configuration on this node:
448+
# Original configuration:
449+
# loopback all all all trust # built-in CockroachDB default
450+
# host all root all cert-password # CockroachDB mandatory rule
451+
# host all all 127.0.0.1/32 cert-password
452+
#
453+
# Interpreted configuration:
454+
# TYPE DATABASE USER ADDRESS METHOD OPTIONS
455+
loopback all all all trust
456+
host all root all cert-password
457+
host all all 127.0.0.1/32 cert-password
458+
459+
sql
460+
CREATE ROLE IF NOT EXISTS "ldap-parent-synced";
461+
----
462+
ok
463+
464+
ldap_mock set_groups=(to_provision_ldap_user,cn=ldap-parent-unsynced,cn=ldap-parent-synced)
465+
----
466+
467+
sql
468+
set cluster setting security.provisioning.ldap.enabled = true;
469+
----
470+
ok
471+
472+
connect user=to_provision_ldap_user password="ldap_pwd"
473+
----
474+
ERROR: password authentication failed for user to_provision_ldap_user (SQLSTATE 28P01)
475+
476+
subtest end
477+
478+
subtest ldap_user_provisioning_invalid_ldap_password
479+
480+
set_hba
481+
host all ldap_user 127.0.0.1/32 ldap ldapserver=localhost ldapport=636 ldapbasedn="O=security org,DC=localhost" ldapbinddn="CN=service_account,O=security org,DC=localhost" ldapbindpasswd=ldap_pwd ldapsearchattribute=sAMAccountName ldapsearchfilter="(memberOf=*)" "ldapgrouplistfilter=(cn=*)"
482+
----
483+
# Active authentication configuration on this node:
484+
# Original configuration:
485+
# loopback all all all trust # built-in CockroachDB default
486+
# host all root all cert-password # CockroachDB mandatory rule
487+
# host all ldap_user 127.0.0.1/32 ldap ldapserver=localhost ldapport=636 ldapbasedn="O=security org,DC=localhost" ldapbinddn="CN=service_account,O=security org,DC=localhost" ldapbindpasswd=ldap_pwd ldapsearchattribute=sAMAccountName ldapsearchfilter="(memberOf=*)" "ldapgrouplistfilter=(cn=*)"
488+
#
489+
# Interpreted configuration:
490+
# TYPE DATABASE USER ADDRESS METHOD OPTIONS
491+
loopback all all all trust
492+
host all root all cert-password
493+
host all ldap_user 127.0.0.1/32 ldap ldapserver=localhost ldapport=636 "ldapbasedn=O=security org,DC=localhost" "ldapbinddn=CN=service_account,O=security org,DC=localhost" ldapbindpasswd=ldap_pwd ldapsearchattribute=sAMAccountName "ldapsearchfilter=(memberOf=*)" "ldapgrouplistfilter=(cn=*)"
494+
495+
sql
496+
CREATE ROLE IF NOT EXISTS "ldap-parent-synced";
497+
----
498+
ok
499+
500+
ldap_mock set_groups=(ldap_user,cn=ldap-parent-unsynced,cn=ldap-parent-synced)
501+
----
502+
503+
sql
504+
set cluster setting security.provisioning.ldap.enabled = true;
505+
----
506+
ok
507+
508+
connect user=ldap_user password="invalid"
509+
----
510+
ERROR: LDAP authentication: unable to bind as LDAP user (SQLSTATE 28000)
511+
DETAIL: credentials invalid for LDAP server user ldap_user
512+
513+
subtest end

pkg/security/provisioning/BUILD.bazel

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
22

33
go_library(
44
name = "provisioning",
5-
srcs = ["provisioning_source.go"],
5+
srcs = [
6+
"provisioning_source.go",
7+
"settings.go",
8+
],
69
importpath = "github.com/cockroachdb/cockroach/pkg/security/provisioning",
710
visibility = ["//visibility:public"],
8-
deps = ["@com_github_cockroachdb_errors//:errors"],
11+
deps = [
12+
"//pkg/settings",
13+
"//pkg/settings/cluster",
14+
"@com_github_cockroachdb_errors//:errors",
15+
],
916
)
1017

1118
go_test(

pkg/security/provisioning/provisioning_source.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func ParseProvisioningSource(sourceStr string) (*Source, error) {
5151
}
5252

5353
func parseAuthMethod(sourceStr string) (authMethod string, idp string, err error) {
54-
supportedProvisioningMethods := []string{"ldap"}
54+
supportedProvisioningMethods := []string{supportedAuthMethodLDAP}
5555
for _, method := range supportedProvisioningMethods {
5656
prefix := method + ":"
5757
if strings.HasPrefix(sourceStr, prefix) {
@@ -79,3 +79,7 @@ func parseIDP(idp string) (u *url.URL, err error) {
7979
func (source *Source) Size() int {
8080
return len(source.authMethod) + len(source.idp.String())
8181
}
82+
83+
func (source *Source) String() string {
84+
return source.authMethod + ":" + source.idp.String()
85+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2025 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the CockroachDB Software License
4+
// included in the /LICENSE file.
5+
6+
package provisioning
7+
8+
import (
9+
"github.com/cockroachdb/cockroach/pkg/settings"
10+
"github.com/cockroachdb/cockroach/pkg/settings/cluster"
11+
)
12+
13+
// All cluster settings necessary for the provisioning feature.
14+
const (
15+
supportedAuthMethodLDAP = "ldap"
16+
testSupportedAuthMethodCertPassword = "cert-password"
17+
baseProvisioningSettingName = "security.provisioning."
18+
ldapProvisioningEnableSettingName = baseProvisioningSettingName + "ldap.enabled"
19+
)
20+
21+
// UserProvisioningConfig allows for customization of automatic user
22+
// provisioning behavior. It is backed by cluster settings in a running node,
23+
// but may be overridden differently in CLI tools.
24+
type UserProvisioningConfig interface {
25+
Enabled(authMethod string) bool
26+
}
27+
28+
// ldapProvisioningEnabled enables automatic user provisioning for ldap
29+
// authentication method.
30+
var ldapProvisioningEnabled = settings.RegisterBoolSetting(
31+
settings.ApplicationLevel,
32+
ldapProvisioningEnableSettingName,
33+
"enables automatic creation of SQL users upon successful LDAP login",
34+
false,
35+
settings.WithReportable(true),
36+
settings.WithPublic,
37+
)
38+
39+
type clusterProvisioningConfig struct {
40+
settings *cluster.Settings
41+
}
42+
43+
var _ UserProvisioningConfig = clusterProvisioningConfig{}
44+
var Testing = struct {
45+
Supported bool
46+
}{}
47+
48+
// Enabled validates if automatic user provisioning is enabled for the provided
49+
// authentication method via cluster settings.
50+
func (c clusterProvisioningConfig) Enabled(authMethod string) bool {
51+
switch authMethod {
52+
case supportedAuthMethodLDAP:
53+
return ldapProvisioningEnabled.Get(&c.settings.SV)
54+
case testSupportedAuthMethodCertPassword:
55+
return Testing.Supported
56+
default:
57+
return false
58+
}
59+
}
60+
61+
// ClusterProvisioningConfig creates a UserProvisioningConfig backed by the
62+
// given cluster settings.
63+
func ClusterProvisioningConfig(settings *cluster.Settings) UserProvisioningConfig {
64+
return clusterProvisioningConfig{settings}
65+
}

pkg/sql/create_role.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/cockroachdb/cockroach/pkg/security/password"
1515
"github.com/cockroachdb/cockroach/pkg/security/username"
1616
"github.com/cockroachdb/cockroach/pkg/sql/catalog/descidgen"
17+
"github.com/cockroachdb/cockroach/pkg/sql/catalog/descs"
1718
"github.com/cockroachdb/cockroach/pkg/sql/decodeusername"
1819
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
1920
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
@@ -346,3 +347,27 @@ func (p *planner) checkPasswordAndGetHash(
346347

347348
return hashedPassword, nil
348349
}
350+
351+
// CreateRoleForProvisioning creates a role for the authenticating user if it is
352+
// enabled via `server.provisioning.ldap.enabled`. This is intended to be used
353+
// when there is an external source of truth configured for access to crdb node
354+
// (e.g. an LDAP server), and we need to enable the session by automatically
355+
// preconfiguring the desired user. This assumes the user is able to
356+
// authenticate against the external IDP and is sufficiently scoped to have a
357+
// role with SQLLOGIN priviledge.
358+
func CreateRoleForProvisioning(
359+
ctx context.Context,
360+
execCfg *ExecutorConfig,
361+
user username.SQLUsername,
362+
provisioningSource string,
363+
) error {
364+
return execCfg.InternalDB.DescsTxn(ctx, func(ctx context.Context, txn descs.Txn) error {
365+
userProvisioningStmt := fmt.Sprintf("CREATE USER IF NOT EXISTS %s WITH PROVISIONSRC %q", user.SQLIdentifier(), provisioningSource)
366+
if _, err := txn.Exec(
367+
ctx, "CreateRoleForProvisioning-provisioning", txn.KV(), userProvisioningStmt,
368+
); err != nil {
369+
return err
370+
}
371+
return nil
372+
})
373+
}

pkg/sql/logictest/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ go_library(
6565
"//pkg/kv/kvserver/txnwait",
6666
"//pkg/multitenant/tenantcapabilities",
6767
"//pkg/multitenant/tenantcapabilitiespb",
68+
"//pkg/security/provisioning",
6869
"//pkg/security/username",
6970
"//pkg/server",
7071
"//pkg/settings/cluster",

pkg/sql/logictest/logic.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import (
4444
"github.com/cockroachdb/cockroach/pkg/kv/kvserver/txnwait"
4545
"github.com/cockroachdb/cockroach/pkg/multitenant/tenantcapabilities"
4646
"github.com/cockroachdb/cockroach/pkg/multitenant/tenantcapabilitiespb"
47+
"github.com/cockroachdb/cockroach/pkg/security/provisioning"
4748
"github.com/cockroachdb/cockroach/pkg/security/username"
4849
"github.com/cockroachdb/cockroach/pkg/server"
4950
"github.com/cockroachdb/cockroach/pkg/settings/cluster"
@@ -3223,6 +3224,7 @@ func (t *logicTest) processSubtest(
32233224
return errors.Errorf("unknown user option: %s", fields[3])
32243225
}
32253226
newSession = true
3227+
provisioning.Testing.Supported = true
32263228
}
32273229
t.setSessionUser(fields[1], nodeIdx, newSession)
32283230
// In multi-tenant tests, we may need to also create database test when

0 commit comments

Comments
 (0)