should be it

This commit is contained in:
2025-10-24 19:21:19 -05:00
parent a4b23fc57c
commit f09560c7b1
14047 changed files with 3161551 additions and 1 deletions

View File

@@ -0,0 +1,6 @@
add_library_unity(test_secrets OBJECT test_custom_secret_storage.cpp
test_persistent_secret_permissions.cpp)
set(ALL_OBJECT_FILES
${ALL_OBJECT_FILES} $<TARGET_OBJECTS:test_secrets>
PARENT_SCOPE)

View File

@@ -0,0 +1,345 @@
#include "catch.hpp"
#include "test_helpers.hpp"
#include "duckdb.hpp"
#include "duckdb/main/database.hpp"
#include "duckdb/main/secret/secret_manager.hpp"
#include "duckdb/main/secret/secret_storage.hpp"
#include "duckdb/main/secret/secret.hpp"
#include "duckdb/main/extension/extension_loader.hpp"
#include "duckdb/main/extension_manager.hpp"
using namespace duckdb;
using namespace std;
struct TestSecretLog {
duckdb::mutex lock;
duckdb::vector<string> remove_secret_requests;
duckdb::vector<string> write_secret_requests;
};
// Demo secret type
struct DemoSecretType {
static duckdb::unique_ptr<BaseSecret> CreateDemoSecret(ClientContext &context, CreateSecretInput &input) {
auto scope = input.scope;
if (scope.empty()) {
scope = {""};
}
auto return_value = make_uniq<KeyValueSecret>(scope, input.type, input.provider, input.name);
return std::move(return_value);
}
static void RegisterDemoSecret(DatabaseInstance &instance, const string &type_name) {
ExtensionInfo extension_info {};
ExtensionActiveLoad load_info {instance, extension_info, "demo_secret_type_" + type_name};
ExtensionLoader loader {load_info};
SecretType secret_type;
secret_type.name = type_name;
secret_type.deserializer = KeyValueSecret::Deserialize<KeyValueSecret>;
secret_type.default_provider = "config";
loader.RegisterSecretType(secret_type);
CreateSecretFunction secret_fun = {type_name, "config", CreateDemoSecret};
loader.RegisterFunction(secret_fun);
}
};
// Demo pluggable secret storage
class TestSecretStorage : public CatalogSetSecretStorage {
public:
TestSecretStorage(const string &name_p, DatabaseInstance &db, TestSecretLog &logger_p, int64_t tie_break_offset_p)
: CatalogSetSecretStorage(db, name_p, tie_break_offset_p), logger(logger_p) {
secrets = make_uniq<CatalogSet>(Catalog::GetSystemCatalog(db));
persistent = true;
include_in_lookups = true;
}
bool IncludeInLookups() override {
return include_in_lookups;
}
bool include_in_lookups;
protected:
void WriteSecret(const BaseSecret &secret, OnCreateConflict on_conflict) override {
duckdb::lock_guard<duckdb::mutex> lock(logger.lock);
logger.write_secret_requests.push_back(secret.GetName());
};
virtual void RemoveSecret(const string &secret, OnEntryNotFound on_entry_not_found) override {
duckdb::lock_guard<duckdb::mutex> lock(logger.lock);
logger.remove_secret_requests.push_back(secret);
};
TestSecretLog &logger;
};
TEST_CASE("Test secret lookups by secret type", "[secret][.]") {
DuckDB db(nullptr);
Connection con(db);
if (!db.ExtensionIsLoaded("httpfs")) {
return;
}
REQUIRE_NO_FAIL(con.Query("set allow_persistent_secrets=false;"));
// Register a demo secret type
DemoSecretType::RegisterDemoSecret(*db.instance, "secret_type_1");
DemoSecretType::RegisterDemoSecret(*db.instance, "secret_type_2");
// Create some secrets
REQUIRE_NO_FAIL(con.Query("CREATE SECRET s1 (TYPE secret_type_1, SCOPE '')"));
REQUIRE_NO_FAIL(con.Query("CREATE SECRET s2 (TYPE secret_type_2, SCOPE '')"));
// Note that the secrets collide completely, except for their types
auto res1 = con.Query("SELECT name FROM which_secret('blablabla', 'secret_type_1')");
auto res2 = con.Query("SELECT name FROM which_secret('blablabla', 'secret_type_2')");
// Correct secret is selected
REQUIRE(res1->GetValue(0, 0).ToString() == "s1");
REQUIRE(res2->GetValue(0, 0).ToString() == "s2");
}
TEST_CASE("Test adding a custom secret storage", "[secret][.]") {
DuckDB db(nullptr);
Connection con(db);
if (!db.ExtensionIsLoaded("httpfs")) {
return;
}
TestSecretLog log;
// Register custom secret storage
auto &secret_manager = duckdb::SecretManager::Get(*db.instance);
auto storage_ptr = duckdb::make_uniq<TestSecretStorage>("test_storage", *db.instance, log, 30);
auto &storage_ref = *storage_ptr;
secret_manager.LoadSecretStorage(std::move(storage_ptr));
REQUIRE_NO_FAIL(con.Query("set allow_persistent_secrets=true;"));
// Set custom secret path to prevent interference with other tests
auto secret_dir = TestCreatePath("custom_secret_storage_cpp_1");
REQUIRE_NO_FAIL(con.Query("set secret_directory='" + secret_dir + "'"));
REQUIRE_NO_FAIL(con.Query("CREATE SECRET s1 IN TEST_STORAGE (TYPE S3, SCOPE 's3://foo')"));
REQUIRE_NO_FAIL(con.Query("CREATE PERSISTENT SECRET s2 IN test_storage (TYPE S3, SCOPE 's3://')"));
REQUIRE_NO_FAIL(con.Query("CREATE TEMPORARY SECRET s2 (TYPE S3, SCOPE 's3://')"));
// We add this secret of the wrong type, but with better matching scope: these should be ignored on lookup
DemoSecretType::RegisterDemoSecret(*db.instance, "test");
REQUIRE_NO_FAIL(con.Query("CREATE SECRET s1_test_type IN TEST_STORAGE (TYPE test, SCOPE 's3://foo/bar.csv')"));
// Inspect current duckdb_secrets output
auto result = con.Query("SELECT name, storage from duckdb_secrets() ORDER BY type, name, storage");
REQUIRE(result->RowCount() == 4);
REQUIRE(result->GetValue(0, 0).ToString() == "s1");
REQUIRE(result->GetValue(1, 0).ToString() == "test_storage");
REQUIRE(result->GetValue(0, 1).ToString() == "s2");
REQUIRE(result->GetValue(1, 1).ToString() == "memory");
REQUIRE(result->GetValue(0, 2).ToString() == "s2");
REQUIRE(result->GetValue(1, 2).ToString() == "test_storage");
REQUIRE(result->GetValue(0, 3).ToString() == "s1_test_type");
REQUIRE(result->GetValue(1, 3).ToString() == "test_storage");
auto transaction = CatalogTransaction::GetSystemTransaction(*db.instance);
// Ambiguous call -> throws
REQUIRE_THROWS(secret_manager.GetSecretByName(transaction, "s2"));
// With specific storage -> works
auto secret_ptr = secret_manager.GetSecretByName(transaction, "s2", "test_storage");
REQUIRE(secret_ptr);
REQUIRE(secret_ptr->storage_mode == "test_storage");
REQUIRE(secret_ptr->secret->GetName() == "s2");
// Now try resolve secret by path -> this will return s1 because its scope matches best
auto which_secret_result = con.Query("SELECT name FROM which_secret('s3://foo/bar.csv', 'S3');");
REQUIRE(which_secret_result->GetValue(0, 0).ToString() == "s1");
// Exclude the storage from lookups
storage_ref.include_in_lookups = false;
// Now the lookup will choose the other storage
which_secret_result = con.Query("SELECT name FROM which_secret('s3://foo/bar.csv', 's3');");
REQUIRE(which_secret_result->GetValue(0, 0).ToString() == "s2");
// Lets drop stuff now
REQUIRE_NO_FAIL(con.Query("DROP TEMPORARY SECRET s2"));
REQUIRE_NO_FAIL(con.Query("DROP SECRET s2 FROM test_storage"));
REQUIRE_NO_FAIL(con.Query("DROP SECRET s1"));
// Inspect the log from our logger
REQUIRE(log.remove_secret_requests.size() == 2);
REQUIRE(log.write_secret_requests.size() == 3);
REQUIRE(log.write_secret_requests[0] == "s1");
REQUIRE(log.write_secret_requests[1] == "s2");
REQUIRE(log.write_secret_requests[2] == "s1_test_type");
REQUIRE(log.remove_secret_requests[0] == "s2");
REQUIRE(log.remove_secret_requests[1] == "s1");
}
TEST_CASE("Test tie-break behaviour for custom secret storage", "[secret][.]") {
DuckDB db(nullptr);
Connection con(db);
if (!db.ExtensionIsLoaded("httpfs")) {
return;
}
TestSecretLog log1;
TestSecretLog log2;
REQUIRE_NO_FAIL(con.Query("set allow_persistent_secrets=true;"));
// Set custom secret path to prevent interference with other tests
auto secret_dir = TestCreatePath("custom_secret_storage_cpp_2");
REQUIRE_NO_FAIL(con.Query("set secret_directory='" + secret_dir + "'"));
// Register custom secret storage
auto &secret_manager = duckdb::SecretManager::Get(*db.instance);
// Correct tie-break offset: 30 places it after temporary and persistent
auto storage_ptr = duckdb::make_uniq<TestSecretStorage>("test_storage_after", *db.instance, log1, 30);
secret_manager.LoadSecretStorage(std::move(storage_ptr));
// Correct tie-break offset: 0 places it before temporary and persistent
auto storage_ptr2 = duckdb::make_uniq<TestSecretStorage>("test_storage_before", *db.instance, log2, 0);
secret_manager.LoadSecretStorage(std::move(storage_ptr2));
// Now create 3 secrets with identical scope: the default s3 scope
REQUIRE_NO_FAIL(con.Query("CREATE TEMPORARY SECRET s1 (TYPE S3)"));
REQUIRE_NO_FAIL(con.Query("CREATE SECRET s2 IN test_storage_after (TYPE S3)"));
REQUIRE_NO_FAIL(con.Query("CREATE SECRET s3 IN test_storage_before (TYPE S3)"));
// Inspect current duckdb_secrets output
auto result = con.Query("SELECT name, storage from duckdb_secrets() ORDER BY name, storage");
REQUIRE(result->RowCount() == 3);
REQUIRE(result->GetValue(0, 0).ToString() == "s1");
REQUIRE(result->GetValue(1, 0).ToString() == "memory");
REQUIRE(result->GetValue(0, 1).ToString() == "s2");
REQUIRE(result->GetValue(1, 1).ToString() == "test_storage_after");
REQUIRE(result->GetValue(0, 2).ToString() == "s3");
REQUIRE(result->GetValue(1, 2).ToString() == "test_storage_before");
result = con.Query("SELECT name FROM which_secret('s3://', 's3');");
REQUIRE(result->GetValue(0, 0).ToString() == "s3");
REQUIRE_NO_FAIL(con.Query("DROP SECRET s3"));
result = con.Query("SELECT name FROM which_secret('s3://', 's3');");
REQUIRE(result->GetValue(0, 0).ToString() == "s1");
REQUIRE_NO_FAIL(con.Query("DROP SECRET s1"));
result = con.Query("SELECT name FROM which_secret('s3://', 's3');");
REQUIRE(result->GetValue(0, 0).ToString() == "s2");
REQUIRE_NO_FAIL(con.Query("DROP SECRET s2"));
// Inspect the log from our logger
REQUIRE(log1.remove_secret_requests.size() == 1);
REQUIRE(log1.write_secret_requests.size() == 1);
REQUIRE(log1.write_secret_requests[0] == "s2");
REQUIRE(log1.remove_secret_requests[0] == "s2");
REQUIRE(log2.remove_secret_requests.size() == 1);
REQUIRE(log2.write_secret_requests.size() == 1);
REQUIRE(log2.write_secret_requests[0] == "s3");
REQUIRE(log2.remove_secret_requests[0] == "s3");
}
TEST_CASE("Secret storage tie-break penalty collision: manager loaded after", "[secret][.]") {
DuckDB db(nullptr);
Connection con(db);
if (!db.ExtensionIsLoaded("httpfs")) {
return;
}
REQUIRE_NO_FAIL(con.Query("set allow_persistent_secrets=false;"));
// Register custom secret storage
auto &secret_manager = duckdb::SecretManager::Get(*db.instance);
// This collides with the temporary secret storage: it will throw, but only on first use of the secret manager
TestSecretLog log;
auto storage_ptr = duckdb::make_uniq<TestSecretStorage>("failing_storage", *db.instance, log, 10);
// This passes but is actually wrong already
secret_manager.LoadSecretStorage(std::move(storage_ptr));
// This will trigger InitializeSecrets and cause tie-break penalty collision
REQUIRE_FAIL(con.Query("FROM duckdb_secrets();"));
}
TEST_CASE("Secret storage tie-break penalty collision: manager loaded before", "[secret][.]") {
DuckDB db(nullptr);
Connection con(db);
if (!db.ExtensionIsLoaded("httpfs")) {
return;
}
REQUIRE_NO_FAIL(con.Query("set allow_persistent_secrets=false;"));
// Register custom secret storage
auto &secret_manager = duckdb::SecretManager::Get(*db.instance);
// This collides with the temporary secret storage: it will throw, but only on first use of the secret manager
TestSecretLog log;
auto storage_ptr = duckdb::make_uniq<TestSecretStorage>("failing_storage", *db.instance, log, 10);
// Ensure secret manager is fully initialized
REQUIRE_NO_FAIL(con.Query("FROM duckdb_secrets();"));
// This fails
REQUIRE_THROWS(secret_manager.LoadSecretStorage(std::move(storage_ptr)));
}
TEST_CASE("Secret storage name collision: manager loaded before", "[secret][.]") {
DuckDB db(nullptr);
Connection con(db);
if (!db.ExtensionIsLoaded("httpfs")) {
return;
}
REQUIRE_NO_FAIL(con.Query("set allow_persistent_secrets=false;"));
// Register custom secret storage
auto &secret_manager = duckdb::SecretManager::Get(*db.instance);
// This collides with the memory manager by name
TestSecretLog log;
auto storage_ptr = duckdb::make_uniq<TestSecretStorage>("memory", *db.instance, log, 50);
// Ensure secret manager is fully initialized
REQUIRE_NO_FAIL(con.Query("FROM duckdb_secrets();"));
// This fails
REQUIRE_THROWS(secret_manager.LoadSecretStorage(std::move(storage_ptr)));
}
TEST_CASE("Secret storage name collision: manager loaded after", "[secret][.]") {
DuckDB db(nullptr);
Connection con(db);
if (!db.ExtensionIsLoaded("httpfs")) {
return;
}
REQUIRE_NO_FAIL(con.Query("set allow_persistent_secrets=false;"));
// Register custom secret storage
auto &secret_manager = duckdb::SecretManager::Get(*db.instance);
// This collides with the memory manager by name
TestSecretLog log;
auto storage_ptr = duckdb::make_uniq<TestSecretStorage>("memory", *db.instance, log, 50);
// This passes but is actually wrong alsready
secret_manager.LoadSecretStorage(std::move(storage_ptr));
// This now fails with a name collision warning
REQUIRE_FAIL(con.Query("FROM duckdb_secrets();"));
}

View File

@@ -0,0 +1,105 @@
#include "catch.hpp"
#include "duckdb.hpp"
#include "duckdb/main/database.hpp"
#include "duckdb/main/extension/extension_loader.hpp"
#include "duckdb/main/secret/secret.hpp"
#include "duckdb/main/secret/secret_manager.hpp"
#include "duckdb/main/secret/secret_storage.hpp"
#include "test_helpers.hpp"
#include <sys/stat.h>
#ifndef _WIN32
#include <fcntl.h>
#include <sys/stat.h>
#endif
using namespace duckdb;
using namespace std;
#ifndef _WIN32
static void assert_correct_permission(string file) {
struct stat st;
auto res = lstat(file.c_str(), &st);
REQUIRE(res == 0);
// Only permissions should be User Read+Write
REQUIRE(st.st_mode & (S_IRUSR | S_IWUSR));
// The rest should be 0
REQUIRE(!(st.st_mode & (S_IXUSR | S_IRWXG | S_IRWXO)));
}
TEST_CASE("Test file permissions on linux/macos", "[secret][.]") {
DuckDB db(nullptr);
Connection con(db);
if (!db.ExtensionIsLoaded("httpfs")) {
return;
}
// Set custom secret path to prevent interference with other tests
REQUIRE_NO_FAIL(con.Query("set allow_persistent_secrets=true;"));
auto secret_dir = TestCreatePath("test_persistent_secret_permissions");
REQUIRE_NO_FAIL(con.Query("set secret_directory='" + secret_dir + "'"));
REQUIRE_NO_FAIL(con.Query("CREATE PERSISTENT SECRET oh_so_secret (TYPE S3)"));
assert_correct_permission(secret_dir + "/" + "oh_so_secret.duckdb_secret");
}
static void assert_duckdb_will_reject_persistent_secret() {
DuckDB db(nullptr);
Connection con(db);
// Set custom secret path to prevent interference with other tests
REQUIRE_NO_FAIL(con.Query("set allow_persistent_secrets=true;"));
auto secret_dir = TestCreatePath("test_persistent_secret_permissions");
REQUIRE_NO_FAIL(con.Query("set secret_directory='" + secret_dir + "'"));
auto res = con.Query("FROM duckdb_secrets()");
REQUIRE(res->HasError());
REQUIRE(StringUtil::Contains(res->GetError(),
"has incorrect permissions! Please set correct permissions or remove file"));
}
TEST_CASE("Test that DuckDB rejects secrets with incorrect permissions on linux/macos", "[secret][.]") {
DuckDB db(nullptr);
Connection con(db);
if (!db.ExtensionIsLoaded("httpfs")) {
return;
}
// Set custom secret path to prevent interference with other tests
REQUIRE_NO_FAIL(con.Query("set allow_persistent_secrets=true;"));
auto secret_dir = TestCreatePath("test_persistent_secret_permissions");
REQUIRE_NO_FAIL(con.Query("set secret_directory='" + secret_dir + "'"));
REQUIRE_NO_FAIL(con.Query("CREATE PERSISTENT SECRET also_very_secret (TYPE S3)"));
string secret_path = secret_dir + "/" + "also_very_secret.duckdb_secret";
mode_t incorrect_permissions[] {S_IRUSR | S_IWUSR | S_IRGRP, // user rw + group read
S_IRUSR | S_IWUSR | S_IWGRP, // user rw + group write
S_IRUSR | S_IWUSR | S_IXGRP, // user rw + group execute
S_IRUSR | S_IWUSR | S_IROTH, // user rw + other read
S_IRUSR | S_IWUSR | S_IWOTH, // user rw + other write
S_IRUSR | S_IWUSR | S_IXOTH}; // user rw + other execute
// Now confirm that for all possible incorrect permissions, we throw
for (auto perm : incorrect_permissions) {
chmod(secret_path.c_str(), perm);
assert_duckdb_will_reject_persistent_secret();
}
// Setting back to correct permission should allow us to read it again
chmod(secret_path.c_str(), S_IRUSR | S_IWUSR);
// Should be gud now
DuckDB db2(nullptr);
Connection con2(db2);
REQUIRE_NO_FAIL(con2.Query("set allow_persistent_secrets=true;"));
REQUIRE_NO_FAIL(con2.Query("set secret_directory='" + secret_dir + "'"));
REQUIRE_NO_FAIL(con2.Query("FROM duckdb_secrets()"));
}
#endif