Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/** @file dynamodb_big_segment_store.hpp
* @brief Server-Side DynamoDB Big Segments Store
*/

#pragma once

#include <launchdarkly/server_side/integrations/big_segments/ibig_segment_store.hpp>
#include <launchdarkly/server_side/integrations/dynamodb/options.hpp>

#include <tl/expected.hpp>

#include <memory>
#include <string>

namespace Aws::DynamoDB {
class DynamoDBClient;
}

namespace launchdarkly::server_side::integrations {

/**
* @brief DynamoDBBigSegmentStore is a Big Segments persistent store backed by
* Amazon DynamoDB.
*
* Call DynamoDBBigSegmentStore::Create to obtain a new instance, then pass it
* to the SDK via the Big Segments config builder.
*
* The DynamoDB table must already exist and follow the LaunchDarkly schema:
* a String partition key named `namespace` and a String sort key named `key`.
* The same table can be shared with @ref DynamoDBDataSource — Big Segments
* rows occupy their own partition-key values and do not conflict with
* flag/segment rows. The LaunchDarkly Relay Proxy is responsible for
* populating Big Segments data in this table; this class only reads from it.
*
* This implementation is backed by the AWS SDK for C++.
*/
class DynamoDBBigSegmentStore final : public IBigSegmentStore {
public:
/**
* @brief Creates a new DynamoDBBigSegmentStore, or returns an error if
* construction failed.
*
* @param table_name Name of the DynamoDB table to read from. The table
* must already exist; this class does not create it.
*
* @param prefix Optional namespace prefix. When non-empty, Big Segments
* rows live under partition keys `<prefix>:big_segments_user` and
* `<prefix>:big_segments_metadata`. This allows multiple LaunchDarkly
* environments to share a single table.
*
* @param options Optional AWS DynamoDB client configuration. See
* @ref DynamoDBClientOptions. When defaulted, the AWS SDK resolves
* region, endpoint, and credentials from the standard provider chain
* (environment variables, shared config files, instance metadata).
*
* @return A DynamoDBBigSegmentStore, or an error if construction failed.
*/
static tl::expected<std::unique_ptr<DynamoDBBigSegmentStore>, std::string>
Create(std::string table_name,
std::string prefix,
DynamoDBClientOptions options = {});

[[nodiscard]] GetMembershipResult GetMembership(
std::string const& context_hash) const override;
[[nodiscard]] GetMetadataResult GetMetadata() const override;

~DynamoDBBigSegmentStore() override;

private:
DynamoDBBigSegmentStore(
std::unique_ptr<Aws::DynamoDB::DynamoDBClient> client,
std::string table_name,
std::string prefix);

std::unique_ptr<Aws::DynamoDB::DynamoDBClient> client_;
std::string const table_name_;
std::string const prefix_;
std::string const user_namespace_;
std::string const metadata_namespace_;
};

} // namespace launchdarkly::server_side::integrations
1 change: 1 addition & 0 deletions libs/server-sdk-dynamodb-source/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ target_sources(${LIBNAME}
PRIVATE
${HEADER_LIST}
dynamodb_source.cpp
dynamodb_big_segment_store.cpp
aws_sdk_guard.cpp
client_factory.cpp
)
Expand Down
12 changes: 12 additions & 0 deletions libs/server-sdk-dynamodb-source/src/dynamodb_attributes.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,16 @@ inline constexpr char kItemAttribute[] = "item";
// {namespace: "myprefix:$inited", key: "myprefix:$inited"}.
inline constexpr char kInitedNamespace[] = "$inited";

// Big Segments schema. Membership rows use partition key
// "{prefix}:big_segments_user" and sort key {context_hash}, with
// "included" / "excluded" String Set attributes naming segment refs. The
// metadata row uses partition key AND sort key both set to
// "{prefix}:big_segments_metadata", with the sync timestamp stored as a
// Number under "synchronizedOn".
inline constexpr char kBigSegmentsUserNamespace[] = "big_segments_user";
inline constexpr char kBigSegmentsMetadataNamespace[] = "big_segments_metadata";
inline constexpr char kBigSegmentsIncludedAttribute[] = "included";
inline constexpr char kBigSegmentsExcludedAttribute[] = "excluded";
inline constexpr char kBigSegmentsSyncTimeAttribute[] = "synchronizedOn";

} // namespace launchdarkly::server_side::integrations::detail
148 changes: 148 additions & 0 deletions libs/server-sdk-dynamodb-source/src/dynamodb_big_segment_store.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#include <launchdarkly/server_side/integrations/dynamodb/dynamodb_big_segment_store.hpp>

#include "aws_sdk_guard.hpp"
#include "client_factory.hpp"
#include "dynamodb_attributes.hpp"
#include "prefix.hpp"

#include <aws/core/utils/Outcome.h>
#include <aws/dynamodb/DynamoDBClient.h>
#include <aws/dynamodb/model/AttributeValue.h>
#include <aws/dynamodb/model/GetItemRequest.h>

#include <cerrno>
#include <cstdint>
#include <cstdlib>
#include <exception>
#include <utility>

namespace launchdarkly::server_side::integrations {

namespace {

using detail::kBigSegmentsExcludedAttribute;
using detail::kBigSegmentsIncludedAttribute;
using detail::kBigSegmentsMetadataNamespace;
using detail::kBigSegmentsSyncTimeAttribute;
using detail::kBigSegmentsUserNamespace;
using detail::kPartitionKey;
using detail::kSortKey;
using detail::PrefixedNamespace;

} // namespace

tl::expected<std::unique_ptr<DynamoDBBigSegmentStore>, std::string>
DynamoDBBigSegmentStore::Create(std::string table_name,
std::string prefix,
DynamoDBClientOptions options) {
try {
detail::AwsSdkGuard::Ensure();
auto maybe_client = detail::BuildDynamoDBClient(options);
if (!maybe_client) {
return tl::make_unexpected(std::move(maybe_client.error()));
}
return std::unique_ptr<DynamoDBBigSegmentStore>(
new DynamoDBBigSegmentStore(std::move(*maybe_client),
std::move(table_name),
std::move(prefix)));
} catch (std::exception const& e) {
return tl::make_unexpected(e.what());
}
}

DynamoDBBigSegmentStore::DynamoDBBigSegmentStore(
std::unique_ptr<Aws::DynamoDB::DynamoDBClient> client,
std::string table_name,
std::string prefix)
: client_(std::move(client)),
table_name_(std::move(table_name)),
prefix_(std::move(prefix)),
user_namespace_(PrefixedNamespace(prefix_, kBigSegmentsUserNamespace)),
metadata_namespace_(
PrefixedNamespace(prefix_, kBigSegmentsMetadataNamespace)) {}

DynamoDBBigSegmentStore::~DynamoDBBigSegmentStore() = default;

IBigSegmentStore::GetMembershipResult DynamoDBBigSegmentStore::GetMembership(
std::string const& context_hash) const {
Aws::DynamoDB::Model::GetItemRequest request;
request.SetTableName(table_name_);
request.SetConsistentRead(true);
request.AddKey(kPartitionKey,
Aws::DynamoDB::Model::AttributeValue{user_namespace_});
request.AddKey(kSortKey,
Aws::DynamoDB::Model::AttributeValue{context_hash});

auto outcome = client_->GetItem(request);
if (!outcome.IsSuccess()) {
return tl::make_unexpected(outcome.GetError().GetMessage());
}

auto const& item = outcome.GetResult().GetItem();
if (item.empty()) {
return std::nullopt;
}

std::vector<std::string> included;
std::vector<std::string> excluded;

if (auto const it = item.find(kBigSegmentsIncludedAttribute);
it != item.end()) {
for (auto const& ref : it->second.GetSS()) {
included.emplace_back(ref);
}
}
if (auto const it = item.find(kBigSegmentsExcludedAttribute);
it != item.end()) {
for (auto const& ref : it->second.GetSS()) {
excluded.emplace_back(ref);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unvalidated GetSS() silently drops membership data on type mismatch

Medium Severity

The GetSS() calls on the included and excluded attributes are not validated for type correctness. AWS SDK's GetSS() silently returns an empty vector when the attribute is a non-SS type (S, N, BOOL, etc.), just as GetS() returns "" for non-String types. Since DynamoDB enforces that SS attributes must have at least one element, an empty result from GetSS() when the attribute key exists in the item indicates a type mismatch. The GetN() call for synchronizedOn at line 131 correctly validates for empty, but these GetSS() calls do not, causing silent membership data loss instead of surfacing an error.

Fix in Cursor Fix in Web

Triggered by learned rule: DynamoDB source: validate AttributeValue type before using GetS()

Reviewed by Cursor Bugbot for commit a3dfc38. Configure here.

}

return Membership::FromSegmentRefs(included, excluded);
}

IBigSegmentStore::GetMetadataResult DynamoDBBigSegmentStore::GetMetadata()
const {
Aws::DynamoDB::Model::GetItemRequest request;
request.SetTableName(table_name_);
request.SetConsistentRead(true);
request.AddKey(kPartitionKey,
Aws::DynamoDB::Model::AttributeValue{metadata_namespace_});
request.AddKey(kSortKey,
Aws::DynamoDB::Model::AttributeValue{metadata_namespace_});

auto outcome = client_->GetItem(request);
if (!outcome.IsSuccess()) {
return tl::make_unexpected(outcome.GetError().GetMessage());
}

auto const& item = outcome.GetResult().GetItem();
if (item.empty()) {
return std::nullopt;
}

auto const it = item.find(kBigSegmentsSyncTimeAttribute);
if (it == item.end()) {
return tl::make_unexpected(
"DynamoDB Big Segments metadata row missing 'synchronizedOn'");
}

auto const& raw = it->second.GetN();
if (raw.empty()) {
return tl::make_unexpected(
"DynamoDB Big Segments 'synchronizedOn' is empty or not type N");
}

errno = 0;
char* end = nullptr;
long long const parsed = std::strtoll(raw.c_str(), &end, 10);
if (errno != 0 || end == raw.c_str() || *end != '\0') {
return tl::make_unexpected(
"DynamoDB Big Segments 'synchronizedOn' is not a valid integer");
}

return StoreMetadata{std::chrono::milliseconds{parsed}};
}

} // namespace launchdarkly::server_side::integrations
Loading
Loading