|  | /* | 
|  | * | 
|  | *    Copyright (c) 2021 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 <app/AppConfig.h> | 
|  | #include <app/AttributeAccessInterfaceRegistry.h> | 
|  | #include <app/AttributeValueDecoder.h> | 
|  | #include <app/ConcreteAttributePath.h> | 
|  | #include <app/GlobalAttributes.h> | 
|  | #include <app/InteractionModelEngine.h> | 
|  | #include <app/MessageDef/EventPathIB.h> | 
|  | #include <app/MessageDef/StatusIB.h> | 
|  | #include <app/RequiredPrivilege.h> | 
|  | #include <app/StatusResponse.h> | 
|  | #include <app/WriteHandler.h> | 
|  | #include <app/data-model-provider/ActionReturnStatus.h> | 
|  | #include <app/data-model-provider/MetadataLookup.h> | 
|  | #include <app/data-model-provider/MetadataTypes.h> | 
|  | #include <app/data-model-provider/OperationTypes.h> | 
|  | #include <app/reporting/Engine.h> | 
|  | #include <app/util/MatterCallbacks.h> | 
|  | #include <credentials/GroupDataProvider.h> | 
|  | #include <lib/core/CHIPError.h> | 
|  | #include <lib/support/CodeUtils.h> | 
|  | #include <lib/support/TypeTraits.h> | 
|  | #include <lib/support/logging/TextOnlyLogging.h> | 
|  | #include <messaging/ExchangeContext.h> | 
|  | #include <protocols/interaction_model/StatusCode.h> | 
|  |  | 
|  | #include <optional> | 
|  |  | 
|  | namespace chip { | 
|  | namespace app { | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | using Protocols::InteractionModel::Status; | 
|  |  | 
|  | /// Wraps a EndpointIterator and ensures that `::Release()` is called | 
|  | /// for the iterator (assuming it is non-null) | 
|  | class AutoReleaseGroupEndpointIterator | 
|  | { | 
|  | public: | 
|  | explicit AutoReleaseGroupEndpointIterator(Credentials::GroupDataProvider::EndpointIterator * iterator) : mIterator(iterator) {} | 
|  | ~AutoReleaseGroupEndpointIterator() | 
|  | { | 
|  | if (mIterator != nullptr) | 
|  | { | 
|  | mIterator->Release(); | 
|  | } | 
|  | } | 
|  |  | 
|  | bool IsNull() const { return mIterator == nullptr; } | 
|  | bool Next(Credentials::GroupDataProvider::GroupEndpoint & item) { return mIterator->Next(item); } | 
|  |  | 
|  | private: | 
|  | Credentials::GroupDataProvider::EndpointIterator * mIterator; | 
|  | }; | 
|  |  | 
|  | } // namespace | 
|  |  | 
|  | using namespace Protocols::InteractionModel; | 
|  | using Status = Protocols::InteractionModel::Status; | 
|  |  | 
|  | CHIP_ERROR WriteHandler::Init(DataModel::Provider * apProvider, WriteHandlerDelegate * apWriteHandlerDelegate) | 
|  | { | 
|  | VerifyOrReturnError(!mExchangeCtx, CHIP_ERROR_INCORRECT_STATE); | 
|  | VerifyOrReturnError(apWriteHandlerDelegate, CHIP_ERROR_INVALID_ARGUMENT); | 
|  | VerifyOrReturnError(apProvider, CHIP_ERROR_INVALID_ARGUMENT); | 
|  | mDataModelProvider = apProvider; | 
|  |  | 
|  | mDelegate = apWriteHandlerDelegate; | 
|  | MoveToState(State::Initialized); | 
|  |  | 
|  | mACLCheckCache.ClearValue(); | 
|  | mProcessingAttributePath.ClearValue(); | 
|  |  | 
|  | return CHIP_NO_ERROR; | 
|  | } | 
|  |  | 
|  | void WriteHandler::Close() | 
|  | { | 
|  | VerifyOrReturn(mState != State::Uninitialized); | 
|  |  | 
|  | // DeliverFinalListWriteEnd will be a no-op if we have called | 
|  | // DeliverFinalListWriteEnd in success conditions, so passing false for | 
|  | // wasSuccessful here is safe: if it does anything, we were in fact not | 
|  | // successful. | 
|  | DeliverFinalListWriteEnd(false /* wasSuccessful */); | 
|  | mExchangeCtx.Release(); | 
|  | mStateFlags.Clear(StateBits::kSuppressResponse); | 
|  | mDataModelProvider = nullptr; | 
|  | MoveToState(State::Uninitialized); | 
|  | } | 
|  |  | 
|  | std::optional<bool> WriteHandler::IsListAttributePath(const ConcreteAttributePath & path) | 
|  | { | 
|  | if (mDataModelProvider == nullptr) | 
|  | { | 
|  | #if CHIP_CONFIG_DATA_MODEL_EXTRA_LOGGING | 
|  | ChipLogError(DataManagement, "Null data model while checking attribute properties."); | 
|  | #endif | 
|  | return std::nullopt; | 
|  | } | 
|  |  | 
|  | DataModel::AttributeFinder finder(mDataModelProvider); | 
|  | std::optional<DataModel::AttributeEntry> info = finder.Find(path); | 
|  |  | 
|  | if (!info.has_value()) | 
|  | { | 
|  | return std::nullopt; | 
|  | } | 
|  |  | 
|  | return info->HasFlags(DataModel::AttributeQualityFlags::kListAttribute); | 
|  | } | 
|  |  | 
|  | Status WriteHandler::HandleWriteRequestMessage(Messaging::ExchangeContext * apExchangeContext, | 
|  | System::PacketBufferHandle && aPayload, bool aIsTimedWrite) | 
|  | { | 
|  | System::PacketBufferHandle packet = System::PacketBufferHandle::New(chip::app::kMaxSecureSduLengthBytes); | 
|  | VerifyOrReturnError(!packet.IsNull(), Status::Failure); | 
|  |  | 
|  | System::PacketBufferTLVWriter messageWriter; | 
|  | messageWriter.Init(std::move(packet)); | 
|  | VerifyOrReturnError(mWriteResponseBuilder.Init(&messageWriter) == CHIP_NO_ERROR, Status::Failure); | 
|  |  | 
|  | mWriteResponseBuilder.CreateWriteResponses(); | 
|  | VerifyOrReturnError(mWriteResponseBuilder.GetError() == CHIP_NO_ERROR, Status::Failure); | 
|  |  | 
|  | Status status = ProcessWriteRequest(std::move(aPayload), aIsTimedWrite); | 
|  |  | 
|  | // Do not send response on Group Write | 
|  | if (status == Status::Success && !apExchangeContext->IsGroupExchangeContext()) | 
|  | { | 
|  | CHIP_ERROR err = SendWriteResponse(std::move(messageWriter)); | 
|  | if (err != CHIP_NO_ERROR) | 
|  | { | 
|  | status = Status::Failure; | 
|  | } | 
|  | } | 
|  |  | 
|  | return status; | 
|  | } | 
|  |  | 
|  | Status WriteHandler::OnWriteRequest(Messaging::ExchangeContext * apExchangeContext, System::PacketBufferHandle && aPayload, | 
|  | bool aIsTimedWrite) | 
|  | { | 
|  | // | 
|  | // Let's take over further message processing on this exchange from the IM. | 
|  | // This is only relevant during chunked requests. | 
|  | // | 
|  | mExchangeCtx.Grab(apExchangeContext); | 
|  |  | 
|  | Status status = HandleWriteRequestMessage(apExchangeContext, std::move(aPayload), aIsTimedWrite); | 
|  |  | 
|  | // The write transaction will be alive only when the message was handled successfully and there are more chunks. | 
|  | if (!(status == Status::Success && mStateFlags.Has(StateBits::kHasMoreChunks))) | 
|  | { | 
|  | Close(); | 
|  | } | 
|  |  | 
|  | return status; | 
|  | } | 
|  |  | 
|  | CHIP_ERROR WriteHandler::OnMessageReceived(Messaging::ExchangeContext * apExchangeContext, const PayloadHeader & aPayloadHeader, | 
|  | System::PacketBufferHandle && aPayload) | 
|  | { | 
|  | CHIP_ERROR err = CHIP_NO_ERROR; | 
|  |  | 
|  | VerifyOrDieWithMsg(apExchangeContext == mExchangeCtx.Get(), DataManagement, | 
|  | "Incoming exchange context should be same as the initial request."); | 
|  | VerifyOrDieWithMsg(!apExchangeContext->IsGroupExchangeContext(), DataManagement, | 
|  | "OnMessageReceived should not be called on GroupExchangeContext"); | 
|  | if (!aPayloadHeader.HasMessageType(Protocols::InteractionModel::MsgType::WriteRequest)) | 
|  | { | 
|  | if (aPayloadHeader.HasMessageType(Protocols::InteractionModel::MsgType::StatusResponse)) | 
|  | { | 
|  | CHIP_ERROR statusError = CHIP_NO_ERROR; | 
|  | // Parse the status response so we can log it properly. | 
|  | StatusResponse::ProcessStatusResponse(std::move(aPayload), statusError); | 
|  | } | 
|  | ChipLogDetail(DataManagement, "Unexpected message type %d", aPayloadHeader.GetMessageType()); | 
|  | StatusResponse::Send(Status::InvalidAction, apExchangeContext, false /*aExpectResponse*/); | 
|  | Close(); | 
|  | return CHIP_ERROR_INVALID_MESSAGE_TYPE; | 
|  | } | 
|  |  | 
|  | Status status = | 
|  | HandleWriteRequestMessage(apExchangeContext, std::move(aPayload), false /* chunked write should not be timed write */); | 
|  | if (status == Status::Success) | 
|  | { | 
|  | // We have no more chunks, the write response has been sent in HandleWriteRequestMessage, so close directly. | 
|  | if (!mStateFlags.Has(StateBits::kHasMoreChunks)) | 
|  | { | 
|  | Close(); | 
|  | } | 
|  | } | 
|  | else | 
|  | { | 
|  | err = StatusResponse::Send(status, apExchangeContext, false /*aExpectResponse*/); | 
|  | Close(); | 
|  | } | 
|  | return CHIP_NO_ERROR; | 
|  | } | 
|  |  | 
|  | void WriteHandler::OnResponseTimeout(Messaging::ExchangeContext * apExchangeContext) | 
|  | { | 
|  | ChipLogError(DataManagement, "Time out! failed to receive status response from Exchange: " ChipLogFormatExchange, | 
|  | ChipLogValueExchange(apExchangeContext)); | 
|  | Close(); | 
|  | } | 
|  |  | 
|  | CHIP_ERROR WriteHandler::FinalizeMessage(System::PacketBufferTLVWriter && aMessageWriter, System::PacketBufferHandle & packet) | 
|  | { | 
|  | VerifyOrReturnError(mState == State::AddStatus, CHIP_ERROR_INCORRECT_STATE); | 
|  | ReturnErrorOnFailure(mWriteResponseBuilder.GetWriteResponses().EndOfAttributeStatuses()); | 
|  | ReturnErrorOnFailure(mWriteResponseBuilder.EndOfWriteResponseMessage()); | 
|  | ReturnErrorOnFailure(aMessageWriter.Finalize(&packet)); | 
|  | return CHIP_NO_ERROR; | 
|  | } | 
|  |  | 
|  | CHIP_ERROR WriteHandler::SendWriteResponse(System::PacketBufferTLVWriter && aMessageWriter) | 
|  | { | 
|  | CHIP_ERROR err = CHIP_NO_ERROR; | 
|  | System::PacketBufferHandle packet; | 
|  |  | 
|  | VerifyOrExit(mState == State::AddStatus, err = CHIP_ERROR_INCORRECT_STATE); | 
|  |  | 
|  | err = FinalizeMessage(std::move(aMessageWriter), packet); | 
|  | SuccessOrExit(err); | 
|  |  | 
|  | VerifyOrExit(mExchangeCtx, err = CHIP_ERROR_INCORRECT_STATE); | 
|  | mExchangeCtx->UseSuggestedResponseTimeout(app::kExpectedIMProcessingTime); | 
|  | err = mExchangeCtx->SendMessage(Protocols::InteractionModel::MsgType::WriteResponse, std::move(packet), | 
|  | mStateFlags.Has(StateBits::kHasMoreChunks) ? Messaging::SendMessageFlags::kExpectResponse | 
|  | : Messaging::SendMessageFlags::kNone); | 
|  | SuccessOrExit(err); | 
|  |  | 
|  | MoveToState(State::Sending); | 
|  |  | 
|  | exit: | 
|  | return err; | 
|  | } | 
|  |  | 
|  | void WriteHandler::DeliverListWriteBegin(const ConcreteAttributePath & aPath) | 
|  | { | 
|  | if (mDataModelProvider != nullptr) | 
|  | { | 
|  | mDataModelProvider->ListAttributeWriteNotification(aPath, DataModel::ListWriteOperation::kListWriteBegin); | 
|  | } | 
|  | } | 
|  |  | 
|  | void WriteHandler::DeliverListWriteEnd(const ConcreteAttributePath & aPath, bool writeWasSuccessful) | 
|  | { | 
|  | if (mDataModelProvider != nullptr) | 
|  | { | 
|  | mDataModelProvider->ListAttributeWriteNotification(aPath, | 
|  | writeWasSuccessful ? DataModel::ListWriteOperation::kListWriteSuccess | 
|  | : DataModel::ListWriteOperation::kListWriteFailure); | 
|  | } | 
|  | } | 
|  |  | 
|  | void WriteHandler::DeliverFinalListWriteEnd(bool writeWasSuccessful) | 
|  | { | 
|  | if (mProcessingAttributePath.HasValue() && mStateFlags.Has(StateBits::kProcessingAttributeIsList)) | 
|  | { | 
|  | DeliverListWriteEnd(mProcessingAttributePath.Value(), writeWasSuccessful); | 
|  | } | 
|  | mProcessingAttributePath.ClearValue(); | 
|  | } | 
|  |  | 
|  | CHIP_ERROR WriteHandler::DeliverFinalListWriteEndForGroupWrite(bool writeWasSuccessful) | 
|  | { | 
|  | VerifyOrReturnError(mProcessingAttributePath.HasValue() && mStateFlags.Has(StateBits::kProcessingAttributeIsList), | 
|  | CHIP_NO_ERROR); | 
|  |  | 
|  | Credentials::GroupDataProvider::GroupEndpoint mapping; | 
|  | Credentials::GroupDataProvider * groupDataProvider = Credentials::GetGroupDataProvider(); | 
|  | Credentials::GroupDataProvider::EndpointIterator * iterator; | 
|  |  | 
|  | GroupId groupId         = mExchangeCtx->GetSessionHandle()->AsIncomingGroupSession()->GetGroupId(); | 
|  | FabricIndex fabricIndex = GetAccessingFabricIndex(); | 
|  |  | 
|  | auto processingConcreteAttributePath = mProcessingAttributePath.Value(); | 
|  | mProcessingAttributePath.ClearValue(); | 
|  |  | 
|  | iterator = groupDataProvider->IterateEndpoints(fabricIndex); | 
|  | VerifyOrReturnError(iterator != nullptr, CHIP_ERROR_NO_MEMORY); | 
|  |  | 
|  | while (iterator->Next(mapping)) | 
|  | { | 
|  | if (groupId != mapping.group_id) | 
|  | { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | processingConcreteAttributePath.mEndpointId = mapping.endpoint_id; | 
|  |  | 
|  | VerifyOrReturnError(mDelegate, CHIP_ERROR_INCORRECT_STATE); | 
|  | if (!mDelegate->HasConflictWriteRequests(this, processingConcreteAttributePath)) | 
|  | { | 
|  | DeliverListWriteEnd(processingConcreteAttributePath, writeWasSuccessful); | 
|  | } | 
|  | } | 
|  | iterator->Release(); | 
|  | return CHIP_NO_ERROR; | 
|  | } | 
|  | namespace { | 
|  |  | 
|  | // To reduce the various use of previousProcessed.HasValue() && previousProcessed.Value() == nextAttribute to save code size. | 
|  | bool IsSameAttribute(const Optional<ConcreteAttributePath> & previousProcessed, const ConcreteDataAttributePath & nextAttribute) | 
|  | { | 
|  | return previousProcessed.HasValue() && previousProcessed.Value() == nextAttribute; | 
|  | } | 
|  |  | 
|  | bool ShouldReportListWriteEnd(const Optional<ConcreteAttributePath> & previousProcessed, bool previousProcessedAttributeIsList, | 
|  | const ConcreteDataAttributePath & nextAttribute) | 
|  | { | 
|  | return previousProcessedAttributeIsList && !IsSameAttribute(previousProcessed, nextAttribute) && previousProcessed.HasValue(); | 
|  | } | 
|  |  | 
|  | bool ShouldReportListWriteBegin(const Optional<ConcreteAttributePath> & previousProcessed, bool previousProcessedAttributeIsList, | 
|  | const ConcreteDataAttributePath & nextAttribute) | 
|  | { | 
|  | return !IsSameAttribute(previousProcessed, nextAttribute) && nextAttribute.IsListOperation(); | 
|  | } | 
|  |  | 
|  | } // namespace | 
|  |  | 
|  | CHIP_ERROR WriteHandler::ProcessAttributeDataIBs(TLV::TLVReader & aAttributeDataIBsReader) | 
|  | { | 
|  | CHIP_ERROR err = CHIP_NO_ERROR; | 
|  |  | 
|  | VerifyOrReturnError(mExchangeCtx, CHIP_ERROR_INTERNAL); | 
|  | const Access::SubjectDescriptor subjectDescriptor = mExchangeCtx->GetSessionHandle()->GetSubjectDescriptor(); | 
|  |  | 
|  | while (CHIP_NO_ERROR == (err = aAttributeDataIBsReader.Next())) | 
|  | { | 
|  | chip::TLV::TLVReader dataReader; | 
|  | AttributeDataIB::Parser element; | 
|  | AttributePathIB::Parser attributePath; | 
|  | ConcreteDataAttributePath dataAttributePath; | 
|  | TLV::TLVReader reader = aAttributeDataIBsReader; | 
|  |  | 
|  | err = element.Init(reader); | 
|  | SuccessOrExit(err); | 
|  |  | 
|  | err = element.GetPath(&attributePath); | 
|  | SuccessOrExit(err); | 
|  |  | 
|  | err = attributePath.GetConcreteAttributePath(dataAttributePath); | 
|  | SuccessOrExit(err); | 
|  |  | 
|  | err = element.GetData(&dataReader); | 
|  | SuccessOrExit(err); | 
|  |  | 
|  | if (!dataAttributePath.IsListOperation() && IsListAttributePath(dataAttributePath).value_or(false)) | 
|  | { | 
|  | dataAttributePath.mListOp = ConcreteDataAttributePath::ListOperation::ReplaceAll; | 
|  | } | 
|  |  | 
|  | VerifyOrExit(mDelegate, err = CHIP_ERROR_INCORRECT_STATE); | 
|  | if (mDelegate->HasConflictWriteRequests(this, dataAttributePath) || | 
|  | // Per chunking protocol, we are processing the list entries, but the initial empty list is not processed, so we reject | 
|  | // it with Busy status code. | 
|  | (dataAttributePath.IsListItemOperation() && !IsSameAttribute(mProcessingAttributePath, dataAttributePath))) | 
|  | { | 
|  | err = AddStatusInternal(dataAttributePath, StatusIB(Status::Busy)); | 
|  | continue; | 
|  | } | 
|  |  | 
|  | if (ShouldReportListWriteEnd(mProcessingAttributePath, mStateFlags.Has(StateBits::kProcessingAttributeIsList), | 
|  | dataAttributePath)) | 
|  | { | 
|  | DeliverListWriteEnd(mProcessingAttributePath.Value(), mStateFlags.Has(StateBits::kAttributeWriteSuccessful)); | 
|  | } | 
|  |  | 
|  | if (ShouldReportListWriteBegin(mProcessingAttributePath, mStateFlags.Has(StateBits::kProcessingAttributeIsList), | 
|  | dataAttributePath)) | 
|  | { | 
|  | DeliverListWriteBegin(dataAttributePath); | 
|  | mStateFlags.Set(StateBits::kAttributeWriteSuccessful); | 
|  | } | 
|  |  | 
|  | mStateFlags.Set(StateBits::kProcessingAttributeIsList, dataAttributePath.IsListOperation()); | 
|  | mProcessingAttributePath.SetValue(dataAttributePath); | 
|  |  | 
|  | DataModelCallbacks::GetInstance()->AttributeOperation(DataModelCallbacks::OperationType::Write, | 
|  | DataModelCallbacks::OperationOrder::Pre, dataAttributePath); | 
|  |  | 
|  | TLV::TLVWriter backup; | 
|  | DataVersion version = 0; | 
|  | mWriteResponseBuilder.GetWriteResponses().Checkpoint(backup); | 
|  | err = element.GetDataVersion(&version); | 
|  | if (CHIP_NO_ERROR == err) | 
|  | { | 
|  | dataAttributePath.mDataVersion.SetValue(version); | 
|  | } | 
|  | else if (CHIP_END_OF_TLV == err) | 
|  | { | 
|  | err = CHIP_NO_ERROR; | 
|  | } | 
|  | SuccessOrExit(err); | 
|  | err = WriteClusterData(subjectDescriptor, dataAttributePath, dataReader); | 
|  | if (err != CHIP_NO_ERROR) | 
|  | { | 
|  | mWriteResponseBuilder.GetWriteResponses().Rollback(backup); | 
|  | err = AddStatusInternal(dataAttributePath, StatusIB(err)); | 
|  | } | 
|  |  | 
|  | DataModelCallbacks::GetInstance()->AttributeOperation(DataModelCallbacks::OperationType::Write, | 
|  | DataModelCallbacks::OperationOrder::Post, dataAttributePath); | 
|  | SuccessOrExit(err); | 
|  | } | 
|  |  | 
|  | if (CHIP_END_OF_TLV == err) | 
|  | { | 
|  | err = CHIP_NO_ERROR; | 
|  | } | 
|  |  | 
|  | SuccessOrExit(err); | 
|  |  | 
|  | if (!mStateFlags.Has(StateBits::kHasMoreChunks)) | 
|  | { | 
|  | DeliverFinalListWriteEnd(mStateFlags.Has(StateBits::kAttributeWriteSuccessful)); | 
|  | } | 
|  |  | 
|  | exit: | 
|  | return err; | 
|  | } | 
|  |  | 
|  | CHIP_ERROR WriteHandler::ProcessGroupAttributeDataIBs(TLV::TLVReader & aAttributeDataIBsReader) | 
|  | { | 
|  | CHIP_ERROR err = CHIP_NO_ERROR; | 
|  |  | 
|  | VerifyOrReturnError(mExchangeCtx, CHIP_ERROR_INTERNAL); | 
|  | const Access::SubjectDescriptor subjectDescriptor = | 
|  | mExchangeCtx->GetSessionHandle()->AsIncomingGroupSession()->GetSubjectDescriptor(); | 
|  |  | 
|  | GroupId groupId    = mExchangeCtx->GetSessionHandle()->AsIncomingGroupSession()->GetGroupId(); | 
|  | FabricIndex fabric = GetAccessingFabricIndex(); | 
|  |  | 
|  | while (CHIP_NO_ERROR == (err = aAttributeDataIBsReader.Next())) | 
|  | { | 
|  | chip::TLV::TLVReader dataReader; | 
|  | AttributeDataIB::Parser element; | 
|  | AttributePathIB::Parser attributePath; | 
|  | ConcreteDataAttributePath dataAttributePath; | 
|  | TLV::TLVReader reader = aAttributeDataIBsReader; | 
|  |  | 
|  | err = element.Init(reader); | 
|  | SuccessOrExit(err); | 
|  |  | 
|  | err = element.GetPath(&attributePath); | 
|  | SuccessOrExit(err); | 
|  |  | 
|  | err = attributePath.GetGroupAttributePath(dataAttributePath); | 
|  | SuccessOrExit(err); | 
|  |  | 
|  | err = element.GetData(&dataReader); | 
|  | SuccessOrExit(err); | 
|  |  | 
|  | if (!dataAttributePath.IsListOperation() && dataReader.GetType() == TLV::TLVType::kTLVType_Array) | 
|  | { | 
|  | dataAttributePath.mListOp = ConcreteDataAttributePath::ListOperation::ReplaceAll; | 
|  | } | 
|  |  | 
|  | ChipLogDetail(DataManagement, | 
|  | "Received group attribute write for Group=%u Cluster=" ChipLogFormatMEI " attribute=" ChipLogFormatMEI, | 
|  | groupId, ChipLogValueMEI(dataAttributePath.mClusterId), ChipLogValueMEI(dataAttributePath.mAttributeId)); | 
|  |  | 
|  | AutoReleaseGroupEndpointIterator iterator(Credentials::GetGroupDataProvider()->IterateEndpoints(fabric)); | 
|  | VerifyOrExit(!iterator.IsNull(), err = CHIP_ERROR_NO_MEMORY); | 
|  |  | 
|  | bool shouldReportListWriteEnd = ShouldReportListWriteEnd( | 
|  | mProcessingAttributePath, mStateFlags.Has(StateBits::kProcessingAttributeIsList), dataAttributePath); | 
|  | bool shouldReportListWriteBegin = false; // This will be set below. | 
|  |  | 
|  | std::optional<bool> isListAttribute = std::nullopt; | 
|  |  | 
|  | Credentials::GroupDataProvider::GroupEndpoint mapping; | 
|  | while (iterator.Next(mapping)) | 
|  | { | 
|  | if (groupId != mapping.group_id) | 
|  | { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | dataAttributePath.mEndpointId = mapping.endpoint_id; | 
|  |  | 
|  | // Try to get the metadata from for the attribute from one of the expanded endpoints (it doesn't really matter which | 
|  | // endpoint we pick, as long as it's valid) and update the path info according to it and recheck if we need to report | 
|  | // list write begin. | 
|  | if (!isListAttribute.has_value()) | 
|  | { | 
|  | isListAttribute             = IsListAttributePath(dataAttributePath); | 
|  | bool currentAttributeIsList = isListAttribute.value_or(false); | 
|  |  | 
|  | if (!dataAttributePath.IsListOperation() && currentAttributeIsList) | 
|  | { | 
|  | dataAttributePath.mListOp = ConcreteDataAttributePath::ListOperation::ReplaceAll; | 
|  | } | 
|  | ConcreteDataAttributePath pathForCheckingListWriteBegin(kInvalidEndpointId, dataAttributePath.mClusterId, | 
|  | dataAttributePath.mEndpointId, dataAttributePath.mListOp, | 
|  | dataAttributePath.mListIndex); | 
|  | shouldReportListWriteBegin = | 
|  | ShouldReportListWriteBegin(mProcessingAttributePath, mStateFlags.Has(StateBits::kProcessingAttributeIsList), | 
|  | pathForCheckingListWriteBegin); | 
|  | } | 
|  |  | 
|  | if (shouldReportListWriteEnd) | 
|  | { | 
|  | auto processingConcreteAttributePath        = mProcessingAttributePath.Value(); | 
|  | processingConcreteAttributePath.mEndpointId = mapping.endpoint_id; | 
|  | VerifyOrExit(mDelegate, err = CHIP_ERROR_INCORRECT_STATE); | 
|  | if (mDelegate->HasConflictWriteRequests(this, processingConcreteAttributePath)) | 
|  | { | 
|  | DeliverListWriteEnd(processingConcreteAttributePath, true /* writeWasSuccessful */); | 
|  | } | 
|  | } | 
|  |  | 
|  | VerifyOrExit(mDelegate, err = CHIP_ERROR_INCORRECT_STATE); | 
|  | if (mDelegate->HasConflictWriteRequests(this, dataAttributePath)) | 
|  | { | 
|  | ChipLogDetail(DataManagement, | 
|  | "Writing attribute endpoint=%u Cluster=" ChipLogFormatMEI " attribute=" ChipLogFormatMEI | 
|  | " is conflict with other write transactions.", | 
|  | mapping.endpoint_id, ChipLogValueMEI(dataAttributePath.mClusterId), | 
|  | ChipLogValueMEI(dataAttributePath.mAttributeId)); | 
|  | continue; | 
|  | } | 
|  |  | 
|  | if (shouldReportListWriteBegin) | 
|  | { | 
|  | DeliverListWriteBegin(dataAttributePath); | 
|  | } | 
|  |  | 
|  | ChipLogDetail(DataManagement, | 
|  | "Processing group attribute write for endpoint=%u Cluster=" ChipLogFormatMEI | 
|  | " attribute=" ChipLogFormatMEI, | 
|  | mapping.endpoint_id, ChipLogValueMEI(dataAttributePath.mClusterId), | 
|  | ChipLogValueMEI(dataAttributePath.mAttributeId)); | 
|  |  | 
|  | chip::TLV::TLVReader tmpDataReader(dataReader); | 
|  |  | 
|  | DataModelCallbacks::GetInstance()->AttributeOperation(DataModelCallbacks::OperationType::Write, | 
|  | DataModelCallbacks::OperationOrder::Pre, dataAttributePath); | 
|  | err = WriteClusterData(subjectDescriptor, dataAttributePath, tmpDataReader); | 
|  | if (err != CHIP_NO_ERROR) | 
|  | { | 
|  | ChipLogError(DataManagement, | 
|  | "WriteClusterData Endpoint=%u Cluster=" ChipLogFormatMEI " Attribute =" ChipLogFormatMEI | 
|  | " failed: %" CHIP_ERROR_FORMAT, | 
|  | mapping.endpoint_id, ChipLogValueMEI(dataAttributePath.mClusterId), | 
|  | ChipLogValueMEI(dataAttributePath.mAttributeId), err.Format()); | 
|  | } | 
|  | DataModelCallbacks::GetInstance()->AttributeOperation(DataModelCallbacks::OperationType::Write, | 
|  | DataModelCallbacks::OperationOrder::Post, dataAttributePath); | 
|  | } | 
|  |  | 
|  | dataAttributePath.mEndpointId = kInvalidEndpointId; | 
|  | mStateFlags.Set(StateBits::kProcessingAttributeIsList, dataAttributePath.IsListOperation()); | 
|  | mProcessingAttributePath.SetValue(dataAttributePath); | 
|  | } | 
|  |  | 
|  | if (CHIP_END_OF_TLV == err) | 
|  | { | 
|  | err = CHIP_NO_ERROR; | 
|  | } | 
|  |  | 
|  | err = DeliverFinalListWriteEndForGroupWrite(true); | 
|  |  | 
|  | exit: | 
|  | // The DeliverFinalListWriteEndForGroupWrite above will deliver the successful state of the list write and clear the | 
|  | // mProcessingAttributePath making the following call no-op. So we call it again after the exit label to deliver a failure state | 
|  | // to the clusters. Ignore the error code since we need to deliver other more important failures. | 
|  | DeliverFinalListWriteEndForGroupWrite(false); | 
|  | return err; | 
|  | } | 
|  |  | 
|  | Status WriteHandler::ProcessWriteRequest(System::PacketBufferHandle && aPayload, bool aIsTimedWrite) | 
|  | { | 
|  | CHIP_ERROR err = CHIP_NO_ERROR; | 
|  | System::PacketBufferTLVReader reader; | 
|  |  | 
|  | WriteRequestMessage::Parser writeRequestParser; | 
|  | AttributeDataIBs::Parser AttributeDataIBsParser; | 
|  | TLV::TLVReader AttributeDataIBsReader; | 
|  | // Default to InvalidAction for our status; that's what we want if any of | 
|  | // the parsing of our overall structure or paths fails.  Once we have a | 
|  | // successfully parsed path, the only way we will get a failure return is if | 
|  | // our path handling fails to AddStatus on us. | 
|  | // | 
|  | // TODO: That's not technically InvalidAction, and we should probably make | 
|  | // our callees hand out Status as well. | 
|  | Status status = Status::InvalidAction; | 
|  |  | 
|  | mLastSuccessfullyWrittenPath = std::nullopt; | 
|  |  | 
|  | reader.Init(std::move(aPayload)); | 
|  |  | 
|  | err = writeRequestParser.Init(reader); | 
|  | SuccessOrExit(err); | 
|  |  | 
|  | #if CHIP_CONFIG_IM_PRETTY_PRINT | 
|  | writeRequestParser.PrettyPrint(); | 
|  | #endif // CHIP_CONFIG_IM_PRETTY_PRINT | 
|  | bool boolValue; | 
|  |  | 
|  | boolValue = mStateFlags.Has(StateBits::kSuppressResponse); | 
|  | err       = writeRequestParser.GetSuppressResponse(&boolValue); | 
|  | if (err == CHIP_END_OF_TLV) | 
|  | { | 
|  | err = CHIP_NO_ERROR; | 
|  | } | 
|  | SuccessOrExit(err); | 
|  | mStateFlags.Set(StateBits::kSuppressResponse, boolValue); | 
|  |  | 
|  | boolValue = mStateFlags.Has(StateBits::kIsTimedRequest); | 
|  | err       = writeRequestParser.GetTimedRequest(&boolValue); | 
|  | SuccessOrExit(err); | 
|  | mStateFlags.Set(StateBits::kIsTimedRequest, boolValue); | 
|  |  | 
|  | boolValue = mStateFlags.Has(StateBits::kHasMoreChunks); | 
|  | err       = writeRequestParser.GetMoreChunkedMessages(&boolValue); | 
|  | if (err == CHIP_ERROR_END_OF_TLV) | 
|  | { | 
|  | err = CHIP_NO_ERROR; | 
|  | } | 
|  | SuccessOrExit(err); | 
|  | mStateFlags.Set(StateBits::kHasMoreChunks, boolValue); | 
|  |  | 
|  | if (mStateFlags.Has(StateBits::kHasMoreChunks) && | 
|  | (mExchangeCtx->IsGroupExchangeContext() || mStateFlags.Has(StateBits::kIsTimedRequest))) | 
|  | { | 
|  | // Sanity check: group exchange context should only have one chunk. | 
|  | // Also, timed requests should not have more than one chunk. | 
|  | ExitNow(err = CHIP_ERROR_INVALID_MESSAGE_TYPE); | 
|  | } | 
|  |  | 
|  | err = writeRequestParser.GetWriteRequests(&AttributeDataIBsParser); | 
|  | SuccessOrExit(err); | 
|  |  | 
|  | if (mStateFlags.Has(StateBits::kIsTimedRequest) != aIsTimedWrite) | 
|  | { | 
|  | // The message thinks it should be part of a timed interaction but it's | 
|  | // not, or vice versa. | 
|  | status = Status::TimedRequestMismatch; | 
|  | goto exit; | 
|  | } | 
|  |  | 
|  | AttributeDataIBsParser.GetReader(&AttributeDataIBsReader); | 
|  |  | 
|  | if (mExchangeCtx->IsGroupExchangeContext()) | 
|  | { | 
|  | err = ProcessGroupAttributeDataIBs(AttributeDataIBsReader); | 
|  | } | 
|  | else | 
|  | { | 
|  | err = ProcessAttributeDataIBs(AttributeDataIBsReader); | 
|  | } | 
|  | SuccessOrExit(err); | 
|  | SuccessOrExit(err = writeRequestParser.ExitContainer()); | 
|  |  | 
|  | if (err == CHIP_NO_ERROR) | 
|  | { | 
|  | status = Status::Success; | 
|  | } | 
|  |  | 
|  | exit: | 
|  | if (err != CHIP_NO_ERROR) | 
|  | { | 
|  | ChipLogError(DataManagement, "Failed to process write request: %" CHIP_ERROR_FORMAT, err.Format()); | 
|  | } | 
|  | return status; | 
|  | } | 
|  |  | 
|  | CHIP_ERROR WriteHandler::AddStatus(const ConcreteDataAttributePath & aPath, | 
|  | const Protocols::InteractionModel::ClusterStatusCode & aStatus) | 
|  | { | 
|  | return AddStatusInternal(aPath, StatusIB{ aStatus }); | 
|  | } | 
|  |  | 
|  | CHIP_ERROR WriteHandler::AddClusterSpecificSuccess(const ConcreteDataAttributePath & aPath, ClusterStatus aClusterStatus) | 
|  | { | 
|  | return AddStatus(aPath, Protocols::InteractionModel::ClusterStatusCode::ClusterSpecificSuccess(aClusterStatus)); | 
|  | } | 
|  |  | 
|  | CHIP_ERROR WriteHandler::AddClusterSpecificFailure(const ConcreteDataAttributePath & aPath, ClusterStatus aClusterStatus) | 
|  | { | 
|  | return AddStatus(aPath, Protocols::InteractionModel::ClusterStatusCode::ClusterSpecificFailure(aClusterStatus)); | 
|  | } | 
|  |  | 
|  | CHIP_ERROR WriteHandler::AddStatusInternal(const ConcreteDataAttributePath & aPath, const StatusIB & aStatus) | 
|  | { | 
|  | AttributeStatusIBs::Builder & writeResponses   = mWriteResponseBuilder.GetWriteResponses(); | 
|  | AttributeStatusIB::Builder & attributeStatusIB = writeResponses.CreateAttributeStatus(); | 
|  |  | 
|  | if (!aStatus.IsSuccess()) | 
|  | { | 
|  | mStateFlags.Clear(StateBits::kAttributeWriteSuccessful); | 
|  | } | 
|  |  | 
|  | ReturnErrorOnFailure(writeResponses.GetError()); | 
|  |  | 
|  | AttributePathIB::Builder & path = attributeStatusIB.CreatePath(); | 
|  | ReturnErrorOnFailure(attributeStatusIB.GetError()); | 
|  | ReturnErrorOnFailure(path.Encode(aPath)); | 
|  |  | 
|  | StatusIB::Builder & statusIBBuilder = attributeStatusIB.CreateErrorStatus(); | 
|  | ReturnErrorOnFailure(attributeStatusIB.GetError()); | 
|  | statusIBBuilder.EncodeStatusIB(aStatus); | 
|  | ReturnErrorOnFailure(statusIBBuilder.GetError()); | 
|  | ReturnErrorOnFailure(attributeStatusIB.EndOfAttributeStatusIB()); | 
|  |  | 
|  | MoveToState(State::AddStatus); | 
|  | return CHIP_NO_ERROR; | 
|  | } | 
|  |  | 
|  | FabricIndex WriteHandler::GetAccessingFabricIndex() const | 
|  | { | 
|  | return mExchangeCtx->GetSessionHandle()->GetFabricIndex(); | 
|  | } | 
|  |  | 
|  | const char * WriteHandler::GetStateStr() const | 
|  | { | 
|  | #if CHIP_DETAIL_LOGGING | 
|  | switch (mState) | 
|  | { | 
|  | case State::Uninitialized: | 
|  | return "Uninitialized"; | 
|  |  | 
|  | case State::Initialized: | 
|  | return "Initialized"; | 
|  |  | 
|  | case State::AddStatus: | 
|  | return "AddStatus"; | 
|  | case State::Sending: | 
|  | return "Sending"; | 
|  | } | 
|  | #endif // CHIP_DETAIL_LOGGING | 
|  | return "N/A"; | 
|  | } | 
|  |  | 
|  | void WriteHandler::MoveToState(const State aTargetState) | 
|  | { | 
|  | mState = aTargetState; | 
|  | ChipLogDetail(DataManagement, "IM WH moving to [%s]", GetStateStr()); | 
|  | } | 
|  |  | 
|  | DataModel::ActionReturnStatus WriteHandler::CheckWriteAllowed(const Access::SubjectDescriptor & aSubject, | 
|  | const ConcreteAttributePath & aPath) | 
|  | { | 
|  | // TODO: ordering is to check writability/existence BEFORE ACL and this seems wrong, however | 
|  | //       existing unit tests (TC_AcessChecker.py) validate that we get UnsupportedWrite instead of UnsupportedAccess | 
|  | // | 
|  | //       This should likely be fixed in spec (probably already fixed by | 
|  | //       https://github.com/CHIP-Specifications/connectedhomeip-spec/pull/9024) | 
|  | //       and tests and implementation | 
|  | // | 
|  | //       Open issue that needs fixing: https://github.com/project-chip/connectedhomeip/issues/33735 | 
|  |  | 
|  | DataModel::AttributeFinder finder(mDataModelProvider); | 
|  |  | 
|  | std::optional<DataModel::AttributeEntry> attributeEntry = finder.Find(aPath); | 
|  |  | 
|  | // if path is not valid, return a spec-compliant return code. | 
|  | if (!attributeEntry.has_value()) | 
|  | { | 
|  | // Global lists are not in metadata and not writable. Return the correct error code according to the spec | 
|  | Status attributeErrorStatus = | 
|  | IsSupportedGlobalAttributeNotInMetadata(aPath.mAttributeId) ? Status::UnsupportedWrite : Status::UnsupportedAttribute; | 
|  |  | 
|  | return DataModel::ValidateClusterPath(mDataModelProvider, aPath, attributeErrorStatus); | 
|  | } | 
|  |  | 
|  | // Allow writes on writable attributes only | 
|  | VerifyOrReturnValue(attributeEntry->GetWritePrivilege().has_value(), Status::UnsupportedWrite); | 
|  |  | 
|  | bool checkAcl = true; | 
|  | if (mLastSuccessfullyWrittenPath.has_value()) | 
|  | { | 
|  | // only validate ACL if path has changed | 
|  | // | 
|  | // Note that this is NOT operator==: we could do `checkAcl == (aPath != *mLastSuccessfullyWrittenPath)` | 
|  | // however that seems to use more flash. | 
|  | if ((aPath.mEndpointId == mLastSuccessfullyWrittenPath->mEndpointId) && | 
|  | (aPath.mClusterId == mLastSuccessfullyWrittenPath->mClusterId) && | 
|  | (aPath.mAttributeId == mLastSuccessfullyWrittenPath->mAttributeId)) | 
|  | { | 
|  | checkAcl = false; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (checkAcl) | 
|  | { | 
|  | Access::RequestPath requestPath{ .cluster     = aPath.mClusterId, | 
|  | .endpoint    = aPath.mEndpointId, | 
|  | .requestType = Access::RequestType::kAttributeWriteRequest, | 
|  | .entityId    = aPath.mAttributeId }; | 
|  | CHIP_ERROR err = Access::GetAccessControl().Check(aSubject, requestPath, RequiredPrivilege::ForWriteAttribute(aPath)); | 
|  |  | 
|  | if (err != CHIP_NO_ERROR) | 
|  | { | 
|  | VerifyOrReturnValue(err != CHIP_ERROR_ACCESS_DENIED, Status::UnsupportedAccess); | 
|  | VerifyOrReturnValue(err != CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL, Status::AccessRestricted); | 
|  |  | 
|  | return err; | 
|  | } | 
|  | } | 
|  |  | 
|  | // validate that timed write is enforced | 
|  | VerifyOrReturnValue(IsTimedWrite() || !attributeEntry->HasFlags(DataModel::AttributeQualityFlags::kTimed), | 
|  | Status::NeedsTimedInteraction); | 
|  |  | 
|  | return Status::Success; | 
|  | } | 
|  |  | 
|  | CHIP_ERROR WriteHandler::WriteClusterData(const Access::SubjectDescriptor & aSubject, const ConcreteDataAttributePath & aPath, | 
|  | TLV::TLVReader & aData) | 
|  | { | 
|  | // Writes do not have a checked-path. If data model interface is enabled (both checked and only version) | 
|  | // the write is done via the DataModel interface | 
|  | VerifyOrReturnError(mDataModelProvider != nullptr, CHIP_ERROR_INCORRECT_STATE); | 
|  |  | 
|  | ChipLogDetail(DataManagement, "Writing attribute: Cluster=" ChipLogFormatMEI " Endpoint=0x%x AttributeId=" ChipLogFormatMEI, | 
|  | ChipLogValueMEI(aPath.mClusterId), aPath.mEndpointId, ChipLogValueMEI(aPath.mAttributeId)); | 
|  |  | 
|  | DataModel::ActionReturnStatus status = CheckWriteAllowed(aSubject, aPath); | 
|  | if (status.IsSuccess()) | 
|  | { | 
|  | DataModel::WriteAttributeRequest request; | 
|  |  | 
|  | request.path              = aPath; | 
|  | request.subjectDescriptor = &aSubject; | 
|  | request.writeFlags.Set(DataModel::WriteFlags::kTimed, IsTimedWrite()); | 
|  |  | 
|  | AttributeValueDecoder decoder(aData, aSubject); | 
|  | status = mDataModelProvider->WriteAttribute(request, decoder); | 
|  | } | 
|  |  | 
|  | mLastSuccessfullyWrittenPath = status.IsSuccess() ? std::make_optional(aPath) : std::nullopt; | 
|  |  | 
|  | return AddStatusInternal(aPath, StatusIB(status.GetStatusCode())); | 
|  | } | 
|  |  | 
|  | } // namespace app | 
|  | } // namespace chip |