diff --git a/Couchbase/Cluster.php b/Couchbase/Cluster.php index 8df1e377..f9f9f5dc 100644 --- a/Couchbase/Cluster.php +++ b/Couchbase/Cluster.php @@ -248,6 +248,23 @@ public function search(string $indexName, SearchRequest $request, ?SearchOptions return new SearchResult($result); } + /** + * Replaces the current authenticator used by this cluster. + * + * NOTE: Setting a new authenticator does not change the authentication status of existing connections. + * + * @param Authenticator $authenticator The authenticator to replace + * + * @return void + * @throws InvalidArgumentException if TLS is not enabled but provided authenticator requires TLS + * @since 4.5.0 + */ + public function setAuthenticator(Authenticator $authenticator): void + { + $function = COUCHBASE_EXTENSION_NAMESPACE . '\\authenticatorSet'; + $function($this->core, $authenticator->export()); + } + /** * Creates a new bucket manager object for managing buckets. * diff --git a/Couchbase/ClusterInterface.php b/Couchbase/ClusterInterface.php index f8ef48bf..24a351ac 100644 --- a/Couchbase/ClusterInterface.php +++ b/Couchbase/ClusterInterface.php @@ -29,4 +29,6 @@ public function query(string $statement, ?QueryOptions $options = null): QueryRe public function analyticsQuery(string $statement, ?AnalyticsOptions $options = null): AnalyticsResult; public function searchQuery(string $indexName, SearchQuery $query, ?SearchOptions $options = null): SearchResult; + + public function setAuthenticator(Authenticator $authenticator): void; } diff --git a/Couchbase/ClusterOptions.php b/Couchbase/ClusterOptions.php index 4cd462a7..29303420 100644 --- a/Couchbase/ClusterOptions.php +++ b/Couchbase/ClusterOptions.php @@ -240,6 +240,18 @@ public function maxHttpConnections(int $numberOfConnections): ClusterOptions return $this; } + /** + * @param int $milliseconds + * + * @return ClusterOptions + * @since 4.5.0 + */ + public function idleHttpConnectionTimeout(int $milliseconds): ClusterOptions + { + $this->idleHttpConnectionTimeoutMilliseconds = $milliseconds; + return $this; + } + /** * @param int $milliseconds * diff --git a/src/deps/couchbase-cxx-client b/src/deps/couchbase-cxx-client index 5bbe6769..a13bafb9 160000 --- a/src/deps/couchbase-cxx-client +++ b/src/deps/couchbase-cxx-client @@ -1 +1 @@ -Subproject commit 5bbe676932704bb723e7a1abeb5023d6d65f1845 +Subproject commit a13bafb98931c9a3aff2722eafbb5379267dedf0 diff --git a/src/php_couchbase.cxx b/src/php_couchbase.cxx index c7238762..acdac976 100644 --- a/src/php_couchbase.cxx +++ b/src/php_couchbase.cxx @@ -374,6 +374,29 @@ PHP_FUNCTION(closeBucket) } } +PHP_FUNCTION(authenticatorSet) +{ + zval* connection = nullptr; + zval* authenticator = nullptr; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_RESOURCE(connection) + Z_PARAM_ARRAY(authenticator) + ZEND_PARSE_PARAMETERS_END(); + + logger_flusher guard; + + auto* handle = fetch_couchbase_connection_from_resource(connection); + if (handle == nullptr) { + RETURN_THROWS(); + } + + if (auto e = handle->authenticator_set(authenticator); e.ec) { + couchbase_throw_exception(e); + RETURN_THROWS(); + } +} + PHP_FUNCTION(documentUpsert) { zval* connection = nullptr; @@ -3868,6 +3891,11 @@ ZEND_ARG_INFO(0, connection) ZEND_ARG_TYPE_INFO(0, bucketName, IS_STRING, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_INFO_EX(ai_CouchbaseExtension_authenticatorSet, 0, 0, 2) +ZEND_ARG_INFO(0, connection) +ZEND_ARG_TYPE_INFO(0, authenticator, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_INFO_EX(ai_CouchbaseExtension_documentUpsert, 0, 0, 7) ZEND_ARG_INFO(0, connection) ZEND_ARG_TYPE_INFO(0, bucket, IS_STRING, 0) @@ -4745,6 +4773,7 @@ static zend_function_entry couchbase_functions[] = { ZEND_NS_FE("Couchbase\\Extension" COUCHBASE_NAMESPACE_ABI_SUFFIX, createConnection, ai_CouchbaseExtension_createConnection) ZEND_NS_FE("Couchbase\\Extension" COUCHBASE_NAMESPACE_ABI_SUFFIX, openBucket, ai_CouchbaseExtension_openBucket) ZEND_NS_FE("Couchbase\\Extension" COUCHBASE_NAMESPACE_ABI_SUFFIX, closeBucket, ai_CouchbaseExtension_closeBucket) + ZEND_NS_FE("Couchbase\\Extension" COUCHBASE_NAMESPACE_ABI_SUFFIX, authenticatorSet, ai_CouchbaseExtension_authenticatorSet) ZEND_NS_FE("Couchbase\\Extension" COUCHBASE_NAMESPACE_ABI_SUFFIX, documentUpsert, ai_CouchbaseExtension_documentUpsert) ZEND_NS_FE("Couchbase\\Extension" COUCHBASE_NAMESPACE_ABI_SUFFIX, documentInsert, ai_CouchbaseExtension_documentInsert) ZEND_NS_FE("Couchbase\\Extension" COUCHBASE_NAMESPACE_ABI_SUFFIX, documentReplace, ai_CouchbaseExtension_documentReplace) diff --git a/src/wrapper/connection_handle.cxx b/src/wrapper/connection_handle.cxx index 9047e4b3..18ee1888 100644 --- a/src/wrapper/connection_handle.cxx +++ b/src/wrapper/connection_handle.cxx @@ -727,6 +727,80 @@ connection_handle::bucket_close(const zend_string* name) -> core_error_info return impl_->bucket_close(cb_string_new(name)); } +COUCHBASE_API +auto +connection_handle::authenticator_set(const zval* auth) -> core_error_info +{ + if (auth == nullptr || Z_TYPE_P(auth) != IS_ARRAY) { + return { errc::common::invalid_argument, ERROR_LOCATION, "expected array for authenticator" }; + } + + const zval* auth_type = zend_symtable_str_find(Z_ARRVAL_P(auth), ZEND_STRL("type")); + if (auth_type == nullptr || Z_TYPE_P(auth_type) != IS_STRING) { + return { errc::common::invalid_argument, + ERROR_LOCATION, + "unexpected type of the authenticator" }; + } + if (zend_binary_strcmp(Z_STRVAL_P(auth_type), Z_STRLEN_P(auth_type), ZEND_STRL("password")) == + 0) { + const zval* username = zend_symtable_str_find(Z_ARRVAL_P(auth), ZEND_STRL("username")); + if (username == nullptr || Z_TYPE_P(username) != IS_STRING) { + return { errc::common::invalid_argument, + ERROR_LOCATION, + "expected username to be a string in the authenticator" }; + } + const zval* password = zend_symtable_str_find(Z_ARRVAL_P(auth), ZEND_STRL("password")); + if (password == nullptr || Z_TYPE_P(password) != IS_STRING) { + return { errc::common::invalid_argument, + ERROR_LOCATION, + "expected password to be a string in the authenticator" }; + } + couchbase::password_authenticator password_auth{ + Z_STRVAL_P(username), + Z_STRVAL_P(password), + }; + + auto ctx = impl_->public_api().set_authenticator(password_auth); + + if (ctx.ec()) { + return { ctx.ec(), ERROR_LOCATION, "unable to set authenticator", build_error_context(ctx) }; + } + return {}; + } + + if (zend_binary_strcmp(Z_STRVAL_P(auth_type), Z_STRLEN_P(auth_type), ZEND_STRL("certificate")) == + 0) { + const zval* certificate_path = + zend_symtable_str_find(Z_ARRVAL_P(auth), ZEND_STRL("certificatePath")); + if (certificate_path == nullptr || Z_TYPE_P(certificate_path) != IS_STRING) { + return { errc::common::invalid_argument, + ERROR_LOCATION, + "expected certificate path to be a string in the authenticator" }; + } + const zval* key_path = zend_symtable_str_find(Z_ARRVAL_P(auth), ZEND_STRL("keyPath")); + if (key_path == nullptr || Z_TYPE_P(key_path) != IS_STRING) { + return { errc::common::invalid_argument, + ERROR_LOCATION, + "expected key path to be a string in the authenticator" }; + } + + couchbase::certificate_authenticator certificate_auth{ + Z_STRVAL_P(certificate_path), + Z_STRVAL_P(key_path), + }; + + auto ctx = impl_->public_api().set_authenticator(certificate_auth); + if (ctx.ec()) { + return { ctx.ec(), ERROR_LOCATION, "unable to set authenticator", build_error_context(ctx) }; + } + return {}; + } + return { errc::common::invalid_argument, + ERROR_LOCATION, + fmt::format("unknown type of the authenticator: {}", + std::string(Z_STRVAL_P(auth_type), Z_STRLEN_P(auth_type))) }; +} + namespace { template diff --git a/src/wrapper/connection_handle.hxx b/src/wrapper/connection_handle.hxx index 9a76f442..61d69d90 100644 --- a/src/wrapper/connection_handle.hxx +++ b/src/wrapper/connection_handle.hxx @@ -109,6 +109,9 @@ public: COUCHBASE_API auto bucket_close(const zend_string* name) -> core_error_info; + COUCHBASE_API + auto authenticator_set(const zval* authenticator) -> core_error_info; + COUCHBASE_API auto document_upsert(zval* return_value, const zend_string* bucket, diff --git a/tests/Helpers/ServerVersion.php b/tests/Helpers/ServerVersion.php index aa8c3511..1ae13c35 100644 --- a/tests/Helpers/ServerVersion.php +++ b/tests/Helpers/ServerVersion.php @@ -300,6 +300,12 @@ public function supportsVectorSearch(): bool return ($this->major == 7 && $this->minor >= 6) || $this->major > 7; } + // MB-39484: The query service authenticates all requests, not just those that require RBAC + public function supportsQueryMB39484(): bool + { + return ($this->major == 7 && $this->minor >= 6) || $this->major > 7; + } + public function supportsServerGroupReplicaReads(): bool { // 7.6.2 or above diff --git a/tests/QueryTest.php b/tests/QueryTest.php index 87c47398..0eaec24d 100644 --- a/tests/QueryTest.php +++ b/tests/QueryTest.php @@ -1,9 +1,12 @@ assertNotEmpty($rows); $this->assertEquals(42, $res->rows()[0][$collection->name()]['bar']); } + + public function testQueryAfterChangingCredentials() + { + $this->skipIfCaves(); + $this->skipIfUnsupported($this->version()->supportsQueryMB39484()); + + $options = new ClusterOptions(); + $options->idleHttpConnectionTimeout(0); + $cluster = $this->connectClusterUnique($options); + + $res = $cluster->query("SELECT 1=1"); + $rows = $res->rows(); + $this->assertNotEmpty($rows); + + $wrongPasswordAuth = new PasswordAuthenticator("wrong-username", "wrong-password"); + + $cluster->setAuthenticator($wrongPasswordAuth); + + $this->expectException(InternalServerFailureException::class); + $cluster->query("SELECT 1=1"); + } }