diff --git a/dbal/cpp/src/security/nonce_check_and_store.hpp b/dbal/cpp/src/security/nonce_check_and_store.hpp new file mode 100644 index 000000000..1c20504c2 --- /dev/null +++ b/dbal/cpp/src/security/nonce_check_and_store.hpp @@ -0,0 +1,43 @@ +#pragma once +/** + * @file nonce_check_and_store.hpp + * @brief Nonce validation and storage + */ + +#include +#include +#include + +namespace dbal::security { + +/** + * Nonce storage state + */ +struct NonceStorage { + std::unordered_map nonces; + std::chrono::steady_clock::time_point last_cleanup{}; + int expiry_seconds = 300; + int cleanup_interval_seconds = 60; +}; + +/** + * Check if nonce is fresh and store it + * @param storage The nonce storage state + * @param nonce The nonce to check + * @return true if fresh (not seen before), false if replay + */ +inline bool nonce_check_and_store(NonceStorage& storage, const std::string& nonce) { + auto now = std::chrono::steady_clock::now(); + + // Check if already exists + auto it = storage.nonces.find(nonce); + if (it != storage.nonces.end()) { + return false; // Replay detected + } + + // Store new nonce + storage.nonces[nonce] = now; + return true; +} + +} // namespace dbal::security diff --git a/dbal/cpp/src/security/nonce_cleanup.hpp b/dbal/cpp/src/security/nonce_cleanup.hpp new file mode 100644 index 000000000..751a01779 --- /dev/null +++ b/dbal/cpp/src/security/nonce_cleanup.hpp @@ -0,0 +1,30 @@ +#pragma once +/** + * @file nonce_cleanup.hpp + * @brief Expired nonce cleanup + */ + +#include "nonce_check_and_store.hpp" + +namespace dbal::security { + +/** + * Remove expired nonces from storage + * @param storage The nonce storage state + */ +inline void nonce_cleanup(NonceStorage& storage) { + auto now = std::chrono::steady_clock::now(); + auto cutoff = now - std::chrono::seconds(storage.expiry_seconds); + + for (auto it = storage.nonces.begin(); it != storage.nonces.end(); ) { + if (it->second < cutoff) { + it = storage.nonces.erase(it); + } else { + ++it; + } + } + + storage.last_cleanup = now; +} + +} // namespace dbal::security diff --git a/dbal/cpp/src/security/nonce_maybe_cleanup.hpp b/dbal/cpp/src/security/nonce_maybe_cleanup.hpp new file mode 100644 index 000000000..6ac22dd05 --- /dev/null +++ b/dbal/cpp/src/security/nonce_maybe_cleanup.hpp @@ -0,0 +1,26 @@ +#pragma once +/** + * @file nonce_maybe_cleanup.hpp + * @brief Conditional nonce cleanup based on interval + */ + +#include "nonce_cleanup.hpp" + +namespace dbal::security { + +/** + * Cleanup expired nonces if interval has passed + * @param storage The nonce storage state + */ +inline void nonce_maybe_cleanup(NonceStorage& storage) { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast( + now - storage.last_cleanup + ).count(); + + if (elapsed >= storage.cleanup_interval_seconds) { + nonce_cleanup(storage); + } +} + +} // namespace dbal::security diff --git a/dbal/cpp/src/security/nonce_size.hpp b/dbal/cpp/src/security/nonce_size.hpp new file mode 100644 index 000000000..2527aeb84 --- /dev/null +++ b/dbal/cpp/src/security/nonce_size.hpp @@ -0,0 +1,20 @@ +#pragma once +/** + * @file nonce_size.hpp + * @brief Get nonce storage size + */ + +#include "nonce_check_and_store.hpp" + +namespace dbal::security { + +/** + * Get number of stored nonces + * @param storage The nonce storage state + * @return Count of stored nonces + */ +inline size_t nonce_size(const NonceStorage& storage) { + return storage.nonces.size(); +} + +} // namespace dbal::security diff --git a/dbal/cpp/src/security/rate_limit_remaining.hpp b/dbal/cpp/src/security/rate_limit_remaining.hpp new file mode 100644 index 000000000..32042855c --- /dev/null +++ b/dbal/cpp/src/security/rate_limit_remaining.hpp @@ -0,0 +1,24 @@ +#pragma once +/** + * @file rate_limit_remaining.hpp + * @brief Get remaining tokens in bucket + */ + +#include "rate_limit_try_acquire.hpp" + +namespace dbal::security { + +/** + * Get remaining tokens in a bucket + * @param bucket The token bucket + * @param max_tokens Default if bucket uninitialized + * @return Number of remaining tokens + */ +inline double rate_limit_remaining(const TokenBucket& bucket, double max_tokens) { + if (bucket.last_update.time_since_epoch().count() == 0) { + return max_tokens; + } + return bucket.tokens; +} + +} // namespace dbal::security diff --git a/dbal/cpp/src/security/rate_limit_try_acquire.hpp b/dbal/cpp/src/security/rate_limit_try_acquire.hpp new file mode 100644 index 000000000..9fa5b8ac4 --- /dev/null +++ b/dbal/cpp/src/security/rate_limit_try_acquire.hpp @@ -0,0 +1,54 @@ +#pragma once +/** + * @file rate_limit_try_acquire.hpp + * @brief Token bucket acquire logic + */ + +#include +#include + +namespace dbal::security { + +/** + * Token bucket state + */ +struct TokenBucket { + double tokens = 0; + std::chrono::steady_clock::time_point last_update{}; +}; + +/** + * Try to acquire a token from bucket, refilling based on elapsed time + * @param bucket The token bucket state + * @param tokens_per_second Refill rate + * @param max_tokens Maximum bucket capacity + * @return true if token acquired, false if rate limited + */ +inline bool rate_limit_try_acquire( + TokenBucket& bucket, + double tokens_per_second, + double max_tokens +) { + auto now = std::chrono::steady_clock::now(); + + // Initialize new buckets + if (bucket.tokens == 0 && bucket.last_update.time_since_epoch().count() == 0) { + bucket.tokens = max_tokens; + bucket.last_update = now; + } + + // Refill tokens based on elapsed time + auto elapsed = std::chrono::duration(now - bucket.last_update).count(); + bucket.tokens = std::min(max_tokens, bucket.tokens + elapsed * tokens_per_second); + bucket.last_update = now; + + // Try to consume + if (bucket.tokens >= 1.0) { + bucket.tokens -= 1.0; + return true; + } + + return false; +} + +} // namespace dbal::security diff --git a/dbal/cpp/tests/unit/client_test.cpp b/dbal/cpp/tests/unit/client_test.cpp index 3fcc908e0..2cc2134bc 100644 --- a/dbal/cpp/tests/unit/client_test.cpp +++ b/dbal/cpp/tests/unit/client_test.cpp @@ -696,6 +696,15 @@ void test_lua_script_validation() { assert(resultGlobals.error().code() == dbal::ErrorCode::ValidationError); std::cout << " ✓ Empty allowed_globals rejected" << std::endl; + dbal::CreateLuaScriptInput inputForbiddenGlobals = input1; + inputForbiddenGlobals.name = "forbidden-globals"; + inputForbiddenGlobals.timeout_ms = 1000; + inputForbiddenGlobals.allowed_globals = {"os"}; + auto resultForbiddenGlobals = client.createLuaScript(inputForbiddenGlobals); + assert(resultForbiddenGlobals.isError()); + assert(resultForbiddenGlobals.error().code() == dbal::ErrorCode::ValidationError); + std::cout << " ✓ Forbidden globals rejected" << std::endl; + dbal::CreateLuaScriptInput input2; input2.name = "duplicate-script"; input2.code = "return true"; @@ -711,6 +720,14 @@ void test_lua_script_validation() { assert(result3.isError()); assert(result3.error().code() == dbal::ErrorCode::Conflict); std::cout << " ✓ Duplicate script name rejected" << std::endl; + + dbal::CreateLuaScriptInput input4 = input2; + input4.name = "dedupe-globals"; + input4.allowed_globals = {"math", "math", "print"}; + auto result4 = client.createLuaScript(input4); + assert(result4.isOk()); + assert(result4.value().allowed_globals.size() == 2); + std::cout << " ✓ Allowed globals deduped" << std::endl; } void test_package_crud() {