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
5 changes: 4 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.14)
project(databricks_sdk
VERSION 0.3.0
VERSION 0.3.1
DESCRIPTION "Databricks C++ SDK"
LANGUAGES CXX)

Expand Down Expand Up @@ -126,6 +126,7 @@ set(SOURCES
src/connection_pool.cpp
src/unity_catalog/unity_catalog_types.cpp
src/unity_catalog/unity_catalog.cpp
src/secrets/secrets.cpp
src/internal/pool_manager.cpp
src/internal/logger.cpp
src/internal/http_client.cpp
Expand All @@ -142,6 +143,8 @@ set(HEADERS
include/databricks/compute/compute_types.h
include/databricks/unity_catalog/unity_catalog.h
include/databricks/unity_catalog/unity_catalog_types.h
include/databricks/secrets/secrets.h
include/databricks/secrets/secrets_types.h
)

# Internal headers (not installed)
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ test: build-tests
.PHONY: test-new
test-new: build-tests
@echo "Running tests for modified files..."
@MODIFIED_FILES=$$(git status --short | grep -E '^\s*M.*\.(cpp|h)' | awk '{print $$2}' | grep -E 'test|jobs|compute|unity' || true); \
@MODIFIED_FILES=$$(git status --short | grep -E '^\s*M.*\.(cpp|h)' | awk '{print $$2}' | grep -E 'test|jobs|compute|unity|secrets' || true); \
if [ -z "$$MODIFIED_FILES" ]; then \
echo "No modified test files detected. Running all tests..."; \
cd $(BUILD_DIR)/tests && ./unit_tests; \
Expand All @@ -75,6 +75,9 @@ test-new: build-tests
elif echo "$$MODIFIED_FILES" | grep -q "unity"; then \
echo "Running Unity Catalog tests..."; \
cd $(BUILD_DIR)/tests && ./unit_tests --gtest_filter='*UnityCatalog*'; \
elif echo "$$MODIFIED_FILES" | grep -q "secrets"; then \
echo "Running Secrets tests..."; \
cd $(BUILD_DIR)/tests && ./unit_tests --gtest_filter='*Secret*'; \
else \
echo "Running all tests for safety..."; \
cd $(BUILD_DIR)/tests && ./unit_tests; \
Expand Down
5 changes: 5 additions & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@ target_link_libraries(compute_example PRIVATE databricks_sdk)
add_executable(unity_catalog_example unity_catalog_example.cpp)
target_link_libraries(unity_catalog_example PRIVATE databricks_sdk)

# ========== Secrets API Examples ==========
add_executable(secrets_example secrets_example.cpp)
target_link_libraries(secrets_example PRIVATE databricks_sdk)

# Set RPATH for all examples to find ODBC libraries
set_target_properties(
simple_query
jobs_example
compute_example
unity_catalog_example
secrets_example
PROPERTIES
BUILD_RPATH "${CMAKE_BINARY_DIR};/opt/homebrew/lib;/usr/local/lib"
INSTALL_RPATH "/opt/homebrew/lib;/usr/local/lib"
Expand Down
125 changes: 125 additions & 0 deletions examples/secrets_example.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright (c) 2025 Calvin Min
// SPDX-License-Identifier: MIT
/**
* @file secrets_example.cpp
* @brief Example demonstrating the Databricks Secrets API
*
* This example shows how to:
* 1. List all secret scopes in your workspace
* 2. Create a new secret scope
* 3. Store a secret in the scope
* 4. List secrets in the scope
* 5. Delete the secret and scope
*/

#include "databricks/core/config.h"
#include "databricks/secrets/secrets.h"

#include <exception>
#include <iostream>

int main() {
try {
// Load configuration from environment
databricks::AuthConfig auth = databricks::AuthConfig::from_environment();

std::cout << "Connecting to: " << auth.host << std::endl;
std::cout << "======================================\n" << std::endl;

// Create Secrets API client
databricks::Secrets secrets(auth);

// ===================================================================
// Example 1: List all secret scopes
// ===================================================================
std::cout << "1. Listing all secret scopes:" << std::endl;
std::cout << "-----------------------------" << std::endl;

auto scopes = secrets.list_scopes();
std::cout << "Found " << scopes.size() << " secret scopes:\n" << std::endl;

for (const auto& scope : scopes) {
std::cout << " Scope Name: " << scope.name << std::endl;
std::cout << " Backend Type: ";
if (scope.backend_type == databricks::SecretScopeBackendType::DATABRICKS) {
std::cout << "DATABRICKS" << std::endl;
} else if (scope.backend_type == databricks::SecretScopeBackendType::AZURE_KEYVAULT) {
std::cout << "AZURE_KEYVAULT" << std::endl;
if (!scope.resource_id.empty()) {
std::cout << " Resource ID: " << scope.resource_id << std::endl;
}
if (!scope.dns_name.empty()) {
std::cout << " DNS Name: " << scope.dns_name << std::endl;
}
} else {
std::cout << "UNKNOWN" << std::endl;
}
std::cout << std::endl;
}

// ===================================================================
// Example 2: Create a new secret scope
// ===================================================================
std::cout << "\n2. Creating a new secret scope:" << std::endl;
std::cout << "--------------------------------" << std::endl;

const std::string example_scope = "example_scope";
std::cout << "Creating scope: " << example_scope << std::endl;

// Create scope with "users" as initial_manage_principal
// This grants MANAGE permission to all workspace users
secrets.create_scope(example_scope, "users", databricks::SecretScopeBackendType::DATABRICKS);

std::cout << "Scope created successfully!\n" << std::endl;

// ===================================================================
// Example 3: Store a secret
// ===================================================================
std::cout << "\n3. Storing a secret:" << std::endl;
std::cout << "--------------------" << std::endl;

const std::string secret_key = "api_key";
const std::string secret_value = "my_secret_value_123";

std::cout << "Storing secret with key: " << secret_key << std::endl;
secrets.put_secret(example_scope, secret_key, secret_value);
std::cout << "Secret stored successfully!\n" << std::endl;

// ===================================================================
// Example 4: List secrets in the scope
// ===================================================================
std::cout << "\n4. Listing secrets in scope '" << example_scope << "':" << std::endl;
std::cout << "-----------------------------------------------" << std::endl;

auto secret_list = secrets.list_secrets(example_scope);
std::cout << "Found " << secret_list.size() << " secrets:\n" << std::endl;

for (const auto& secret : secret_list) {
std::cout << " Key: " << secret.key << std::endl;
std::cout << " Last Updated: " << secret.last_updated_timestamp << std::endl;
std::cout << std::endl;
}

// ===================================================================
// Example 5: Cleanup - Delete secret and scope
// ===================================================================
std::cout << "\n5. Cleaning up (deleting secret and scope):" << std::endl;
std::cout << "-------------------------------------------" << std::endl;

std::cout << "Deleting secret: " << secret_key << std::endl;
secrets.delete_secret(example_scope, secret_key);
std::cout << "Secret deleted successfully!" << std::endl;

std::cout << "Deleting scope: " << example_scope << std::endl;
secrets.delete_scope(example_scope);
std::cout << "Scope deleted successfully!\n" << std::endl;

std::cout << "\n======================================" << std::endl;
std::cout << "Secrets API example completed successfully!" << std::endl;
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}

return 0;
}
161 changes: 161 additions & 0 deletions include/databricks/secrets/secrets.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright (c) 2025 Calvin Min
// SPDX-License-Identifier: MIT
#pragma once

#include "databricks/core/config.h"
#include "databricks/secrets/secrets_types.h"

#include <optional>
#include <vector>

namespace databricks {
namespace internal {
class IHttpClient;
}

/**
* @brief Client for interacting with the Databricks Secrets API
*
* The Secrets API allows you to securely store and manage credentials, tokens, and other
* sensitive information in your Databricks workspace. Rather than entering credentials
* directly into notebooks, you can store them as secrets and reference them securely.
* This implementation uses Secrets API 2.0.
*
* Example usage:
* @code
* databricks::AuthConfig auth = databricks::AuthConfig::from_environment();
* databricks::Secrets secrets(auth);
*
* // List all secret scopes
* auto scopes = secrets.list_scopes();
*
* // Create a new secret scope
* secrets.create_scope("my_scope", databricks::SecretScopeBackendType::DATABRICKS);
*
* // Store a secret
* secrets.put_secret("my_scope", "api_key", "my_secret_value");
*
* // List secrets in a scope
* auto secret_list = secrets.list_secrets("my_scope");
*
* // Delete a secret
* secrets.delete_secret("my_scope", "api_key");
* @endcode
*/
class Secrets {
public:
/**
* @brief Construct a Secrets API client
* @param auth Authentication configuration with host and token
* @param api_version Secrets API version to use (default: "2.0")
*/
explicit Secrets(const AuthConfig& auth, const std::string& api_version = "2.0");

/**
* @brief Construct a Secrets API client with dependency injection (for testing)
* @param http_client Injected HTTP client (use MockHttpClient for unit tests)
* @note This constructor is primarily for testing with mock HTTP clients
*/
explicit Secrets(std::shared_ptr<internal::IHttpClient> http_client);

/**
* @brief Destructor
*/
~Secrets();

Secrets(const Secrets&) = delete;
Secrets& operator=(const Secrets&) = delete;

// Scope operations
/**
* @brief List all secret scopes in the workspace
*
* @return Vector of SecretScope objects
* @throws std::runtime_error if the API request fails
*/
std::vector<SecretScope> list_scopes();

/**
* @brief Create a new secret scope
*
* @param scope The name of the secret scope to create
* @param initial_manage_principal The principal (user or group) that will be granted MANAGE
* permission on the new scope. Common values:
* - "users": grants MANAGE access to all workspace users (default)
* - "admins": grants MANAGE access to workspace admins only
* - A specific user or group name
* @param backend_type The type of backend (DATABRICKS or AZURE_KEYVAULT)
* @param azure_resource_id Azure Key Vault resource ID (required for AZURE_KEYVAULT backend)
* @param azure_tenant_id Azure tenant ID (required for AZURE_KEYVAULT backend)
* @param dns_name Azure Key Vault DNS name (required for AZURE_KEYVAULT backend)
* @throws std::runtime_error if the scope already exists or the API request fails
* @throws std::invalid_argument if Azure parameters are missing for AZURE_KEYVAULT backend
*
* @note Databricks-backed scopes are stored in the control plane. Azure Key Vault-backed
* scopes are stored in your Azure Key Vault instance.
*/
void create_scope(const std::string& scope, const std::string& initial_manage_principal,
SecretScopeBackendType backend_type,
const std::optional<std::string>& azure_resource_id = std::nullopt,
const std::optional<std::string>& azure_tenant_id = std::nullopt,
const std::optional<std::string>& dns_name = std::nullopt);

/**
* @brief Delete a secret scope
*
* @param scope The name of the secret scope to delete
* @throws std::runtime_error if the scope is not found or the API request fails
*
* @note Deleting a scope also deletes all secrets stored in that scope.
* This operation cannot be undone.
*/
void delete_scope(const std::string& scope);

// Secret operations
/**
* @brief Store a secret as a string value
*
* @param scope The name of the secret scope
* @param key The name of the secret to store
* @param value The secret value as a string
* @throws std::runtime_error if the API request fails
*
* @note The value parameter is passed by const reference to avoid unnecessary copies.
* For enhanced security, users should securely clear the value from memory
* after calling this function.
*/
void put_secret(const std::string& scope, const std::string& key, const std::string& value);

/**
* @brief Delete a secret from a scope
*
* @param scope The name of the secret scope
* @param key The name of the secret to delete
* @throws std::runtime_error if the secret is not found or the API request fails
*/
void delete_secret(const std::string& scope, const std::string& key);

/**
* @brief List all secrets in a scope
*
* @param scope The name of the secret scope
* @return Vector of Secret objects containing metadata (not the secret values)
* @throws std::runtime_error if the API request fails
*
* @note This method returns secret metadata only. Secret values cannot be retrieved
* via the API for security reasons.
*/
std::vector<Secret> list_secrets(const std::string& scope);

private:
class Impl;
std::unique_ptr<Impl> pimpl_;

std::string backend_type_to_string(SecretScopeBackendType backend_type) const;

// Helper methods for parsing API responses
static std::vector<SecretScope> parse_scopes_list(const std::string& json_str);
static std::vector<Secret> parse_secrets_list(const std::string& json_str);
};

} // namespace databricks
Loading