blob: 2808a44377d8fe6f23676d5a1640b4327f8cb4e1 [file] [edit]
/*
*
* Copyright (c) 2026 Project CHIP Authors
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <pw_unit_test/framework.h>
#include <app/AttributeValueEncoder.h>
#include <app/InteractionModelEngine.h>
#include <app/ReadClient.h>
#include <app/data-model-provider/MetadataTypes.h>
#include <app/server-cluster/testing/EmptyProvider.h>
#include <app/tests/AppTestContext.h>
#include <clusters/OperationalCredentials/Structs.h>
#include <clusters/TlsCertificateManagement/Metadata.h>
#include <clusters/TlsClientManagement/Metadata.h>
#include <clusters/WebRTCTransportProvider/Metadata.h>
#include <clusters/WebRTCTransportRequestor/Metadata.h>
#include <lib/core/StringBuilderAdapters.h>
#include <lib/core/TLVUtilities.h>
#include <lib/support/tests/ExtraPwTestMacros.h>
#include <map>
#include <optional>
namespace {
using namespace chip;
using namespace chip::app;
constexpr EndpointId kEndpoint = 1;
constexpr ClusterId kCluster = 0xFFF1FC01;
constexpr AttributeId kFabricSensitiveList = 1;
constexpr AttributeId kFabricScopedList = 2;
// Three rows, one on the accessing fabric and two on other fabrics.
constexpr size_t kRowCount = 3;
using ListEntryType = Clusters::OperationalCredentials::Structs::FabricDescriptorStruct::Type;
/// Exposes two list attributes of fabric-scoped entries. Both read the same rows;
/// only kFabricSensitiveList carries the fabric-sensitive quality flag.
class FabricScopedListProvider : public chip::Testing::EmptyProvider
{
public:
CHIP_ERROR Endpoints(ReadOnlyBufferBuilder<DataModel::EndpointEntry> & builder) override
{
ReturnErrorOnFailure(builder.EnsureAppendCapacity(1));
return builder.Append({ kEndpoint, kInvalidEndpointId, DataModel::EndpointCompositionPattern::kFullFamily });
}
CHIP_ERROR ServerClusters(EndpointId endpointId, ReadOnlyBufferBuilder<DataModel::ServerClusterEntry> & builder) override
{
VerifyOrReturnError(endpointId == kEndpoint, CHIP_IM_GLOBAL_STATUS(UnsupportedEndpoint));
ReturnErrorOnFailure(builder.EnsureAppendCapacity(1));
return builder.Append({ kCluster, 1 /* dataVersion */, {} });
}
CHIP_ERROR Attributes(const ConcreteClusterPath & path, ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder) override
{
VerifyOrReturnError(path.mEndpointId == kEndpoint, CHIP_IM_GLOBAL_STATUS(UnsupportedEndpoint));
VerifyOrReturnError(path.mClusterId == kCluster, CHIP_IM_GLOBAL_STATUS(UnsupportedCluster));
using Flags = DataModel::AttributeQualityFlags;
ReturnErrorOnFailure(builder.EnsureAppendCapacity(2));
ReturnErrorOnFailure(builder.Append(
DataModel::AttributeEntry(kFabricSensitiveList, BitFlags<Flags>(Flags::kListAttribute, Flags::kFabricSensitive),
Access::Privilege::kView, std::nullopt)));
return builder.Append(DataModel::AttributeEntry(kFabricScopedList, BitFlags<Flags>(Flags::kListAttribute),
Access::Privilege::kView, std::nullopt));
}
ActionReturnStatus ReadAttribute(const DataModel::ReadAttributeRequest & request, AttributeValueEncoder & encoder) override
{
VerifyOrReturnError(request.path.mEndpointId == kEndpoint, Protocols::InteractionModel::Status::UnsupportedEndpoint);
VerifyOrReturnError(request.path.mClusterId == kCluster, Protocols::InteractionModel::Status::UnsupportedCluster);
VerifyOrReturnError(request.path.mAttributeId == kFabricSensitiveList || request.path.mAttributeId == kFabricScopedList,
Protocols::InteractionModel::Status::UnsupportedAttribute);
const FabricIndex accessing = request.subjectDescriptor.fabricIndex;
VerifyOrReturnError(accessing != kUndefinedFabricIndex, Protocols::InteractionModel::Status::Failure);
return encoder.EncodeList([accessing](const auto & listEncoder) -> CHIP_ERROR {
for (size_t i = 0; i < kRowCount; i++)
{
ListEntryType row;
// Row 0 belongs to the accessing fabric; the others sit on fabrics that
// are guaranteed distinct from it and valid (1..254).
row.fabricIndex = static_cast<FabricIndex>(((accessing - 1 + i) % 254) + 1);
ReturnErrorOnFailure(listEncoder.Encode(row));
}
return CHIP_NO_ERROR;
});
}
};
class ListCountingCallback : public ReadClient::Callback
{
public:
void OnAttributeData(const ConcreteDataAttributePath & aPath, TLV::TLVReader * apData, const StatusIB & aStatus) override
{
if (!aStatus.IsSuccess())
{
mFailureStatus = aStatus.mStatus;
ChipLogError(Test, "Attribute " ChipLogFormatMEI " status 0x%02x", ChipLogValueMEI(aPath.mAttributeId),
to_underlying(aStatus.mStatus));
return;
}
if (apData == nullptr)
{
mError = CHIP_ERROR_INVALID_ARGUMENT;
return;
}
VerifyOrReturn(aPath.IsListOperation() && !aPath.IsListItemOperation());
TLV::TLVType containerType;
VerifyOrReturn(apData->EnterContainer(containerType) == CHIP_NO_ERROR);
size_t count = 0;
VerifyOrReturn(TLV::Utilities::Count(*apData, count, /* aRecurse = */ false) == CHIP_NO_ERROR);
mListSizes[aPath.mAttributeId] = count;
}
void OnError(CHIP_ERROR aError) override { mError = aError; }
void OnDone(ReadClient *) override { mDone = true; }
std::optional<size_t> ListSize(AttributeId id) const
{
auto it = mListSizes.find(id);
return it == mListSizes.end() ? std::nullopt : std::optional<size_t>(it->second);
}
std::map<AttributeId, size_t> mListSizes;
std::optional<Protocols::InteractionModel::Status> mFailureStatus;
CHIP_ERROR mError = CHIP_NO_ERROR;
bool mDone = false;
};
class TestFabricSensitiveAttributeRead : public chip::Testing::AppContext
{
public:
void SetUp() override
{
AppContext::SetUp();
mOldProvider = InteractionModelEngine::GetInstance()->SetDataModelProvider(&mProvider);
}
void TearDown() override
{
InteractionModelEngine::GetInstance()->SetDataModelProvider(mOldProvider);
AppContext::TearDown();
}
void ReadBothLists(bool fabricFiltered, ListCountingCallback & callback)
{
AttributePathParams paths[2];
paths[0] = AttributePathParams(kEndpoint, kCluster, kFabricSensitiveList);
paths[1] = AttributePathParams(kEndpoint, kCluster, kFabricScopedList);
ReadPrepareParams params(GetSessionBobToAlice());
params.mpAttributePathParamsList = paths;
params.mAttributePathParamsListSize = 2;
params.mIsFabricFiltered = fabricFiltered;
ReadClient readClient(InteractionModelEngine::GetInstance(), &GetExchangeManager(), callback,
ReadClient::InteractionType::Read);
ASSERT_SUCCESS(readClient.SendRequest(params));
DrainAndServiceIO();
ASSERT_SUCCESS(callback.mError);
ASSERT_TRUE(callback.mDone);
ASSERT_FALSE(callback.mFailureStatus.has_value());
}
private:
FabricScopedListProvider mProvider;
DataModel::Provider * mOldProvider = nullptr;
};
TEST_F(TestFabricSensitiveAttributeRead, UnfilteredReadIsStillFilteredForFabricSensitiveAttribute)
{
ListCountingCallback callback;
ReadBothLists(/* fabricFiltered = */ false, callback);
// The plain fabric-scoped list honors the request flag and returns every row.
ASSERT_TRUE(callback.ListSize(kFabricScopedList).has_value());
EXPECT_EQ(callback.ListSize(kFabricScopedList).value_or(0), kRowCount);
// The fabric-sensitive list is filtered to the accessing fabric regardless of the flag.
ASSERT_TRUE(callback.ListSize(kFabricSensitiveList).has_value());
EXPECT_EQ(callback.ListSize(kFabricSensitiveList).value_or(0), 1u);
}
TEST_F(TestFabricSensitiveAttributeRead, FilteredReadFiltersBothLists)
{
ListCountingCallback callback;
ReadBothLists(/* fabricFiltered = */ true, callback);
ASSERT_TRUE(callback.ListSize(kFabricScopedList).has_value());
EXPECT_EQ(callback.ListSize(kFabricScopedList).value_or(0), 1u);
ASSERT_TRUE(callback.ListSize(kFabricSensitiveList).has_value());
EXPECT_EQ(callback.ListSize(kFabricSensitiveList).value_or(0), 1u);
}
// Every attribute the data model marks fabric-sensitive must carry the quality in
// its generated metadata, otherwise the reporting engine cannot enforce it.
// PushAvStreamTransport::CurrentConnections is not listed yet: its XML has not been
// regenerated with an Alchemy release that emits the marker.
static_assert(Clusters::WebRTCTransportProvider::Attributes::CurrentSessions::kMetadataEntry.HasFlags(
DataModel::AttributeQualityFlags::kFabricSensitive));
static_assert(Clusters::WebRTCTransportRequestor::Attributes::CurrentSessions::kMetadataEntry.HasFlags(
DataModel::AttributeQualityFlags::kFabricSensitive));
static_assert(Clusters::TlsCertificateManagement::Attributes::ProvisionedRootCertificates::kMetadataEntry.HasFlags(
DataModel::AttributeQualityFlags::kFabricSensitive));
static_assert(Clusters::TlsCertificateManagement::Attributes::ProvisionedClientCertificates::kMetadataEntry.HasFlags(
DataModel::AttributeQualityFlags::kFabricSensitive));
static_assert(Clusters::TlsClientManagement::Attributes::ProvisionedEndpoints::kMetadataEntry.HasFlags(
DataModel::AttributeQualityFlags::kFabricSensitive));
} // namespace