Skip to content

Commit 471083c

Browse files
committed
PCBC-1032: Support mTLS Cert Refresh (without restart)
1 parent d3f05d8 commit 471083c

File tree

8 files changed

+156
-1
lines changed

8 files changed

+156
-1
lines changed

Couchbase/Cluster.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,23 @@ public function search(string $indexName, SearchRequest $request, ?SearchOptions
248248
return new SearchResult($result);
249249
}
250250

251+
/**
252+
* Replaces the current authenticator used by this cluster.
253+
*
254+
* NOTE: Setting a new authenticator does not change the authentication status of existing connections.
255+
*
256+
* @param Authenticator $authenticator The authenticator to replace
257+
*
258+
* @return void
259+
* @throws InvalidArgumentException if TLS is not enabled but provided authenticator requires TLS
260+
* @since 4.5.0
261+
*/
262+
public function setAuthenticator(Authenticator $authenticator): void
263+
{
264+
$function = COUCHBASE_EXTENSION_NAMESPACE . '\\authenticatorSet';
265+
$function($this->core, $authenticator->export());
266+
}
267+
251268
/**
252269
* Creates a new bucket manager object for managing buckets.
253270
*

Couchbase/ClusterInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,6 @@ public function query(string $statement, ?QueryOptions $options = null): QueryRe
2929
public function analyticsQuery(string $statement, ?AnalyticsOptions $options = null): AnalyticsResult;
3030

3131
public function searchQuery(string $indexName, SearchQuery $query, ?SearchOptions $options = null): SearchResult;
32+
33+
public function setAuthenticator(Authenticator $authenticator): void;
3234
}

src/deps/couchbase-cxx-client

src/php_couchbase.cxx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,29 @@ PHP_FUNCTION(closeBucket)
374374
}
375375
}
376376

377+
PHP_FUNCTION(authenticatorSet)
378+
{
379+
zval* connection = nullptr;
380+
zval* authenticator = nullptr;
381+
382+
ZEND_PARSE_PARAMETERS_START(2, 2)
383+
Z_PARAM_RESOURCE(connection)
384+
Z_PARAM_ARRAY(authenticator)
385+
ZEND_PARSE_PARAMETERS_END();
386+
387+
logger_flusher guard;
388+
389+
auto* handle = fetch_couchbase_connection_from_resource(connection);
390+
if (handle == nullptr) {
391+
RETURN_THROWS();
392+
}
393+
394+
if (auto e = handle->authenticator_set(authenticator); e.ec) {
395+
couchbase_throw_exception(e);
396+
RETURN_THROWS();
397+
}
398+
}
399+
377400
PHP_FUNCTION(documentUpsert)
378401
{
379402
zval* connection = nullptr;
@@ -3868,6 +3891,11 @@ ZEND_ARG_INFO(0, connection)
38683891
ZEND_ARG_TYPE_INFO(0, bucketName, IS_STRING, 0)
38693892
ZEND_END_ARG_INFO()
38703893

3894+
ZEND_BEGIN_ARG_INFO_EX(ai_CouchbaseExtension_authenticatorSet, 0, 0, 2)
3895+
ZEND_ARG_INFO(0, connection)
3896+
ZEND_ARG_TYPE_INFO(0, authenticator, IS_ARRAY, 0)
3897+
ZEND_END_ARG_INFO()
3898+
38713899
ZEND_BEGIN_ARG_INFO_EX(ai_CouchbaseExtension_documentUpsert, 0, 0, 7)
38723900
ZEND_ARG_INFO(0, connection)
38733901
ZEND_ARG_TYPE_INFO(0, bucket, IS_STRING, 0)
@@ -4745,6 +4773,7 @@ static zend_function_entry couchbase_functions[] = {
47454773
ZEND_NS_FE("Couchbase\\Extension" COUCHBASE_NAMESPACE_ABI_SUFFIX, createConnection, ai_CouchbaseExtension_createConnection)
47464774
ZEND_NS_FE("Couchbase\\Extension" COUCHBASE_NAMESPACE_ABI_SUFFIX, openBucket, ai_CouchbaseExtension_openBucket)
47474775
ZEND_NS_FE("Couchbase\\Extension" COUCHBASE_NAMESPACE_ABI_SUFFIX, closeBucket, ai_CouchbaseExtension_closeBucket)
4776+
ZEND_NS_FE("Couchbase\\Extension" COUCHBASE_NAMESPACE_ABI_SUFFIX, authenticatorSet, ai_CouchbaseExtension_authenticatorSet)
47484777
ZEND_NS_FE("Couchbase\\Extension" COUCHBASE_NAMESPACE_ABI_SUFFIX, documentUpsert, ai_CouchbaseExtension_documentUpsert)
47494778
ZEND_NS_FE("Couchbase\\Extension" COUCHBASE_NAMESPACE_ABI_SUFFIX, documentInsert, ai_CouchbaseExtension_documentInsert)
47504779
ZEND_NS_FE("Couchbase\\Extension" COUCHBASE_NAMESPACE_ABI_SUFFIX, documentReplace, ai_CouchbaseExtension_documentReplace)

src/wrapper/connection_handle.cxx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,80 @@ connection_handle::bucket_close(const zend_string* name) -> core_error_info
727727
return impl_->bucket_close(cb_string_new(name));
728728
}
729729

730+
COUCHBASE_API
731+
auto
732+
connection_handle::authenticator_set(const zval* auth) -> core_error_info
733+
{
734+
if (auth == nullptr || Z_TYPE_P(auth) != IS_ARRAY) {
735+
return { errc::common::invalid_argument, ERROR_LOCATION, "expected array for authenticator" };
736+
}
737+
738+
const zval* auth_type = zend_symtable_str_find(Z_ARRVAL_P(auth), ZEND_STRL("type"));
739+
if (auth_type == nullptr || Z_TYPE_P(auth_type) != IS_STRING) {
740+
return { errc::common::invalid_argument,
741+
ERROR_LOCATION,
742+
"unexpected type of the authenticator" };
743+
}
744+
if (zend_binary_strcmp(Z_STRVAL_P(auth_type), Z_STRLEN_P(auth_type), ZEND_STRL("password")) ==
745+
0) {
746+
const zval* username = zend_symtable_str_find(Z_ARRVAL_P(auth), ZEND_STRL("username"));
747+
if (username == nullptr || Z_TYPE_P(username) != IS_STRING) {
748+
return { errc::common::invalid_argument,
749+
ERROR_LOCATION,
750+
"expected username to be a string in the authenticator" };
751+
}
752+
const zval* password = zend_symtable_str_find(Z_ARRVAL_P(auth), ZEND_STRL("password"));
753+
if (password == nullptr || Z_TYPE_P(password) != IS_STRING) {
754+
return { errc::common::invalid_argument,
755+
ERROR_LOCATION,
756+
"expected password to be a string in the authenticator" };
757+
}
758+
couchbase::password_authenticator password_auth{
759+
Z_STRVAL_P(username),
760+
Z_STRVAL_P(password),
761+
};
762+
763+
auto ctx = impl_->public_api().set_authenticator(password_auth);
764+
765+
if (ctx.ec()) {
766+
return { ctx.ec(), ERROR_LOCATION, "unable to set authenticator", build_error_context(ctx) };
767+
}
768+
return {};
769+
}
770+
771+
if (zend_binary_strcmp(Z_STRVAL_P(auth_type), Z_STRLEN_P(auth_type), ZEND_STRL("certificate")) ==
772+
0) {
773+
const zval* certificate_path =
774+
zend_symtable_str_find(Z_ARRVAL_P(auth), ZEND_STRL("certificatePath"));
775+
if (certificate_path == nullptr || Z_TYPE_P(certificate_path) != IS_STRING) {
776+
return { errc::common::invalid_argument,
777+
ERROR_LOCATION,
778+
"expected certificate path to be a string in the authenticator" };
779+
}
780+
const zval* key_path = zend_symtable_str_find(Z_ARRVAL_P(auth), ZEND_STRL("keyPath"));
781+
if (key_path == nullptr || Z_TYPE_P(key_path) != IS_STRING) {
782+
return { errc::common::invalid_argument,
783+
ERROR_LOCATION,
784+
"expected key path to be a string in the authenticator" };
785+
}
786+
787+
couchbase::certificate_authenticator certificate_auth{
788+
Z_STRVAL_P(certificate_path),
789+
Z_STRVAL_P(key_path),
790+
};
791+
792+
auto ctx = impl_->public_api().set_authenticator(certificate_auth);
793+
if (ctx.ec()) {
794+
return { ctx.ec(), ERROR_LOCATION, "unable to set authenticator", build_error_context(ctx) };
795+
}
796+
return {};
797+
}
798+
return { errc::common::invalid_argument,
799+
ERROR_LOCATION,
800+
fmt::format("unknown type of the authenticator: {}",
801+
std::string(Z_STRVAL_P(auth_type), Z_STRLEN_P(auth_type))) };
802+
}
803+
730804
namespace
731805
{
732806
template<typename Request>

src/wrapper/connection_handle.hxx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ public:
109109
COUCHBASE_API
110110
auto bucket_close(const zend_string* name) -> core_error_info;
111111

112+
COUCHBASE_API
113+
auto authenticator_set(const zval* authenticator) -> core_error_info;
114+
112115
COUCHBASE_API
113116
auto document_upsert(zval* return_value,
114117
const zend_string* bucket,

tests/Helpers/ServerVersion.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,12 @@ public function supportsVectorSearch(): bool
300300
return ($this->major == 7 && $this->minor >= 6) || $this->major > 7;
301301
}
302302

303+
// MB-39484: The query service authenticates all requests, not just those that require RBAC
304+
public function supportsQueryMB39484(): bool
305+
{
306+
return ($this->major == 7 && $this->minor >= 6) || $this->major > 7;
307+
}
308+
303309
public function supportsServerGroupReplicaReads(): bool
304310
{
305311
// 7.6.2 or above

tests/QueryTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
<?php
22

33
use Couchbase\ClusterInterface;
4+
use Couchbase\ClusterOptions;
5+
use Couchbase\Exception\InternalServerFailureException;
46
use Couchbase\GetOptions;
57
use Couchbase\JsonTranscoder;
68
use Couchbase\MutationState;
9+
use Couchbase\PasswordAuthenticator;
710
use Couchbase\QueryOptions;
811
use Couchbase\QueryScanConsistency;
912
use Couchbase\UpsertOptions;
@@ -275,4 +278,25 @@ public function testScope()
275278
$this->assertNotEmpty($rows);
276279
$this->assertEquals(42, $res->rows()[0][$collection->name()]['bar']);
277280
}
281+
282+
public function testQueryAfterChangingCredentials()
283+
{
284+
$this->skipIfCaves();
285+
$this->skipIfUnsupported($this->version()->supportsQueryMB39484());
286+
287+
$options = new ClusterOptions();
288+
$options->idleHttpConnectionTimeout(0);
289+
$cluster = $this->connectClusterUnique($options);
290+
291+
$res = $cluster->query("SELECT 1=1");
292+
$rows = $res->rows();
293+
$this->assertNotEmpty($rows);
294+
295+
$wrongPasswordAuth = new PasswordAuthenticator("wrong-username", "wrong-password");
296+
297+
$cluster->setAuthenticator($wrongPasswordAuth);
298+
299+
$this->expectException(InternalServerFailureException::class);
300+
$cluster->query("SELECT 1=1");
301+
}
278302
}

0 commit comments

Comments
 (0)