From d8c771395990e93bd9f80a2c8fe2a4bc0683bb5c Mon Sep 17 00:00:00 2001 From: Piyush Jain Date: Thu, 10 Jun 2021 17:16:29 -0700 Subject: [PATCH] [#4421] [YCQL] Enable LDAP based authentication Summary: In terms of supported functionality - YCQL will support all options that are allowed in YSQL's LDAP auth. Broadly, this includes the simple bind and search + bind mode. Instead of a full blown file based auth config like ysql_hba.conf in YSQL (where the config supports many features apart from LDAP), we chose to allow LDAP configuration using a set of gflags. This is simpler to do now. In case we later plan to support more auth based rules based on remote ip, keyspace name, etc, we can add a similar auth config file for YCQL. Most code is almost just a copy paste from src/postgres/src/backend/libpq/auth.c - 1. InitializeLDAPConnection() 2. CheckLDAPAuth() 3. errdetail_for_ldap() is logic present in LDAPError class Given the minimal functionality needed, a copy paste from auth.c is a simpler and less error-prone than making functions in auth.c generic enough to be used from both postgres and YCQL proxy. One difference is that - LDAP_DEPRECATED is removed and so some interface calls to ldap library are different. Test Plan: ./yb_build.sh --java-test org.yb.cql.TestLDAPAuth Jenkins: urgent Reviewers: mihnea, neil, alan, dmitry Reviewed By: dmitry Subscribers: yql Differential Revision: https://phabricator.dev.yugabyte.com/D12095 --- java/yb-cql/pom.xml | 17 + .../org/yb/cql/BaseAuthenticationCQLTest.java | 7 +- .../src/test/java/org/yb/cql/BaseCQLTest.java | 2 +- .../test/java/org/yb/cql/TestLDAPAuth.java | 294 +++++++++++++++++ src/yb/yql/cql/cqlserver/CMakeLists.txt | 4 +- src/yb/yql/cql/cqlserver/cql_processor.cc | 301 +++++++++++++++++- 6 files changed, 609 insertions(+), 16 deletions(-) create mode 100644 java/yb-cql/src/test/java/org/yb/cql/TestLDAPAuth.java diff --git a/java/yb-cql/pom.xml b/java/yb-cql/pom.xml index 779271331f93..dc7c60cd3ec6 100644 --- a/java/yb-cql/pom.xml +++ b/java/yb-cql/pom.xml @@ -71,5 +71,22 @@ junit test + + org.apache.directory.server + apacheds-all + 2.0.0-M22 + + + + org.apache.directory.shared + shared-ldap-schema + + + org.apache.directory.api + api-ldap-schema-data + + + diff --git a/java/yb-cql/src/test/java/org/yb/cql/BaseAuthenticationCQLTest.java b/java/yb-cql/src/test/java/org/yb/cql/BaseAuthenticationCQLTest.java index 0bcd1c253553..fc5c3780c897 100644 --- a/java/yb-cql/src/test/java/org/yb/cql/BaseAuthenticationCQLTest.java +++ b/java/yb-cql/src/test/java/org/yb/cql/BaseAuthenticationCQLTest.java @@ -113,8 +113,11 @@ public void checkConnectivityWithMessage(boolean usingAuth, assertFalse(expectFailure); } catch (com.datastax.driver.core.exceptions.AuthenticationException e) { // If we're expecting a failure, we should be in here. - assertTrue(expectFailure); - assertTrue(e.getMessage().contains(expectedMessage)); + assertTrue(e.getMessage(), expectFailure); + if (!e.getMessage().contains(expectedMessage)) { + LOG.info("Expecting '" + expectedMessage + "' contained in '" + e.getMessage() + "'"); + assertTrue(false); + } } } diff --git a/java/yb-cql/src/test/java/org/yb/cql/BaseCQLTest.java b/java/yb-cql/src/test/java/org/yb/cql/BaseCQLTest.java index 9056352a5135..8c42c3985d0c 100644 --- a/java/yb-cql/src/test/java/org/yb/cql/BaseCQLTest.java +++ b/java/yb-cql/src/test/java/org/yb/cql/BaseCQLTest.java @@ -457,7 +457,7 @@ protected void dropRoles() throws Exception { String roleName = row.getString("role"); if (!DEFAULT_ROLE.equals(roleName)) { LOG.info("Dropping role " + roleName); - session.execute("DROP ROLE " + roleName); + session.execute("DROP ROLE '" + roleName + "'"); } } } diff --git a/java/yb-cql/src/test/java/org/yb/cql/TestLDAPAuth.java b/java/yb-cql/src/test/java/org/yb/cql/TestLDAPAuth.java new file mode 100644 index 000000000000..3dca4bacf6c1 --- /dev/null +++ b/java/yb-cql/src/test/java/org/yb/cql/TestLDAPAuth.java @@ -0,0 +1,294 @@ +// Copyright (c) YugaByte, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations +// under the License. +// +package org.yb.cql; + + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import com.datastax.driver.core.ProtocolOptions; + +import org.apache.directory.server.annotations.CreateLdapServer; +import org.apache.directory.server.annotations.CreateTransport; +import org.apache.directory.server.core.annotations.ApplyLdifs; +import org.apache.directory.server.core.annotations.CreateDS; +import org.apache.directory.server.core.annotations.CreatePartition; +import org.apache.directory.server.core.integ.CreateLdapServerRule; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.yb.YBTestRunner; + +// TODO(Piyush): Test with TLS and LDAPS + +@RunWith(value=YBTestRunner.class) +@CreateDS(name = "myDS", + partitions = { + @CreatePartition(name = "test", suffix = "dc=myorg,dc=com") + }) +@CreateLdapServer(transports = { + @CreateTransport(protocol = "LDAP", address = "localhost", port=10389)}) +@ApplyLdifs({ + "dn: dc=myorg,dc=com", + "objectClass: domain", + "objectClass: top", + "dc: myorg", + "dn: ou=Users,dc=myorg,dc=com", + "objectClass: organizationalUnit", + "objectClass: top", + "ou: Users", + "dn: cn=admin,ou=Users,dc=myorg,dc=com", + "objectClass: inetOrgPerson", + "objectClass: organizationalPerson", + "objectClass: person", + "objectClass: top", + "cn: admin", + "sn: Ldap", + "uid: adminUid", + "userPassword: adminPasswd", + "dn: cn=testUser1,ou=Users,dc=myorg,dc=com", + "objectClass: inetOrgPerson", + "objectClass: organizationalPerson", + "objectClass: person", + "objectClass: top", + "cn: testUser1", + "sn: Ldap", + "uid: testUser1Uid", + "userPassword: 12345", + "dn: cn=testUserNonUnique,ou=Users,dc=myorg,dc=com", + "objectClass: inetOrgPerson", + "objectClass: organizationalPerson", + "objectClass: person", + "objectClass: top", + "cn: testUserNonUnique", + "sn: Ldap", + "uid: testUserNonUniqueUid", + "userPassword: 12345", + "dn: cn=testUserNonUnique,dc=myorg,dc=com", + "objectClass: inetOrgPerson", + "objectClass: organizationalPerson", + "objectClass: person", + "objectClass: top", + "cn: testUserNonUnique", + "sn: Ldap", + "uid: testUserNonUniqueUid", + "userPassword: 12345", +}) +public class TestLDAPAuth extends BaseAuthenticationCQLTest { + private static final Logger LOG = LoggerFactory.getLogger(TestLDAPAuth.class); + + @ClassRule + public static CreateLdapServerRule serverRule = new CreateLdapServerRule(); + + private void recreateMiniCluster(Map extraTserverFlag) throws Exception { + destroyMiniCluster(); + Map tserverFlags = new HashMap<>(); + tserverFlags.put("use_cassandra_authentication", "true"); + tserverFlags.put("ycql_use_ldap", "true"); + tserverFlags.put("ycql_ldap_users_to_skip_csv", "cassandra"); + tserverFlags.put("ycql_ldap_server", "ldap://localhost:10389"); + tserverFlags.put("ycql_ldap_tls", "false"); + tserverFlags.put("vmodule", "cql_processor=4"); + tserverFlags.putAll(extraTserverFlag); + createMiniCluster(Collections.emptyMap(), tserverFlags); + setUpCqlClient(); + } + + @Test + public void incorrectLDAPServer() throws Exception { + Map extraTserverFlagMap = getTServerFlags(); + extraTserverFlagMap.put("ycql_ldap_server", "ldap://localhost:1039"); + extraTserverFlagMap.put("ycql_ldap_user_prefix", "cn="); + extraTserverFlagMap.put("ycql_ldap_user_suffix", ",ou=Users,dc=myorg,dc=com"); + recreateMiniCluster(extraTserverFlagMap); + session.execute("CREATE ROLE 'testUser1' WITH LOGIN = true"); + + checkConnectivityWithMessage(true, "testUser1", "12345", ProtocolOptions.Compression.NONE, + true /* expectFailure */, "Can't contact LDAP server"); + session.execute("DROP ROLE 'testUser1'"); + } + + @Test + public void simpleBindMode() throws Exception { + // Test with incorrect user prefix + Map extraTserverFlagMap = new HashMap(); + extraTserverFlagMap.put("ycql_ldap_user_prefix", "dummy="); + extraTserverFlagMap.put("ycql_ldap_user_suffix", ",ou=Users,dc=myorg,dc=com"); + recreateMiniCluster(extraTserverFlagMap); + session.execute("CREATE ROLE 'testUser1' WITH LOGIN = true"); + + checkConnectivityWithMessage(true, "testUser1", "12345", ProtocolOptions.Compression.NONE, + true /* expectFailure */, + "Failed to authenticate using LDAP: Provided username testUser1 and/or password are " + + "incorrect"); + + // Test with incorrect user suffix + extraTserverFlagMap.clear(); + extraTserverFlagMap.put("ycql_ldap_user_prefix", "cn="); + extraTserverFlagMap.put("ycql_ldap_user_suffix", ",dc=myorg,dc=com"); + recreateMiniCluster(extraTserverFlagMap); + session.execute("CREATE ROLE 'testUser1' WITH LOGIN = true"); + + checkConnectivityWithMessage(true, "testUser1", "12345", ProtocolOptions.Compression.NONE, + true /* expectFailure */, + "Failed to authenticate using LDAP: Provided username testUser1 and/or password are " + + "incorrect"); + + // Test with correct prefix and suffix + extraTserverFlagMap.clear(); + extraTserverFlagMap.put("ycql_ldap_user_prefix", "cn="); + extraTserverFlagMap.put("ycql_ldap_user_suffix", ",ou=Users,dc=myorg,dc=com"); + recreateMiniCluster(extraTserverFlagMap); + session.execute("CREATE ROLE 'testUser1' WITH LOGIN = true"); + + checkConnectivityWithMessage(true, "testUser1", "12345", ProtocolOptions.Compression.NONE, + false /* expectFailure */, ""); + + // Incorrect username/password + checkConnectivityWithMessage(true, "testUser1", "1234", ProtocolOptions.Compression.NONE, + true /* expectFailure */, + "Failed to authenticate using LDAP: Provided username testUser1 and/or password are " + + "incorrect"); + + checkConnectivityWithMessage(true, "testUser2", "12345", ProtocolOptions.Compression.NONE, + true /* expectFailure */, + "Provided username testUser2 and/or password are incorrect"); + + checkConnectivityWithMessage(true, "testUser1", "", ProtocolOptions.Compression.NONE, + true /* expectFailure */, + "Failed to authenticate using LDAP: Internal error"); + + session.execute("DROP ROLE 'testUser1'"); + } + + @Test + public void searchBindMode() throws Exception { + session.execute("CREATE ROLE 'testUser1' WITH LOGIN = true"); + + // Test with incorrect bind user dn + Map extraTserverFlagMap = getTServerFlags(); + extraTserverFlagMap.put("ycql_ldap_bind_dn", "cn=dummy,ou=Users,dc=myorg,dc=com"); + extraTserverFlagMap.put("ycql_ldap_bind_passwd", "adminPasswd"); + extraTserverFlagMap.put("ycql_ldap_base_dn", "ou=Users,dc=myorg,dc=com"); + extraTserverFlagMap.put("ycql_ldap_search_attribute", "cn"); + recreateMiniCluster(extraTserverFlagMap); + session.execute("CREATE ROLE 'testUser1' WITH LOGIN = true"); + + checkConnectivityWithMessage(true, "testUser1", "12345", ProtocolOptions.Compression.NONE, + true, /* expectFailure */ + "could not perform initial LDAP bind for ldapbinddn 'cn=dummy,ou=Users,dc=myorg,dc=com'"); + + // Test with incorrect bind password + extraTserverFlagMap.clear(); + extraTserverFlagMap.put("ycql_ldap_bind_dn", "cn=admin,ou=Users,dc=myorg,dc=com"); + extraTserverFlagMap.put("ycql_ldap_bind_passwd", "dummyPasswd"); + extraTserverFlagMap.put("ycql_ldap_base_dn", "ou=Users,dc=myorg,dc=com"); + extraTserverFlagMap.put("ycql_ldap_search_attribute", "cn"); + recreateMiniCluster(extraTserverFlagMap); + session.execute("CREATE ROLE 'testUser1' WITH LOGIN = true"); + + checkConnectivityWithMessage(true, "testUser1", "12345", ProtocolOptions.Compression.NONE, + true, /* expectFailure */ + "could not perform initial LDAP bind for ldapbinddn 'cn=admin,ou=Users,dc=myorg,dc=com'"); + + // Test with non-existant base dn + extraTserverFlagMap.clear(); + extraTserverFlagMap.put("ycql_ldap_bind_dn", "cn=admin,ou=Users,dc=myorg,dc=com"); + extraTserverFlagMap.put("ycql_ldap_bind_passwd", "adminPasswd"); + extraTserverFlagMap.put("ycql_ldap_base_dn", "ou=dummy,dc=myorg,dc=com"); + extraTserverFlagMap.put("ycql_ldap_search_attribute", "cn"); + recreateMiniCluster(extraTserverFlagMap); + session.execute("CREATE ROLE 'testUser1' WITH LOGIN = true"); + + checkConnectivityWithMessage(true, "testUser1", "12345", ProtocolOptions.Compression.NONE, + true, /* expectFailure */ + "could not search LDAP for filter '(cn=testUser1)' on server " + + "'ldap://localhost:10389': No such object LDAP diagnostics: " + + "NO_SUCH_OBJECT: failed for MessageType : SEARCH_REQUEST"); + + // Test with incorrect incorrect search attribute + extraTserverFlagMap.clear(); + extraTserverFlagMap.put("ycql_ldap_bind_dn", "cn=admin,ou=Users,dc=myorg,dc=com"); + extraTserverFlagMap.put("ycql_ldap_bind_passwd", "adminPasswd"); + extraTserverFlagMap.put("ycql_ldap_base_dn", "ou=Users,dc=myorg,dc=com"); + extraTserverFlagMap.put("ycql_ldap_search_attribute", "dummy"); + recreateMiniCluster(extraTserverFlagMap); + session.execute("CREATE ROLE 'testUser1' WITH LOGIN = true"); + + checkConnectivityWithMessage(true, "testUser1", "12345", ProtocolOptions.Compression.NONE, + true, /* expectFailure */ + "LDAP user 'testUser1' does not exist. LDAP search for filter '(dummy=testUser1)' on " + + "server 'ldap://localhost:10389' returned no entries."); + + // Test with all correct - bind db, bind password, base dn, search attribute + extraTserverFlagMap.clear(); + extraTserverFlagMap.put("ycql_ldap_bind_dn", "cn=admin,ou=Users,dc=myorg,dc=com"); + extraTserverFlagMap.put("ycql_ldap_bind_passwd", "adminPasswd"); + extraTserverFlagMap.put("ycql_ldap_base_dn", "ou=Users,dc=myorg,dc=com"); + extraTserverFlagMap.put("ycql_ldap_search_attribute", "cn"); + recreateMiniCluster(extraTserverFlagMap); + session.execute("CREATE ROLE 'testUser1' WITH LOGIN = true"); + + // Test with incorrect user + checkConnectivityWithMessage(true, "dummy", "12345", ProtocolOptions.Compression.NONE, + true, /* expectFailure */ + "Provided username dummy and/or password are incorrect"); + + // Test with incorrect user and password + checkConnectivityWithMessage(true, "testUser1", "1234", ProtocolOptions.Compression.NONE, + true, /* expectFailure */ + "Failed to authenticate using LDAP: Provided username testUser1 and/or " + + "password are incorrect"); + + // Test with correct password + checkConnectivity(true, "testUser1", "12345", false /* expectFailure */); + + // Test with prefix of base dn + extraTserverFlagMap.clear(); + extraTserverFlagMap.put("ycql_ldap_bind_dn", "cn=admin,ou=Users,dc=myorg,dc=com"); + extraTserverFlagMap.put("ycql_ldap_bind_passwd", "adminPasswd"); + extraTserverFlagMap.put("ycql_ldap_base_dn", "dc=myorg,dc=com"); + extraTserverFlagMap.put("ycql_ldap_search_attribute", "cn"); + recreateMiniCluster(extraTserverFlagMap); + session.execute("CREATE ROLE 'testUser1' WITH LOGIN = true"); + + // Test with incorrect user + checkConnectivityWithMessage(true, "dummy", "12345", ProtocolOptions.Compression.NONE, + true, /* expectFailure */ + "Provided username dummy and/or password are incorrect"); + + // Test with incorrect user password + checkConnectivityWithMessage(true, "testUser1", "1234", ProtocolOptions.Compression.NONE, + true, /* expectFailure */ + "Failed to authenticate using LDAP: Provided username testUser1 and/or " + + "password are incorrect"); + + // Test with correct password + checkConnectivity(true, "testUser1", "12345", false /* expectFailure */); + + session.execute("CREATE ROLE 'test*User1' WITH LOGIN = true"); + // Test with user name that has characters that are not allowed + checkConnectivityWithMessage(true, "test*User1", "12345", ProtocolOptions.Compression.NONE, + true, /* expectFailure */ + "invalid character in user name for LDAP authentication"); + + session.execute("CREATE ROLE 'testUserNonUnique' WITH LOGIN = true"); + // Test with more than one user name matching search criteria + checkConnectivityWithMessage(true, "testUserNonUnique", "12345", + ProtocolOptions.Compression.NONE, true, /* expectFailure */ + "LDAP user 'testUserNonUnique' is not unique, 2 entries exist."); + } +} diff --git a/src/yb/yql/cql/cqlserver/CMakeLists.txt b/src/yb/yql/cql/cqlserver/CMakeLists.txt index 134b131203ec..95354f5dc229 100644 --- a/src/yb/yql/cql/cqlserver/CMakeLists.txt +++ b/src/yb/yql/cql/cqlserver/CMakeLists.txt @@ -56,7 +56,9 @@ target_link_libraries(yb-cql yb_client cql_service_proto server_common - server_process) + server_process + ldap + lber) # Tests set(YB_TEST_LINK_LIBS yb-cql integration-tests ${YB_MIN_TEST_LIBS}) diff --git a/src/yb/yql/cql/cqlserver/cql_processor.cc b/src/yb/yql/cql/cqlserver/cql_processor.cc index f6b7b5a6455b..9ebdf341920d 100644 --- a/src/yb/yql/cql/cqlserver/cql_processor.cc +++ b/src/yb/yql/cql/cqlserver/cql_processor.cc @@ -15,6 +15,8 @@ #include "yb/yql/cql/cqlserver/cql_processor.h" +#include + #include "yb/common/ql_value.h" #include "yb/gutil/strings/escaping.h" @@ -85,6 +87,31 @@ DECLARE_bool(use_cassandra_authentication); DECLARE_bool(ycql_cache_login_info); DECLARE_int32(client_read_write_timeout_ms); +// LDAP specific flags +DEFINE_bool(ycql_use_ldap, false, "Use LDAP for user logins"); +DEFINE_string(ycql_ldap_users_to_skip_csv, "", "Users that are authenticated via the local password" + " check instead of LDAP (if ycql_use_ldap=true). This is a comma separated list"); +DEFINE_string(ycql_ldap_server, "", "LDAP server of the form ://:"); +DEFINE_bool(ycql_ldap_tls, false, "Connect to LDAP server using TLS encryption."); + +// LDAP flags for simple bind mode +DEFINE_string(ycql_ldap_user_prefix, "", "String used for prepending the user name when forming " + "the DN for binding to the LDAP server"); +DEFINE_string(ycql_ldap_user_suffix, "", "String used for appending the user name when forming the " + "DN for binding to the LDAP Server."); + +// Flags for LDAP search + bind mode +DEFINE_string(ycql_ldap_base_dn, "", "Specifies the base directory to begin the user name search"); +DEFINE_string(ycql_ldap_bind_dn, "", "Specifies the username to perform the initial search when " + "doing search + bind authentication"); +DEFINE_string(ycql_ldap_bind_passwd, "", "Password for username being used to perform the initial " + "search when doing search + bind authentication"); +DEFINE_string(ycql_ldap_search_attribute, "", "Attribute to match against the username in the " + "search when doing search + bind authentication. If no attribute is specified, the uid attribute " + "is used."); +DEFINE_string(ycql_ldap_search_filter, "", "The search filter to use when doing search + bind " + "authentication."); + namespace yb { namespace cqlserver { @@ -579,29 +606,279 @@ unique_ptr CQLProcessor::ProcessError(const Status& s, s.ToUserMessage()); } +namespace { + +struct LDAPMemoryDeleter { + void operator()(void* ptr) const { ldap_memfree(ptr); } +}; + +struct LDAPMessageDeleter { + void operator()(LDAPMessage* ptr) const { ldap_msgfree(ptr); } +}; + +struct LDAPDeleter { + void operator()(LDAP* ptr) const { ldap_unbind_ext(ptr, NULL, NULL); } +}; + +using LDAPHolder = unique_ptr; +using LDAPMessageHolder = unique_ptr; +template +using LDAPMemoryHolder = unique_ptr; + +class LDAPError { + public: + explicit LDAPError(int c) : code(c), ldap_(nullptr) {} + LDAPError(int c, const LDAPHolder& l) : code(c), ldap_(&l) {} + + LDAP* GetLDAP() const { return ldap_ ? ldap_->get() : nullptr; } + const int code; + + private: + const LDAPHolder* ldap_; +}; + +/* +* Add a detail error message text to the current error if one can be +* constructed from the LDAP 'diagnostic message'. +*/ +ostream& operator<<(ostream& str, const LDAPError& error) { + str << ldap_err2string(error.code); + auto ldap = error.GetLDAP(); + if (ldap) { + char *message = nullptr; + const auto rc = ldap_get_option(ldap, LDAP_OPT_DIAGNOSTIC_MESSAGE, &message); + if (rc == LDAP_SUCCESS && message != nullptr) { + LDAPMemoryHolder holder(message); + str << " LDAP diagnostics: " << message; + } + } + return str; +} + +Result InitializeLDAPConnection(const char *uris) { + LDAP* ldap_ptr = nullptr; + auto r = ldap_initialize(&ldap_ptr, uris); + if (r != LDAP_SUCCESS) { + return STATUS_FORMAT(InternalError, "could not initialize LDAP: $0", LDAPError(r)); + } + LDAPHolder ldap(ldap_ptr); + VLOG(4) << "Successfully initialized LDAP struct"; + + int ldapversion = LDAP_VERSION3; + if ((r = ldap_set_option(ldap_ptr, LDAP_OPT_PROTOCOL_VERSION, &ldapversion)) != LDAP_SUCCESS) { + return STATUS_FORMAT(InternalError, "could not set LDAP protocol version: $0", + LDAPError(r, ldap)); + } + VLOG(4) << "Successfully set protocol version option"; + + if (FLAGS_ycql_ldap_tls && ((r = ldap_start_tls_s(ldap_ptr, NULL, NULL)) != LDAP_SUCCESS)) { + return STATUS_FORMAT(InternalError, "could not start LDAP TLS session: $0", LDAPError(r, ldap)); + } + + return ldap; +} + +Result CheckLDAPAuth(const ql::AuthResponseRequest::AuthQueryParameters& params) { + VLOG(4) << "Attempting ldap_initialize() with " << FLAGS_ycql_ldap_server; + if (FLAGS_ycql_ldap_server.empty()) + return STATUS(InvalidArgument, "LDAP server not specified"); + + const auto& uris = FLAGS_ycql_ldap_server; + auto ldap = VERIFY_RESULT(InitializeLDAPConnection(uris.c_str())); + + int r; + std::string fulluser; + if (!FLAGS_ycql_ldap_base_dn.empty()) { + /* + * First perform an LDAP search to find the DN for the user we are + * trying to log in as. + */ + char ldap_no_attrs[sizeof(LDAP_NO_ATTRS)+1]; + strncpy(ldap_no_attrs, LDAP_NO_ATTRS, sizeof(ldap_no_attrs)); + char *attributes[] = {ldap_no_attrs, NULL}; + + /* + * Disallow any characters that we would otherwise need to escape, + * since they aren't really reasonable in a username anyway. Allowing + * them would make it possible to inject any kind of custom filters in + * the LDAP filter. + */ + for (const char& c : params.username) { + switch (c) { + case '*': + case '(': + case ')': + case '\\': + case '/': + return STATUS(InvalidArgument, "invalid character in user name for LDAP authentication"); + } + } + + /* + * Bind with a pre-defined username/password (if available) for + * searching. If none is specified, this turns into an anonymous bind. + */ + struct berval cred; + ber_str2bv(FLAGS_ycql_ldap_bind_passwd.c_str(), 0 /* len */, 0 /* duplicate */ , &cred); + r = ldap_sasl_bind_s(ldap.get(), FLAGS_ycql_ldap_bind_dn.c_str(), + LDAP_SASL_SIMPLE, &cred, + NULL /* serverctrls */, NULL /* clientctrls */, + NULL /* servercredp */); + if (r != LDAP_SUCCESS) { + return STATUS_FORMAT( + InvalidArgument, + "could not perform initial LDAP bind for ldapbinddn '$0' on server '$1': $2", + FLAGS_ycql_ldap_bind_dn, FLAGS_ycql_ldap_server, LDAPError(r, ldap)); + } + + std::string filter; + /* Build a custom filter or a single attribute filter? */ + // TODO(Piyush): Support the search filter mode + // if (FLAGS_ycql_ldap_search_filter) + // filter = FormatSearchFilter(FLAGS_ycql_ldap_search_filter, params.username); + if (!FLAGS_ycql_ldap_search_attribute.empty()) { + filter = "(" + FLAGS_ycql_ldap_search_attribute + "=" + params.username + ")"; + } else { + filter = "(uid=" + params.username + ")"; + } + + LDAPMessage *search_message; + r = ldap_search_ext_s(ldap.get(), FLAGS_ycql_ldap_base_dn.c_str(), LDAP_SCOPE_SUBTREE, + filter.c_str(), attributes, 0, NULL, NULL, NULL, 0, &search_message); + LDAPMessageHolder search_message_holder{search_message}; + + if (r != LDAP_SUCCESS) { + return STATUS_FORMAT( + InternalError, "could not search LDAP for filter '$0' on server '$1': $2", filter, + FLAGS_ycql_ldap_server, LDAPError(r, ldap)); + } + + auto count = ldap_count_entries(ldap.get(), search_message); + switch(count) { + case 0: + return STATUS_FORMAT( + NotFound, + "LDAP user '$0' does not exist. "\ + "LDAP search for filter '$1' on server '$2' returned no entries.", + params.username, filter, FLAGS_ycql_ldap_server); + case 1: + break; + default: + return STATUS_FORMAT( + NotFound, "LDAP user '$0' is not unique, $1 entries exist.", params.username, count); + } + + // No need to free entry pointer since it is a pointer to data in + // search_message. Freeing search_message takes cares of it. + auto *entry = ldap_first_entry(ldap.get(), search_message); + char *dn = ldap_get_dn(ldap.get(), entry); + if (dn == NULL) { + int error; + ldap_get_option(ldap.get(), LDAP_OPT_ERROR_NUMBER, &error); + return STATUS_FORMAT( + NotFound, "could not get dn for the first entry matching '$0' on server '$1': $2", + filter, FLAGS_ycql_ldap_server, LDAPError(error, ldap)); + } + LDAPMemoryHolder dn_holder{dn}; + fulluser = dn; + + /* + * Need to re-initialize the LDAP connection, so that we can bind to + * it with a different username. + */ + ldap = VERIFY_RESULT(InitializeLDAPConnection(uris.c_str())); + } else { + fulluser = FLAGS_ycql_ldap_user_prefix + params.username + FLAGS_ycql_ldap_user_suffix; + } + + VLOG(4) << "Checking authentication using LDAP for user DN=" << fulluser; + + struct berval cred; + ber_str2bv(params.password.c_str(), 0 /* len */, 0 /* duplicate */, &cred); + r = ldap_sasl_bind_s(ldap.get(), fulluser.c_str(), + LDAP_SASL_SIMPLE, &cred, + NULL /* serverctrls */, NULL /* clientctrls */, + NULL /* servercredp */); + VLOG(4) << "ldap_sasl_bind_s return value =" << r; + + if (r != LDAP_SUCCESS) { + std::ostringstream str; + str << "LDAP login failed for user '" << fulluser << "' on server '" + << FLAGS_ycql_ldap_server << "': " << LDAPError(r, ldap); + auto error_msg = str.str(); + if (r == LDAP_INVALID_CREDENTIALS) { + LOG(ERROR) << error_msg; + return false; + } + + return STATUS(InternalError, error_msg); + } + + return true; +} + +static bool UserIn(const std::string& username, const std::string& users_to_skip) { + size_t comma_index = 0; + size_t prev_comma_index = -1; + + // TODO(Piyush): Store a static list of usernames from csv instead of traversing each time. + while ((comma_index = users_to_skip.find(",", prev_comma_index + 1)) != std::string::npos) { + if (users_to_skip.substr(prev_comma_index + 1, + comma_index - (prev_comma_index + 1)) == username) + return true; + VLOG(2) << "Check " << username << " with " + << users_to_skip.substr(prev_comma_index + 1, comma_index - (prev_comma_index + 1)); + prev_comma_index = comma_index; + } + VLOG(2) << "Check " << username << " with " + << users_to_skip.substr( + prev_comma_index + 1, users_to_skip.size() - (prev_comma_index + 1)); + return users_to_skip.substr(prev_comma_index + 1, users_to_skip.size() - (prev_comma_index + 1)) + == username; +} + +} // namespace + unique_ptr CQLProcessor::ProcessAuthResult(const string& saved_hash, bool can_login) { const auto& req = down_cast(*request_); const auto& params = req.params(); unique_ptr response = nullptr; bool authenticated = false; - // Username doesn't have a password, but one is required for authentication. Return an error. - if (saved_hash.empty()) { - response = make_unique(*request_, - ErrorResponse::Code::BAD_CREDENTIALS, + + if (FLAGS_ycql_use_ldap && !UserIn(params.username, FLAGS_ycql_ldap_users_to_skip_csv)) { + Result ldap_auth_result = CheckLDAPAuth(req.params()); + if (!ldap_auth_result.ok()) { + return make_unique( + *request_, ErrorResponse::Code::SERVER_ERROR, + "Failed to authenticate using LDAP: " + yb::ToString(ldap_auth_result)); + } else if (!*ldap_auth_result) { + response = make_unique( + *request_, ErrorResponse::Code::BAD_CREDENTIALS, + "Failed to authenticate using LDAP: Provided username " + params.username + + " and/or password are incorrect"); + } else { + authenticated = true; + call_->ql_session()->set_current_role_name(params.username); + response = make_unique(*request_, + "" /* this does not matter */); + } + } else if (saved_hash.empty()) { + // Username doesn't have a password, but one is required for authentication. Return an error. + response = make_unique( + *request_, ErrorResponse::Code::BAD_CREDENTIALS, "Provided username " + params.username + " and/or password are incorrect"); } else { if (!service_impl_->CheckPassword(params.password, saved_hash)) { - response = make_unique(*request_, - ErrorResponse::Code::BAD_CREDENTIALS, + response = make_unique( + *request_, ErrorResponse::Code::BAD_CREDENTIALS, "Provided username " + params.username + " and/or password are incorrect"); } else if (!can_login) { - response = make_unique(*request_, - ErrorResponse::Code::BAD_CREDENTIALS, + response = make_unique( + *request_, ErrorResponse::Code::BAD_CREDENTIALS, params.username + " is not permitted to log in"); } else { call_->ql_session()->set_current_role_name(params.username); - response = make_unique(*request_, - "" /* this does not matter */); + response = make_unique(*request_, "" /* this does not matter */); authenticated = true; } } @@ -640,8 +917,8 @@ unique_ptr CQLProcessor::ProcessResult(const ExecutedResult::Shared const auto row_block = rows_result->GetRowBlock(); unique_ptr response = nullptr; if (row_block->row_count() != 1) { - response = make_unique(*request_, - ErrorResponse::Code::BAD_CREDENTIALS, + response = make_unique( + *request_, ErrorResponse::Code::BAD_CREDENTIALS, "Provided username " + params.username + " and/or password are incorrect"); } else { const auto& row = row_block->row(0);