diff --git a/pkg/ccl/ldapccl/BUILD.bazel b/pkg/ccl/ldapccl/BUILD.bazel index 2aa4ad86c266..ca6cc522db9f 100644 --- a/pkg/ccl/ldapccl/BUILD.bazel +++ b/pkg/ccl/ldapccl/BUILD.bazel @@ -6,6 +6,7 @@ go_library( "authentication_ldap.go", "authorization_ldap.go", "ldap_manager.go", + "ldap_test_util.go", "ldap_util.go", "settings.go", ], @@ -41,7 +42,6 @@ go_test( srcs = [ "authentication_ldap_test.go", "authorization_ldap_test.go", - "ldap_test_util_test.go", "main_test.go", "settings_test.go", ], @@ -56,7 +56,6 @@ go_test( "//pkg/security/securitytest", "//pkg/security/username", "//pkg/server", - "//pkg/sql/pgwire/hba", "//pkg/testutils", "//pkg/testutils/serverutils", "//pkg/testutils/testcluster", @@ -65,7 +64,6 @@ go_test( "//pkg/util/randutil", "@com_github_cockroachdb_errors//:errors", "@com_github_cockroachdb_redact//:redact", - "@com_github_go_ldap_ldap_v3//:ldap", "@com_github_stretchr_testify//require", ], ) diff --git a/pkg/ccl/ldapccl/authentication_ldap.go b/pkg/ccl/ldapccl/authentication_ldap.go index 5940d1e8c079..8fd79a7f89ac 100644 --- a/pkg/ccl/ldapccl/authentication_ldap.go +++ b/pkg/ccl/ldapccl/authentication_ldap.go @@ -80,16 +80,6 @@ func (authManager *ldapAuthManager) FetchLDAPUserDN( errors.Newf("LDAP authentication: unable to parse hba conf options") } - if err := authManager.validateLDAPBaseOptions(); err != nil { - return nil, redact.Sprintf("error validating base hba conf options for LDAP: %v", err), - errors.Newf("LDAP authentication: unable to validate authManager base options") - } - - if err := authManager.validateLDAPUserFetchOptions(); err != nil { - return nil, redact.Sprintf("error validating authentication hba conf options for LDAP: %v", err), - errors.Newf("LDAP authentication: unable to validate authManager authentication options") - } - // Establish a LDAPs connection with the set LDAP server and port err := authManager.mu.util.MaybeInitLDAPsConn(ctx, authManager.mu.conf) if err != nil { @@ -118,18 +108,6 @@ func (authManager *ldapAuthManager) FetchLDAPUserDN( return retrievedUserDN, "", nil } -// validateLDAPUserFetchOptions checks the ldap user search config values. -func (authManager *ldapAuthManager) validateLDAPUserFetchOptions() error { - const ldapOptionsErrorMsg = "ldap authentication params in HBA conf missing" - if authManager.mu.conf.ldapSearchFilter == "" { - return errors.New(ldapOptionsErrorMsg + " search filter") - } - if authManager.mu.conf.ldapSearchAttribute == "" { - return errors.New(ldapOptionsErrorMsg + " search attribute") - } - return nil -} - // ValidateLDAPLogin validates an attempt to bind provided user DN to configured LDAP server. // In particular, it checks that: // * The cluster has an enterprise license. @@ -173,11 +151,6 @@ func (authManager *ldapAuthManager) ValidateLDAPLogin( errors.Newf("LDAP authentication: unable to parse hba conf options") } - if err := authManager.validateLDAPBaseOptions(); err != nil { - return redact.Sprintf("error validating base hba conf options for LDAP: %v", err), - errors.Newf("LDAP authentication: unable to validate authManager base options") - } - // Establish a LDAPs connection with the set LDAP server and port err := authManager.mu.util.MaybeInitLDAPsConn(ctx, authManager.mu.conf) if err != nil { diff --git a/pkg/ccl/ldapccl/authentication_ldap_test.go b/pkg/ccl/ldapccl/authentication_ldap_test.go index 38f671c510e1..22d93b25b310 100644 --- a/pkg/ccl/ldapccl/authentication_ldap_test.go +++ b/pkg/ccl/ldapccl/authentication_ldap_test.go @@ -29,9 +29,7 @@ func TestLDAPFetchUser(t *testing.T) { // Intercept the call to NewLDAPUtil and return the mocked NewLDAPUtil function defer testutils.TestingHook( &NewLDAPUtil, - func(ctx context.Context, conf ldapConfig) (ILDAPUtil, error) { - return &mockLDAPUtil{tlsConfig: &tls.Config{}}, nil - })() + NewMockLDAPUtil)() ctx := context.Background() s := serverutils.StartServerOnly(t, base.TestServerArgs{}) defer s.Stopper().Stop(ctx) @@ -63,62 +61,34 @@ func TestLDAPFetchUser(t *testing.T) { hbaConfLDAPOpts: map[string]string{"invalidOpt": "invalidVal"}, user: "foo", fetchUserSuccess: false, expectedErr: "LDAP authentication: unable to parse hba conf options", expectedDetailedErrMsg: `error parsing hba conf options for LDAP: invalid LDAP option provided in hba conf: ‹invalidOpt›`}, - {testName: "empty server", - hbaConfLDAPOpts: map[string]string{"ldapserver": emptyParam}, user: "foo", fetchUserSuccess: false, - expectedErr: "LDAP authentication: unable to validate authManager base options", - expectedDetailedErrMsg: "error validating base hba conf options for LDAP: ldap params in HBA conf missing ldap server"}, {testName: "invalid server", hbaConfLDAPOpts: map[string]string{"ldapserver": invalidParam}, user: "foo", fetchUserSuccess: false, expectedErr: "LDAP authentication: unable to establish LDAP connection", expectedDetailedErrMsg: "error when trying to create LDAP connection: LDAPs connection failed: invalid ldap server provided"}, - {testName: "empty port", - hbaConfLDAPOpts: map[string]string{"ldapport": emptyParam}, user: "foo", fetchUserSuccess: false, - expectedErr: "LDAP authentication: unable to validate authManager base options", - expectedDetailedErrMsg: "error validating base hba conf options for LDAP: ldap params in HBA conf missing ldap port"}, {testName: "invalid port", hbaConfLDAPOpts: map[string]string{"ldapport": invalidParam}, user: "foo", fetchUserSuccess: false, expectedErr: "LDAP authentication: unable to establish LDAP connection", expectedDetailedErrMsg: "error when trying to create LDAP connection: LDAPs connection failed: invalid ldap port provided"}, - {testName: "empty base dn", - hbaConfLDAPOpts: map[string]string{"ldapbasedn": emptyParam}, user: "foo", fetchUserSuccess: false, - expectedErr: "LDAP authentication: unable to validate authManager base options", - expectedDetailedErrMsg: "error validating base hba conf options for LDAP: ldap params in HBA conf missing base DN"}, {testName: "invalid base dn", hbaConfLDAPOpts: map[string]string{"ldapbasedn": invalidParam}, user: "foo", fetchUserSuccess: false, expectedErr: "LDAP authentication: unable to find LDAP user distinguished name", expectedErrDetails: "cannot find provided user foo on LDAP server", expectedDetailedErrMsg: `error when searching for user in LDAP server: LDAP search failed: invalid base DN ‹"invalid"› provided`}, - {testName: "empty bind dn", - hbaConfLDAPOpts: map[string]string{"ldapbinddn": emptyParam}, user: "foo", fetchUserSuccess: false, - expectedErr: "LDAP authentication: unable to validate authManager base options", - expectedDetailedErrMsg: "error validating base hba conf options for LDAP: ldap params in HBA conf missing bind DN"}, {testName: "invalid bind dn", hbaConfLDAPOpts: map[string]string{"ldapbinddn": invalidParam}, user: "foo", fetchUserSuccess: false, expectedErr: "LDAP authentication: unable to find LDAP user distinguished name", expectedErrDetails: "cannot find provided user foo on LDAP server", expectedDetailedErrMsg: "error when searching for user in LDAP server: LDAP search failed: LDAP bind failed: invalid username provided"}, - {testName: "empty bind pwd", - hbaConfLDAPOpts: map[string]string{"ldapbindpasswd": emptyParam}, user: "foo", fetchUserSuccess: false, - expectedErr: "LDAP authentication: unable to validate authManager base options", - expectedDetailedErrMsg: "error validating base hba conf options for LDAP: ldap params in HBA conf missing bind password"}, {testName: "invalid bind pwd", hbaConfLDAPOpts: map[string]string{"ldapbindpasswd": invalidParam}, user: "foo", fetchUserSuccess: false, expectedErr: "LDAP authentication: unable to find LDAP user distinguished name", expectedErrDetails: "cannot find provided user foo on LDAP server", expectedDetailedErrMsg: "error when searching for user in LDAP server: LDAP search failed: LDAP bind failed: invalid password provided"}, - {testName: "empty search attribute", - hbaConfLDAPOpts: map[string]string{"ldapsearchattribute": emptyParam}, user: "foo", fetchUserSuccess: false, - expectedErr: "LDAP authentication: unable to validate authManager authentication options", - expectedDetailedErrMsg: "error validating authentication hba conf options for LDAP: ldap authentication params in HBA conf missing search attribute"}, {testName: "invalid search attribute", hbaConfLDAPOpts: map[string]string{"ldapsearchattribute": invalidParam}, user: "foo", fetchUserSuccess: false, expectedErr: "LDAP authentication: unable to find LDAP user distinguished name", expectedErrDetails: "cannot find provided user foo on LDAP server", expectedDetailedErrMsg: `error when searching for user in LDAP server: LDAP search failed: invalid search attribute ‹"invalid"› provided`}, - {testName: "empty search filter", - hbaConfLDAPOpts: map[string]string{"ldapsearchfilter": emptyParam}, user: "foo", fetchUserSuccess: false, - expectedErr: "LDAP authentication: unable to validate authManager authentication options", - expectedDetailedErrMsg: "error validating authentication hba conf options for LDAP: ldap authentication params in HBA conf missing search filter"}, {testName: "invalid search filter", hbaConfLDAPOpts: map[string]string{"ldapsearchfilter": invalidParam}, user: "foo", fetchUserSuccess: false, expectedErr: "LDAP authentication: unable to find LDAP user distinguished name", @@ -189,18 +159,10 @@ func TestLDAPAuthentication(t *testing.T) { hbaConfLDAPOpts: map[string]string{"invalidOpt": "invalidVal"}, user: "foo", pwd: "bar", ldapAuthSuccess: false, expectedErr: "LDAP authentication: unable to parse hba conf options", expectedDetailedErrMsg: `error parsing hba conf options for LDAP: invalid LDAP option provided in hba conf: ‹invalidOpt›`}, - {testName: "empty server", - hbaConfLDAPOpts: map[string]string{"ldapserver": emptyParam}, user: "foo", pwd: "bar", ldapAuthSuccess: false, - expectedErr: "LDAP authentication: unable to validate authManager base options", - expectedDetailedErrMsg: "error validating base hba conf options for LDAP: ldap params in HBA conf missing ldap server"}, {testName: "invalid server", hbaConfLDAPOpts: map[string]string{"ldapserver": invalidParam}, user: "foo", pwd: "bar", ldapAuthSuccess: false, expectedErr: "LDAP authentication: unable to establish LDAP connection", expectedDetailedErrMsg: "error when trying to create LDAP connection: LDAPs connection failed: invalid ldap server provided"}, - {testName: "empty port", - hbaConfLDAPOpts: map[string]string{"ldapport": emptyParam}, user: "foo", pwd: "bar", ldapAuthSuccess: false, - expectedErr: "LDAP authentication: unable to validate authManager base options", - expectedDetailedErrMsg: "error validating base hba conf options for LDAP: ldap params in HBA conf missing ldap port"}, {testName: "invalid port", hbaConfLDAPOpts: map[string]string{"ldapport": invalidParam}, user: "foo", pwd: "bar", ldapAuthSuccess: false, expectedErr: "LDAP authentication: unable to establish LDAP connection", diff --git a/pkg/ccl/ldapccl/authorization_ldap.go b/pkg/ccl/ldapccl/authorization_ldap.go index c5b3a9e66595..f3de821d6ae3 100644 --- a/pkg/ccl/ldapccl/authorization_ldap.go +++ b/pkg/ccl/ldapccl/authorization_ldap.go @@ -33,15 +33,6 @@ var ( authZSuccessCounter = telemetry.GetCounterOnce(authZSuccessCounterName) ) -// validateLDAPAuthZOptions checks the ldap authorization config values. -func (authManager *ldapAuthManager) validateLDAPAuthZOptions() error { - const ldapOptionsErrorMsg = "ldap authorization params in HBA conf missing" - if authManager.mu.conf.ldapGroupListFilter == "" { - return errors.New(ldapOptionsErrorMsg + " group list attribute") - } - return nil -} - // FetchLDAPGroups retrieves ldap groups for supplied ldap user DN. // In particular, it checks that: // * The cluster has an enterprise license. @@ -86,16 +77,6 @@ func (authManager *ldapAuthManager) FetchLDAPGroups( errors.Newf("LDAP authorization: unable to parse hba conf options") } - if err := authManager.validateLDAPBaseOptions(); err != nil { - return nil, redact.Sprintf("error validating base hba conf options for LDAP: %v", err), - errors.Newf("LDAP authorization: unable to validate authManager base options") - } - - if err := authManager.validateLDAPAuthZOptions(); err != nil { - return nil, redact.Sprintf("error validating authorization hba conf options for LDAP: %v", err), - errors.Newf("LDAP authorization: unable to validate authManager authorization options") - } - // Establish a LDAPs connection with the set LDAP server and port err := authManager.mu.util.MaybeInitLDAPsConn(ctx, authManager.mu.conf) if err != nil { diff --git a/pkg/ccl/ldapccl/authorization_ldap_test.go b/pkg/ccl/ldapccl/authorization_ldap_test.go index a33784beffb8..ff3ac2484794 100644 --- a/pkg/ccl/ldapccl/authorization_ldap_test.go +++ b/pkg/ccl/ldapccl/authorization_ldap_test.go @@ -29,7 +29,7 @@ func TestLDAPAuthorization(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) // Intercept the call to NewLDAPUtil and return the mocked NewLDAPUtil function - mockLDAP := &mockLDAPUtil{tlsConfig: &tls.Config{}} + mockLDAP := &mockLDAPUtil{} defer testutils.TestingHook( &NewLDAPUtil, func(ctx context.Context, conf ldapConfig) (ILDAPUtil, error) { @@ -65,53 +65,29 @@ func TestLDAPAuthorization(t *testing.T) { hbaConfLDAPOpts: map[string]string{"invalidOpt": "invalidVal"}, user: "cn=foo", authZSuccess: false, expectedErr: "LDAP authorization: unable to parse hba conf options", expectedDetailedErrMsg: `error parsing hba conf options for LDAP: invalid LDAP option provided in hba conf: ‹invalidOpt›`}, - {testName: "empty server", - hbaConfLDAPOpts: map[string]string{"ldapserver": emptyParam}, user: "cn=foo", authZSuccess: false, - expectedErr: "LDAP authorization: unable to validate authManager base options", - expectedDetailedErrMsg: "error validating base hba conf options for LDAP: ldap params in HBA conf missing ldap server"}, {testName: "invalid server", hbaConfLDAPOpts: map[string]string{"ldapserver": invalidParam}, user: "cn=foo", authZSuccess: false, expectedErr: "LDAP authorization: unable to establish LDAP connection", expectedDetailedErrMsg: "error when trying to create LDAP connection: LDAPs connection failed: invalid ldap server provided"}, - {testName: "empty port", - hbaConfLDAPOpts: map[string]string{"ldapport": emptyParam}, user: "cn=foo", authZSuccess: false, - expectedErr: "LDAP authorization: unable to validate authManager base options", - expectedDetailedErrMsg: "error validating base hba conf options for LDAP: ldap params in HBA conf missing ldap port"}, {testName: "invalid port", hbaConfLDAPOpts: map[string]string{"ldapport": invalidParam}, user: "cn=foo", authZSuccess: false, expectedErr: "LDAP authorization: unable to establish LDAP connection", expectedDetailedErrMsg: "error when trying to create LDAP connection: LDAPs connection failed: invalid ldap port provided"}, - {testName: "empty base dn", - hbaConfLDAPOpts: map[string]string{"ldapbasedn": emptyParam}, user: "cn=foo", authZSuccess: false, - expectedErr: "LDAP authorization: unable to validate authManager base options", - expectedDetailedErrMsg: "error validating base hba conf options for LDAP: ldap params in HBA conf missing base DN"}, {testName: "invalid base dn", hbaConfLDAPOpts: map[string]string{"ldapbasedn": invalidParam}, user: "cn=foo", authZSuccess: false, expectedErr: "LDAP authorization: unable to fetch groups for user", expectedErrDetails: "cannot find groups for which user is a member", expectedDetailedErrMsg: `error when fetching groups for user dn ‹"cn=foo"› in LDAP server: LDAP groups list failed: invalid base DN ‹"invalid"› provided`}, - {testName: "empty bind dn", - hbaConfLDAPOpts: map[string]string{"ldapbinddn": emptyParam}, user: "cn=foo", authZSuccess: false, - expectedErr: "LDAP authorization: unable to validate authManager base options", - expectedDetailedErrMsg: "error validating base hba conf options for LDAP: ldap params in HBA conf missing bind DN"}, {testName: "invalid bind dn", hbaConfLDAPOpts: map[string]string{"ldapbinddn": invalidParam}, user: "cn=foo", authZSuccess: false, expectedErr: "LDAP authorization: unable to fetch groups for user", expectedErrDetails: "cannot find groups for which user is a member", expectedDetailedErrMsg: `error when fetching groups for user dn ‹"cn=foo"› in LDAP server: LDAP groups list failed: LDAP bind failed: invalid username provided`}, - {testName: "empty bind pwd", - hbaConfLDAPOpts: map[string]string{"ldapbindpasswd": emptyParam}, user: "cn=foo", authZSuccess: false, - expectedErr: "LDAP authorization: unable to validate authManager base options", - expectedDetailedErrMsg: "error validating base hba conf options for LDAP: ldap params in HBA conf missing bind password"}, {testName: "invalid bind pwd", hbaConfLDAPOpts: map[string]string{"ldapbindpasswd": invalidParam}, user: "cn=foo", authZSuccess: false, expectedErr: "LDAP authorization: unable to fetch groups for user", expectedErrDetails: "cannot find groups for which user is a member", expectedDetailedErrMsg: `error when fetching groups for user dn ‹"cn=foo"› in LDAP server: LDAP groups list failed: LDAP bind failed: invalid password provided`}, - {testName: "empty group list filter", - hbaConfLDAPOpts: map[string]string{"ldapgrouplistfilter": emptyParam}, user: "cn=foo", authZSuccess: false, - expectedErr: "LDAP authorization: unable to validate authManager authorization options", - expectedDetailedErrMsg: "error validating authorization hba conf options for LDAP: ldap authorization params in HBA conf missing group list attribute"}, {testName: "invalid group list filter", hbaConfLDAPOpts: map[string]string{"ldapgrouplistfilter": invalidParam}, user: "cn=foo", authZSuccess: false, expectedErr: "LDAP authorization: unable to fetch groups for user", diff --git a/pkg/ccl/ldapccl/ldap_manager.go b/pkg/ccl/ldapccl/ldap_manager.go index 499adf8be284..e3831cd4856f 100644 --- a/pkg/ccl/ldapccl/ldap_manager.go +++ b/pkg/ccl/ldapccl/ldap_manager.go @@ -7,8 +7,12 @@ package ldapccl import ( "context" + "net/url" + "regexp" + "github.com/cockroachdb/cockroach/pkg/security/distinguishedname" "github.com/cockroachdb/cockroach/pkg/server/telemetry" + "github.com/cockroachdb/cockroach/pkg/settings" "github.com/cockroachdb/cockroach/pkg/settings/cluster" "github.com/cockroachdb/cockroach/pkg/sql/pgwire" "github.com/cockroachdb/cockroach/pkg/sql/pgwire/hba" @@ -23,7 +27,18 @@ const ( enableCounterName = counterPrefix + "enable" ) -var enableUseCounter = telemetry.GetCounterOnce(enableCounterName) +var ( + enableUseCounter = telemetry.GetCounterOnce(enableCounterName) + // ldapSearchRe performs a regex match for ldap search options provided in HBA + // configuration. This generally adheres to the format "(key=value)" with + // interleaved spaces but could be more flexible as value field could be + // provided as a regex string(mail=*@example.com), a key-value distinguished + // name(memberOf="CN=test") or a combination of both(memberOf="CN=Sh*"). + // + // The regex string is kept generic as search options could also contain + // multiple search entries like "(key1=value1)(key2=value2)". + ldapSearchRe = regexp.MustCompile(`\(\s*\S+\s*=\s*\S+.*\)`) +) // ldapAuthManager is an object that is used for both: // 1. enabling ldap connection validation that are used as part of the CRDB @@ -79,11 +94,10 @@ func (authManager *ldapAuthManager) reloadConfig(ctx context.Context, st *cluste // reloadConfig refreshes the values in conf from the cluster settings without locking the mutex. func (authManager *ldapAuthManager) reloadConfigLocked(ctx context.Context, st *cluster.Settings) { - conf := ldapConfig{ - domainCACert: LDAPDomainCACertificate.Get(&st.SV), - clientTLSCert: LDAPClientTLSCertSetting.Get(&st.SV), - clientTLSKey: LDAPClientTLSKeySetting.Get(&st.SV), - } + conf := authManager.mu.conf + conf.domainCACert = LDAPDomainCACertificate.Get(&st.SV) + conf.clientTLSCert = LDAPClientTLSCertSetting.Get(&st.SV) + conf.clientTLSKey = LDAPClientTLSKeySetting.Get(&st.SV) authManager.mu.conf = conf var err error @@ -103,9 +117,7 @@ func (authManager *ldapAuthManager) reloadConfigLocked(ctx context.Context, st * // setLDAPConfigOptions extracts hba conf parameters required for connecting and // querying LDAP server from hba conf entry and sets them for LDAP auth. func (authManager *ldapAuthManager) setLDAPConfigOptions(entry *hba.Entry) error { - conf := ldapConfig{ - domainCACert: authManager.mu.conf.domainCACert, - } + conf := authManager.mu.conf for _, opt := range entry.Options { switch opt[0] { case "ldapserver": @@ -132,23 +144,48 @@ func (authManager *ldapAuthManager) setLDAPConfigOptions(entry *hba.Entry) error return nil } -// validateLDAPBaseOptions checks the mandatory ldap auth config values for validity. -func (authManager *ldapAuthManager) validateLDAPBaseOptions() error { - const ldapOptionsErrorMsg = "ldap params in HBA conf missing" - if authManager.mu.conf.ldapServer == "" { - return errors.New(ldapOptionsErrorMsg + " ldap server") - } - if authManager.mu.conf.ldapPort == "" { - return errors.New(ldapOptionsErrorMsg + " ldap port") - } - if authManager.mu.conf.ldapBaseDN == "" { - return errors.New(ldapOptionsErrorMsg + " base DN") - } - if authManager.mu.conf.ldapBindDN == "" { - return errors.New(ldapOptionsErrorMsg + " bind DN") +// checkHBAEntryLDAP validates that the HBA entry for ldap has all the options +// set to acceptable values and mandatory options are all set. +func checkHBAEntryLDAP(_ *settings.Values, entry hba.Entry) error { + var parseErr error + entryOptions := map[string]bool{} + for _, opt := range entry.Options { + switch opt[0] { + case "ldapserver": + _, parseErr = url.Parse(opt[1]) + case "ldapport": + if opt[1] != "389" && opt[1] != "636" { + parseErr = errors.Newf("%q is not set to either 389 or 636", opt[0]) + } + case "ldapbasedn": + fallthrough + case "ldapbinddn": + _, parseErr = distinguishedname.ParseDN(opt[1]) + case "ldapbindpasswd": + fallthrough + case "ldapsearchattribute": + if opt[1] == "" { + parseErr = errors.Newf("%q is set to empty", opt[0]) + } + case "ldapsearchfilter": + fallthrough + case "ldapgrouplistfilter": + if !ldapSearchRe.MatchString(opt[1]) { + parseErr = errors.Newf("%q is not of the format \"(key = value)\"", opt[0]) + } + default: + return errors.Newf("unknown ldap option provided in hba conf: %q", opt[0]) + } + if parseErr != nil { + return errors.Wrapf(parseErr, "LDAP option %q is set to invalid value: %q", opt[0], opt[1]) + } + entryOptions[opt[0]] = true } - if authManager.mu.conf.ldapBindPassword == "" { - return errors.New(ldapOptionsErrorMsg + " bind password") + // check for missing ldap options + for _, opt := range []string{"ldapserver", "ldapport", "ldapbasedn", "ldapbinddn", "ldapbindpasswd", "ldapsearchattribute", "ldapsearchfilter"} { + if _, ok := entryOptions[opt]; !ok { + return errors.Newf("ldap option not found in hba entry: %q", opt) + } } return nil } @@ -178,4 +215,5 @@ var ConfigureLDAPAuth = func( func init() { pgwire.ConfigureLDAPAuth = ConfigureLDAPAuth + pgwire.RegisterAuthMethod("ldap", pgwire.AuthLDAP, hba.ConnAny, checkHBAEntryLDAP) } diff --git a/pkg/ccl/ldapccl/ldap_test_util_test.go b/pkg/ccl/ldapccl/ldap_test_util.go similarity index 96% rename from pkg/ccl/ldapccl/ldap_test_util_test.go rename to pkg/ccl/ldapccl/ldap_test_util.go index f0a7b466e71c..28fd644d5cea 100644 --- a/pkg/ccl/ldapccl/ldap_test_util_test.go +++ b/pkg/ccl/ldapccl/ldap_test_util.go @@ -28,6 +28,15 @@ type mockLDAPUtil struct { groupDNs []string } +var _ ILDAPUtil = &mockLDAPUtil{} + +var NewMockLDAPUtil func(context.Context, ldapConfig) (ILDAPUtil, error) = func( + ctx context.Context, + conf ldapConfig, +) (ILDAPUtil, error) { + return &mockLDAPUtil{}, nil +} + // MaybeInitLDAPsConn implements the ILDAPUtil interface. func (lu *mockLDAPUtil) MaybeInitLDAPsConn(ctx context.Context, conf ldapConfig) error { if strings.Contains(conf.ldapServer, invalidParam) { @@ -113,8 +122,6 @@ func (lu *mockLDAPUtil) ListGroups( return lu.groupDNs, nil } -var _ ILDAPUtil = &mockLDAPUtil{} - func constructHBAEntry( t *testing.T, hbaEntryBase string, diff --git a/pkg/ccl/logictestccl/testdata/logic_test/senstive_cluster_settings b/pkg/ccl/logictestccl/testdata/logic_test/senstive_cluster_settings index 6cc3b1327990..bee4e9d1965e 100644 --- a/pkg/ccl/logictestccl/testdata/logic_test/senstive_cluster_settings +++ b/pkg/ccl/logictestccl/testdata/logic_test/senstive_cluster_settings @@ -13,8 +13,10 @@ statement ok SET CLUSTER SETTING server.identity_map.configuration = "crdb fake_external_userid fake_user" statement ok -SET CLUSTER SETTING server.host_based_authentication.configuration = "host all fake_user all ldap ldapbindpasswd=fake_password map=crdb -host all all all trust" +SET CLUSTER SETTING server.host_based_authentication.configuration = +'host all fake_user all ldap ldapserver=example.com ldapport=636 ldapbasedn="DC=example,DC=com" ldapbinddn="CN=testuser,DC=example,DC=com" ldapbindpasswd=pass ldapsearchattribute=cn ldapsearchfilter=(email=*) +host all fake_user all cert map=crdb +host all all all trust' statement ok SET CLUSTER SETTING server.redact_sensitive_settings.enabled = false @@ -43,7 +45,8 @@ crdb fake_external_userid fake_user query T SHOW CLUSTER SETTING server.host_based_authentication.configuration ---- -host all fake_user all ldap ldapbindpasswd=fake_password map=crdb +host all fake_user all ldap ldapserver=example.com ldapport=636 ldapbasedn="DC=example,DC=com" ldapbinddn="CN=testuser,DC=example,DC=com" ldapbindpasswd=pass ldapsearchattribute=cn ldapsearchfilter=(email=*) +host all fake_user all cert map=crdb host all all all trust query TT rowsort @@ -66,7 +69,8 @@ SELECT variable, value FROM [show all cluster settings] WHERE variable = 'server.host_based_authentication.configuration' ---- -server.host_based_authentication.configuration host all fake_user all ldap ldapbindpasswd=fake_password map=crdb +server.host_based_authentication.configuration host all fake_user all ldap ldapserver=example.com ldapport=636 ldapbasedn="DC=example,DC=com" ldapbinddn="CN=testuser,DC=example,DC=com" ldapbindpasswd=pass ldapsearchattribute=cn ldapsearchfilter=(email=*) + host all fake_user all cert map=crdb host all all all trust user root @@ -148,7 +152,8 @@ crdb fake_external_userid fake_user query T SHOW CLUSTER SETTING server.host_based_authentication.configuration ---- -host all fake_user all ldap ldapbindpasswd=fake_password map=crdb +host all fake_user all ldap ldapserver=example.com ldapport=636 ldapbasedn="DC=example,DC=com" ldapbinddn="CN=testuser,DC=example,DC=com" ldapbindpasswd=pass ldapsearchattribute=cn ldapsearchfilter=(email=*) +host all fake_user all cert map=crdb host all all all trust query TT rowsort @@ -171,7 +176,8 @@ SELECT variable, value FROM [show all cluster settings] WHERE variable = 'server.host_based_authentication.configuration' ---- -server.host_based_authentication.configuration host all fake_user all ldap ldapbindpasswd=fake_password map=crdb +server.host_based_authentication.configuration host all fake_user all ldap ldapserver=example.com ldapport=636 ldapbasedn="DC=example,DC=com" ldapbinddn="CN=testuser,DC=example,DC=com" ldapbindpasswd=pass ldapsearchattribute=cn ldapsearchfilter=(email=*) + host all fake_user all cert map=crdb host all all all trust # Verify that tenant overrides for sensitive settings can only be viewed with @@ -189,8 +195,10 @@ statement ok ALTER TENANT [10] SET CLUSTER SETTING server.identity_map.configuration = "crdb fake_external_userid fake_user" statement ok -ALTER TENANT [10] SET CLUSTER SETTING server.host_based_authentication.configuration = "host all fake_user all ldap ldapbindpasswd=fake_password map=crdb -host all all all trust" +ALTER TENANT [10] SET CLUSTER SETTING server.host_based_authentication.configuration = +'host all fake_user all ldap ldapserver=example.com ldapport=636 ldapbasedn="DC=example,DC=com" ldapbinddn="CN=testuser,DC=example,DC=com" ldapbindpasswd=pass ldapsearchattribute=cn ldapsearchfilter=(email=*) +host all fake_user all cert map=crdb +host all all all trust' statement ok CREATE USER testuser @@ -269,7 +277,7 @@ SELECT variable, value, origin FROM [SHOW CLUSTER SETTINGS FOR TENANT [10]] WHERE variable = 'server.host_based_authentication.configuration' ---- -server.host_based_authentication.configuration host all fake_user all ldap ldapbindpasswd=fake_password map=crdb\nhost all all all trust per-tenant-override +server.host_based_authentication.configuration host all fake_user all ldap ldapserver=example.com ldapport=636 ldapbasedn="DC=example,DC=com" ldapbinddn="CN=testuser,DC=example,DC=com" ldapbindpasswd=pass ldapsearchattribute=cn ldapsearchfilter=(email=*)\nhost all fake_user all cert map=crdb\nhost all all all trust per-tenant-override query T SHOW CLUSTER SETTING server.identity_map.configuration FOR TENANT [10] @@ -279,5 +287,6 @@ crdb fake_external_userid fake_user query T SHOW CLUSTER SETTING server.host_based_authentication.configuration FOR TENANT [10] ---- -host all fake_user all ldap ldapbindpasswd=fake_password map=crdb +host all fake_user all ldap ldapserver=example.com ldapport=636 ldapbasedn="DC=example,DC=com" ldapbinddn="CN=testuser,DC=example,DC=com" ldapbindpasswd=pass ldapsearchattribute=cn ldapsearchfilter=(email=*) +host all fake_user all cert map=crdb host all all all trust diff --git a/pkg/ccl/testccl/authccl/BUILD.bazel b/pkg/ccl/testccl/authccl/BUILD.bazel index 20e3750e216d..4836684160dd 100644 --- a/pkg/ccl/testccl/authccl/BUILD.bazel +++ b/pkg/ccl/testccl/authccl/BUILD.bazel @@ -11,6 +11,7 @@ go_test( "//pkg/base", "//pkg/ccl", "//pkg/ccl/jwtauthccl", + "//pkg/ccl/ldapccl", "//pkg/security/securityassets", "//pkg/security/securitytest", "//pkg/security/username", diff --git a/pkg/ccl/testccl/authccl/auth_test.go b/pkg/ccl/testccl/authccl/auth_test.go index 72f78163bac0..3a7c3d5fb014 100644 --- a/pkg/ccl/testccl/authccl/auth_test.go +++ b/pkg/ccl/testccl/authccl/auth_test.go @@ -10,6 +10,7 @@ import ( "context" gosql "database/sql" "fmt" + "io" "math" "net" "net/http" @@ -25,6 +26,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/base" "github.com/cockroachdb/cockroach/pkg/ccl/jwtauthccl" + "github.com/cockroachdb/cockroach/pkg/ccl/ldapccl" "github.com/cockroachdb/cockroach/pkg/security/username" "github.com/cockroachdb/cockroach/pkg/server/apiconstants" "github.com/cockroachdb/cockroach/pkg/server/authserver" @@ -110,7 +112,7 @@ func TestAuthenticationAndHBARules(t *testing.T) { skip.UnderRace(t, "takes >1min under race") testutils.RunTrueAndFalse(t, "insecure", func(t *testing.T, insecure bool) { - jwtRunTest(t, insecure) + authCCLRunTest(t, insecure) }) } @@ -145,8 +147,7 @@ func makeSocketFile(t *testing.T) (socketDir, socketFile string, cleanupFn func( func() { _ = os.RemoveAll(tempDir) } } -func jwtRunTest(t *testing.T, insecure bool) { - +func authCCLRunTest(t *testing.T, insecure bool) { datadriven.Walk(t, datapathutils.TestDataPath(t), func(t *testing.T, path string) { defer leaktest.AfterTest(t)() @@ -190,13 +191,28 @@ func jwtRunTest(t *testing.T, insecure bool) { }) defer srv.Stopper().Stop(context.Background()) s := srv.ApplicationLayer() + pgServer := s.PGServer().(*pgwire.Server) + pgServer.TestingEnableConnLogging() + pgServer.TestingEnableAuthLogging() + s.PGPreServer().(*pgwire.PreServeConnHandler).TestingAcceptSystemIdentityOption(true) + httpClient, err := s.GetAdminHTTPClient() + if err != nil { + t.Fatal(err) + } + httpHBAUrl := s.AdminURL().WithPath("/debug/hba_conf").String() sv := &s.ClusterSettings().SV - if _, err := conn.ExecContext(context.Background(), fmt.Sprintf(`CREATE USER %s`, username.TestUser)); err != nil { t.Fatal(err) } + // Intercept the call to NewLDAPUtil and return the mocked NewLDAPUtil function + if strings.Contains(path, "ldap") { + defer testutils.TestingHook( + &ldapccl.NewLDAPUtil, + ldapccl.NewMockLDAPUtil)() + } + datadriven.RunTest(t, path, func(t *testing.T, td *datadriven.TestData) string { resultString, err := func() (string, error) { switch td.Cmd { @@ -420,6 +436,47 @@ func jwtRunTest(t *testing.T, insecure bool) { defer resp.Body.Close() return strconv.Itoa(resp.StatusCode), nil + case "set_hba": + _, err := conn.ExecContext(context.Background(), + `SET CLUSTER SETTING server.host_based_authentication.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 := pgwire.DefaultHBAConfig + if td.Input != "" { + expConf, err = pgwire.ParseAndNormalize(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( + "HBA config 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 := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + return string(body), nil + default: td.Fatalf(t, "unknown command: %s", td.Cmd) } diff --git a/pkg/ccl/testccl/authccl/testdata/ldap b/pkg/ccl/testccl/authccl/testdata/ldap new file mode 100644 index 000000000000..40595816fb9b --- /dev/null +++ b/pkg/ccl/testccl/authccl/testdata/ldap @@ -0,0 +1,168 @@ +# Verify LDAP HBA entry and authentication/authorization works. + +config secure +---- + +sql +CREATE USER ldap_user; +CREATE USER test; +CREATE USER test2; +---- +ok + +subtest missing_ldap_hba_param + +set_hba +host all ldap_user 127.0.0.1/32 ldap +---- +ERROR: ldap option not found in hba entry: "ldapserver" + +set_hba +host all ldap_user 127.0.0.1/32 ldap ldapserver=localhost +---- +ERROR: ldap option not found in hba entry: "ldapport" + +set_hba +host all ldap_user 127.0.0.1/32 ldap ldapserver=localhost ldapport=636 +---- +ERROR: ldap option not found in hba entry: "ldapbasedn" + +set_hba +host all ldap_user 127.0.0.1/32 ldap ldapserver=localhost ldapport=636 ldapbasedn="O=security org,DC=localhost" +---- +ERROR: ldap option not found in hba entry: "ldapbinddn" + +set_hba +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" +---- +ERROR: ldap option not found in hba entry: "ldapbindpasswd" + +set_hba +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 +---- +ERROR: ldap option not found in hba entry: "ldapsearchattribute" + +set_hba +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 +---- +ERROR: ldap option not found in hba entry: "ldapsearchfilter" + +subtest end + +subtest incorrect_ldap_hba_param + +set_hba +host all ldap_user 127.0.0.1/32 ldap ldapserver=":invalid" ldapport=636 ldapbasedn="O=security org,DC=localhost" ldapbinddn="CN=service_account,O=security org,DC=localhost" ldapbindpasswd=ldap_pwd ldapsearchattribute=sAMAccountName ldapsearchfilter="(memberOf=*)" +---- +ERROR: LDAP option "ldapserver" is set to invalid value: ":invalid": parse ":invalid": missing protocol scheme + +set_hba +host all ldap_user 127.0.0.1/32 ldap ldapserver=localhost ldapport=007 ldapbasedn="O=security org,DC=localhost" ldapbinddn="CN=service_account,O=security org,DC=localhost" ldapbindpasswd=ldap_pwd ldapsearchattribute=sAMAccountName ldapsearchfilter="(memberOf=*)" +---- +ERROR: LDAP option "ldapport" is set to invalid value: "007": "ldapport" is not set to either 389 or 636 + +set_hba +host all ldap_user 127.0.0.1/32 ldap ldapserver=localhost ldapport=636 ldapbasedn="baseDN" ldapbinddn="CN=service_account,O=security org,DC=localhost" ldapbindpasswd=ldap_pwd ldapsearchattribute=sAMAccountName ldapsearchfilter="(memberOf=*)" +---- +ERROR: LDAP option "ldapbasedn" is set to invalid value: "baseDN": failed to parse distinguished name baseDN: DN ended with incomplete type, value pair + +set_hba +host all ldap_user 127.0.0.1/32 ldap ldapserver=localhost ldapport=636 ldapbasedn="O=security org,DC=localhost" ldapbinddn="bindDN" ldapbindpasswd=ldap_pwd ldapsearchattribute=sAMAccountName ldapsearchfilter="(memberOf=*)" +---- +ERROR: LDAP option "ldapbinddn" is set to invalid value: "bindDN": failed to parse distinguished name bindDN: DN ended with incomplete type, value pair + +set_hba +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="" ldapsearchattribute=sAMAccountName ldapsearchfilter="(memberOf=*)" +---- +ERROR: LDAP option "ldapbindpasswd" is set to invalid value: "": "ldapbindpasswd" is set to empty + +set_hba +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="" ldapsearchfilter="(memberOf=*)" +---- +ERROR: LDAP option "ldapsearchattribute" is set to invalid value: "": "ldapsearchattribute" is set to empty + +set_hba +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="(*)" +---- +ERROR: LDAP option "ldapsearchfilter" is set to invalid value: "(*)": "ldapsearchfilter" is not of the format "(key = value)" + +subtest end + +subtest invalid_ldap_password + +set_hba +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=*)" +---- +# Active authentication configuration on this node: +# Original configuration: +# loopback all all all trust # built-in CockroachDB default +# host all root all cert-password # CockroachDB mandatory rule +# 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=*)" +# +# Interpreted configuration: +# TYPE DATABASE USER ADDRESS METHOD OPTIONS +loopback all all all trust +host all root all cert-password +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=*)" + +connect user=ldap_user password="invalid" +---- +ERROR: LDAP authentication: unable to bind as LDAP user (SQLSTATE 28000) +DETAIL: credentials invalid for LDAP server user ldap_user + +subtest end + +subtest correct_ldap_password + +connect user=ldap_user password="valid" +---- +ok defaultdb + +subtest end + +subtest unknown_ldap_user + +set_hba +host all invalid_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=*)" +---- +# Active authentication configuration on this node: +# Original configuration: +# loopback all all all trust # built-in CockroachDB default +# host all root all cert-password # CockroachDB mandatory rule +# host all invalid_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=*)" +# +# Interpreted configuration: +# TYPE DATABASE USER ADDRESS METHOD OPTIONS +loopback all all all trust +host all root all cert-password +host all invalid_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=*)" + +connect user=invalid_ldap_user password="valid" +---- +ERROR: LDAP authentication: unable to find LDAP user distinguished name (SQLSTATE 28000) +DETAIL: cannot find provided user invalid_ldap_user on LDAP server + +subtest end + +subtest unknown_sql_user + +set_hba +host all unknown_sql_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=*)" +---- +# Active authentication configuration on this node: +# Original configuration: +# loopback all all all trust # built-in CockroachDB default +# host all root all cert-password # CockroachDB mandatory rule +# host all unknown_sql_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=*)" +# +# Interpreted configuration: +# TYPE DATABASE USER ADDRESS METHOD OPTIONS +loopback all all all trust +host all root all cert-password +host all unknown_sql_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=*)" + +connect user=unknown_sql_user password="valid" +---- +ERROR: password authentication failed for user unknown_sql_user (SQLSTATE 28P01) + +subtest end diff --git a/pkg/sql/pgwire/auth_methods.go b/pkg/sql/pgwire/auth_methods.go index ef400a1a8b26..bbef6163d8cf 100644 --- a/pkg/sql/pgwire/auth_methods.go +++ b/pkg/sql/pgwire/auth_methods.go @@ -82,13 +82,6 @@ func loadDefaultMethods() { // The "trust" method accepts any connection attempt that matches // the current rule. RegisterAuthMethod("trust", authTrust, hba.ConnAny, NoOptionsAllowed) - // The "ldap" method requires a clear text password which will be used to bind - // with a LDAP server. The remaining connection parameters are provided in hba - // conf options - // - // Care should be taken by administrators to only accept this auth - // method over secure connections, e.g. those encrypted using SSL. - RegisterAuthMethod("ldap", authLDAP, hba.ConnAny, nil) } // AuthMethod is a top-level factory for composing the various @@ -112,7 +105,7 @@ var _ AuthMethod = authTrust var _ AuthMethod = authReject var _ AuthMethod = authSessionRevivalToken([]byte{}) var _ AuthMethod = authJwtToken -var _ AuthMethod = authLDAP +var _ AuthMethod = AuthLDAP // authPassword is the AuthMethod constructor for HBA method // "password": authenticate using a cleartext password received from @@ -918,9 +911,14 @@ var ConfigureLDAPAuth = func( return &noLDAPConfigured{} } -// authLDAP is the AuthMethod constructor for the CRDB-specific -// ldap auth mechanism. -func authLDAP( +// AuthLDAP is the AuthMethod constructor for the CRDB-specific ldap auth +// mechanism. The "LDAP" method requires a clear text password which will be +// used to bind with a LDAP server. The remaining connection parameters are +// provided in hba conf options. +// +// Care should be taken by administrators to only accept this auth method over +// secure connections, e.g. those encrypted using SSL. +func AuthLDAP( sCtx context.Context, c AuthConn, sessionUser username.SQLUsername,