Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Couchbase/Cluster.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
2 changes: 2 additions & 0 deletions Couchbase/ClusterInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
12 changes: 12 additions & 0 deletions Couchbase/ClusterOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
2 changes: 1 addition & 1 deletion src/deps/couchbase-cxx-client
29 changes: 29 additions & 0 deletions src/php_couchbase.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
74 changes: 74 additions & 0 deletions src/wrapper/connection_handle.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typename Request>
Expand Down
3 changes: 3 additions & 0 deletions src/wrapper/connection_handle.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions tests/Helpers/ServerVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions tests/QueryTest.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
<?php

use Couchbase\ClusterInterface;
use Couchbase\ClusterOptions;
use Couchbase\Exception\InternalServerFailureException;
use Couchbase\GetOptions;
use Couchbase\JsonTranscoder;
use Couchbase\MutationState;
use Couchbase\PasswordAuthenticator;
use Couchbase\QueryOptions;
use Couchbase\QueryScanConsistency;
use Couchbase\UpsertOptions;
Expand Down Expand Up @@ -275,4 +278,25 @@ public function testScope()
$this->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");
}
}
Loading