| /** | |
| * Copyright (c) 2020-2026 Project CHIP Authors | |
| * | |
| * 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/clusters/groups-server/GroupsClusterImpl.h> | |
| #include <app/ConcreteAttributePath.h> | |
| #include <app/clusters/access-control-server/AccessControlEventHelper.h> | |
| #include <app/clusters/scenes-server/Constants.h> | |
| #include <app/server-cluster/AttributeListBuilder.h> | |
| #include <clusters/GroupKeyManagement/Ids.h> | |
| #include <clusters/Groups/Attributes.h> | |
| #include <clusters/Groups/Commands.h> | |
| #include <clusters/Groups/Metadata.h> | |
| #include <clusters/Groups/Structs.h> | |
| #include <lib/support/AutoRelease.h> | |
| #include <tracing/macros.h> | |
| using namespace chip::app::Clusters::Groups; | |
| using chip::Credentials::GroupDataProvider; | |
| using chip::Protocols::InteractionModel::Status; | |
| // The macro LogErrorOnFailure generally logs file:line which makes the macro larger. We use here a smaller | |
| // macro (i.e. no line & file info) since the underlying error would have the original cause and logs would contain | |
| // what call is being processed. This is to save about 100 bytes or so of flash. | |
| #define LogIfFailure(expr) \ | |
| do \ | |
| { \ | |
| CHIP_ERROR __err = (expr); \ | |
| if (__err != CHIP_NO_ERROR) \ | |
| { \ | |
| ChipLogError(Zcl, "Error: %" CHIP_ERROR_FORMAT, __err.Format()); \ | |
| } \ | |
| } while (false) | |
| namespace chip::app::Clusters { | |
| namespace { | |
| void NotifyGroupTableChanged(ServerClusterContext * context) | |
| { | |
| // TODO: This seems a bit coupled: we are notifying in this cluster that ANOTHER cluster | |
| // has changed. We should support only one cluster or another really... | |
| VerifyOrReturn(context != nullptr); | |
| const ConcreteAttributePath kGroupKeyGroupTableAttributePath{ kRootEndpointId, GroupKeyManagement::Id, | |
| GroupKeyManagement::Attributes::GroupTable::Id }; | |
| context->provider.NotifyAttributeChanged(kGroupKeyGroupTableAttributePath, DataModel::AttributeChangeType::kReportable); | |
| } | |
| /** | |
| * @brief Checks if there are key set associated with the given GroupId | |
| */ | |
| bool KeyExists(GroupDataProvider & provider, FabricIndex fabricIndex, GroupId groupId) | |
| { | |
| AutoRelease it(provider.IterateGroupKeys(fabricIndex)); | |
| VerifyOrReturnValue(!it.IsNull(), false); | |
| GroupDataProvider::GroupKey key; | |
| while (it->Next(key)) | |
| { | |
| Credentials::GroupDataProvider::KeySet keys; | |
| if ((key.group_id == groupId) && (provider.GetKeySet(fabricIndex, key.keyset_id, keys) == CHIP_NO_ERROR)) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| struct GroupMembershipResponse | |
| { | |
| // A null capacity means that it is unknown if any further groups MAY be added. | |
| const DataModel::Nullable<uint8_t> kCapacityUnknown; | |
| // Use GetCommandId instead of commandId directly to avoid naming conflict with CommandIdentification in ExecutionOfACommand | |
| static constexpr CommandId GetCommandId() { return Commands::GetGroupMembershipResponse::Id; } | |
| static constexpr ClusterId GetClusterId() { return Groups::Id; } | |
| GroupMembershipResponse(const Commands::GetGroupMembership::DecodableType & data, EndpointId endpoint, | |
| GroupDataProvider::EndpointIterator * iter) : | |
| mCommandData(data), | |
| mEndpoint(endpoint), mIterator(iter) | |
| {} | |
| const Commands::GetGroupMembership::DecodableType & mCommandData; | |
| EndpointId mEndpoint = kInvalidEndpointId; | |
| GroupDataProvider::EndpointIterator * mIterator = nullptr; | |
| CHIP_ERROR Encode(TLV::TLVWriter & writer, TLV::Tag tag) const | |
| { | |
| TLV::TLVType outer; | |
| ReturnErrorOnFailure(writer.StartContainer(tag, TLV::kTLVType_Structure, outer)); | |
| ReturnErrorOnFailure( | |
| DataModel::Encode(writer, TLV::ContextTag(Commands::GetGroupMembershipResponse::Fields::kCapacity), kCapacityUnknown)); | |
| { | |
| TLV::TLVType type; | |
| ReturnErrorOnFailure(writer.StartContainer(TLV::ContextTag(Commands::GetGroupMembershipResponse::Fields::kGroupList), | |
| TLV::kTLVType_Array, type)); | |
| { | |
| GroupDataProvider::GroupEndpoint mapping; | |
| size_t requestedCount = 0; | |
| ReturnErrorOnFailure(mCommandData.groupList.ComputeSize(&requestedCount)); | |
| auto accept_group = [&](GroupId groupId) { | |
| // 1.3.6.3.1. If the GroupList field is empty, the entity SHALL respond with all group identifiers of which the | |
| // entity is a member. | |
| VerifyOrReturnValue(0 != requestedCount, true); | |
| auto iter = mCommandData.groupList.begin(); | |
| while (iter.Next()) | |
| { | |
| VerifyOrReturnValue(groupId != iter.GetValue(), true); | |
| } | |
| return false; | |
| }; | |
| while (mIterator && mIterator->Next(mapping)) | |
| { | |
| if (mapping.endpoint_id != mEndpoint) | |
| { | |
| continue; | |
| } | |
| if (!accept_group(mapping.group_id)) | |
| { | |
| continue; | |
| } | |
| ReturnErrorOnFailure(DataModel::Encode(writer, TLV::AnonymousTag(), mapping.group_id)); | |
| ChipLogDetail(Zcl, " 0x%02x", mapping.group_id); | |
| } | |
| ChipLogDetail(Zcl, "]"); | |
| } | |
| ReturnErrorOnFailure(writer.EndContainer(type)); | |
| } | |
| ReturnErrorOnFailure(writer.EndContainer(outer)); | |
| return CHIP_NO_ERROR; | |
| } | |
| }; | |
| } // namespace | |
| CHIP_ERROR GroupsClusterImpl::Attributes(const ConcreteClusterPath & path, | |
| ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder) | |
| { | |
| AttributeListBuilder listBuilder(builder); | |
| return listBuilder.Append(Attributes::kMandatoryMetadata, {}); | |
| } | |
| CHIP_ERROR GroupsClusterImpl::AcceptedCommands(const ConcreteClusterPath & path, | |
| ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> & builder) | |
| { | |
| using namespace Commands; | |
| static constexpr DataModel::AcceptedCommandEntry kAcceptedCommands[] = { | |
| AddGroup::kMetadataEntry, // | |
| ViewGroup::kMetadataEntry, // | |
| GetGroupMembership::kMetadataEntry, // | |
| RemoveGroup::kMetadataEntry, // | |
| RemoveAllGroups::kMetadataEntry, // | |
| AddGroupIfIdentifying::kMetadataEntry, // | |
| }; | |
| return builder.ReferenceExisting(kAcceptedCommands); | |
| } | |
| CHIP_ERROR GroupsClusterImpl::GeneratedCommands(const ConcreteClusterPath & path, ReadOnlyBufferBuilder<CommandId> & builder) | |
| { | |
| using namespace Commands; | |
| static constexpr CommandId kGeneratedCommands[] = { | |
| AddGroupResponse::Id, // | |
| ViewGroupResponse::Id, // | |
| GetGroupMembershipResponse::Id, // | |
| RemoveGroupResponse::Id, // | |
| }; | |
| return builder.ReferenceExisting(kGeneratedCommands); | |
| } | |
| DataModel::ActionReturnStatus GroupsClusterImpl::ReadAttribute(const DataModel::ReadAttributeRequest & request, | |
| AttributeValueEncoder & encoder) | |
| { | |
| using namespace Attributes; | |
| switch (request.path.mAttributeId) | |
| { | |
| case ClusterRevision::Id: { | |
| // Forcefully set the revision to the last Groups cluster revision with full command support (4). | |
| // With revision 5+, The groups cluster is soft deprecated in favor of the Groupcast cluster. | |
| // Group revision 5, is a stubbed implementation for backwards compatibility. See StubbedGroupsCluster.cpp. | |
| constexpr uint16_t klastLegacyGroupRevision = 4; | |
| return encoder.Encode(klastLegacyGroupRevision); | |
| } | |
| case FeatureMap::Id: | |
| // Group names is hardcoded (feature is M conformance in the spec) | |
| return encoder.Encode(Feature::kGroupNames); | |
| case NameSupport::Id: | |
| // According to the spec, kGroupNames must be set (M conformance in the spec) | |
| return encoder.Encode(NameSupportBitmap::kGroupNames); | |
| default: | |
| return Status::UnsupportedAttribute; | |
| } | |
| } | |
| std::optional<DataModel::ActionReturnStatus> GroupsClusterImpl::InvokeCommand(const DataModel::InvokeRequest & request, | |
| TLV::TLVReader & input_arguments, | |
| CommandHandler * handler) | |
| { | |
| using namespace Commands; | |
| const FabricIndex fabricIndex = request.GetAccessingFabricIndex(); | |
| switch (request.path.mCommandId) | |
| { | |
| case AddGroup::Id: { | |
| MATTER_TRACE_SCOPE("AddGroup", "Groups"); | |
| AddGroup::DecodableType request_data; | |
| ReturnErrorOnFailure(request_data.Decode(input_arguments, fabricIndex)); | |
| Groups::Commands::AddGroupResponse::Type response; | |
| response.groupID = request_data.groupID; | |
| response.status = to_underlying(AddGroup(request_data.groupID, request_data.groupName, request.subjectDescriptor)); | |
| handler->AddResponse(request.path, response); | |
| return std::nullopt; | |
| } | |
| case ViewGroup::Id: { | |
| MATTER_TRACE_SCOPE("ViewGroup", "Groups"); | |
| ViewGroup::DecodableType request_data; | |
| ReturnErrorOnFailure(request_data.Decode(input_arguments, fabricIndex)); | |
| const GroupId groupId = request_data.groupID; | |
| Credentials::GroupDataProvider::GroupInfo info; | |
| const Status status = [&]() { | |
| VerifyOrReturnError(IsValidGroupId(groupId), Status::ConstraintError); | |
| VerifyOrReturnError(mGroupDataProvider.HasEndpoint(fabricIndex, groupId, mPath.mEndpointId), Status::NotFound); | |
| if (mGroupDataProvider.GetGroupInfo(fabricIndex, groupId, info) != CHIP_NO_ERROR) | |
| { | |
| return Status::NotFound; | |
| } | |
| return Status::Success; | |
| }(); | |
| if (status != Status::Success) | |
| { | |
| info.name[0] = 0; | |
| } | |
| handler->AddResponse(request.path, | |
| ViewGroupResponse::Type{ | |
| .status = to_underlying(status), | |
| .groupID = request_data.groupID, | |
| .groupName = CharSpan(info.name, strnlen(info.name, GroupDataProvider::GroupInfo::kGroupNameMax)), | |
| }); | |
| return std::nullopt; | |
| } | |
| case GetGroupMembership::Id: { | |
| MATTER_TRACE_SCOPE("GetGroupMembership", "Groups"); | |
| GetGroupMembership::DecodableType request_data; | |
| ReturnErrorOnFailure(request_data.Decode(input_arguments, fabricIndex)); | |
| AutoRelease iter(mGroupDataProvider.IterateEndpoints(fabricIndex)); | |
| VerifyOrReturnError(!iter.IsNull(), Status::Failure); | |
| handler->AddResponse(request.path, GroupMembershipResponse(request_data, mPath.mEndpointId, &*iter)); | |
| return std::nullopt; | |
| } | |
| case RemoveGroup::Id: { | |
| MATTER_TRACE_SCOPE("RemoveGroup", "Groups"); | |
| RemoveGroup::DecodableType request_data; | |
| ReturnErrorOnFailure(request_data.Decode(input_arguments, fabricIndex)); | |
| const GroupId groupId = request_data.groupID; | |
| handler->AddResponse( | |
| request.path, | |
| Groups::Commands::RemoveGroupResponse::Type{ | |
| .status = to_underlying([&]() { | |
| VerifyOrReturnError(IsValidGroupId(groupId), Status::ConstraintError); | |
| VerifyOrReturnError(mGroupDataProvider.HasEndpoint(fabricIndex, groupId, mPath.mEndpointId), Status::NotFound); | |
| if (CHIP_ERROR err = mGroupDataProvider.RemoveEndpoint(fabricIndex, groupId, mPath.mEndpointId); | |
| err != CHIP_NO_ERROR) | |
| { | |
| ChipLogDetail(Zcl, "ERR: Failed to remove mapping (end:%d, group:0x%x), err:%" CHIP_ERROR_FORMAT, | |
| mPath.mEndpointId, groupId, err.Format()); | |
| return Status::NotFound; | |
| } | |
| if (mScenesIntegration != nullptr) | |
| { | |
| // If a group is removed the scenes associated with that group SHOULD be removed. | |
| LogIfFailure(mScenesIntegration->GroupWillBeRemoved(fabricIndex, groupId)); | |
| } | |
| NotifyGroupTableChanged(mContext); | |
| return Status::Success; | |
| }()), | |
| .groupID = request_data.groupID, | |
| }); | |
| if (mGroupDataProvider.ConsumeAuxAclNotificationNeeded()) | |
| { | |
| AccessControl::EmitAuxiliaryAccessUpdated(mContext->interactionContext.eventsGenerator, request.subjectDescriptor); | |
| } | |
| return std::nullopt; | |
| } | |
| case RemoveAllGroups::Id: { | |
| MATTER_TRACE_SCOPE("RemoveAllGroups", "Groups"); | |
| if (mScenesIntegration != nullptr) | |
| { | |
| AutoRelease iter(mGroupDataProvider.IterateEndpoints(fabricIndex)); | |
| GroupDataProvider::GroupEndpoint mapping; | |
| VerifyOrReturnError(!iter.IsNull(), Status::Failure); | |
| while (iter->Next(mapping)) | |
| { | |
| if (mPath.mEndpointId == mapping.endpoint_id) | |
| { | |
| LogIfFailure(mScenesIntegration->GroupWillBeRemoved(fabricIndex, mapping.group_id)); | |
| } | |
| } | |
| LogIfFailure(mScenesIntegration->GroupWillBeRemoved(fabricIndex, scenes::kGlobalSceneGroupId)); | |
| } | |
| LogIfFailure(mGroupDataProvider.RemoveEndpoint(fabricIndex, mPath.mEndpointId)); | |
| NotifyGroupTableChanged(mContext); | |
| if (mGroupDataProvider.ConsumeAuxAclNotificationNeeded()) | |
| { | |
| AccessControl::EmitAuxiliaryAccessUpdated(mContext->interactionContext.eventsGenerator, request.subjectDescriptor); | |
| } | |
| return Status::Success; | |
| } | |
| case AddGroupIfIdentifying::Id: { | |
| MATTER_TRACE_SCOPE("AddGroupIfIdentifying", "Groups"); | |
| AddGroupIfIdentifying::DecodableType request_data; | |
| ReturnErrorOnFailure(request_data.Decode(input_arguments, fabricIndex)); | |
| // skip with success if we are not identifying | |
| VerifyOrReturnValue((mIdentifyIntegration != nullptr) && mIdentifyIntegration->IsIdentifying(), Status::Success); | |
| // AddGroupIfIdentifying is response `Y` in the spec: we return the status (not a structure, as opposed to AddGroup) | |
| return AddGroup(request_data.groupID, request_data.groupName, request.subjectDescriptor); | |
| } | |
| default: | |
| return Status::UnsupportedCommand; | |
| } | |
| } | |
| Status GroupsClusterImpl::AddGroup(GroupId groupID, CharSpan groupName, const chip::Access::SubjectDescriptor & subjectDescriptor) | |
| { | |
| FabricIndex fabricIndex = subjectDescriptor.fabricIndex; | |
| VerifyOrReturnError(IsValidGroupId(groupID), Status::ConstraintError); | |
| VerifyOrReturnError(groupName.size() <= GroupDataProvider::GroupInfo::kGroupNameMax, Status::ConstraintError); | |
| VerifyOrReturnError(KeyExists(mGroupDataProvider, fabricIndex, groupID), Status::UnsupportedAccess); | |
| GroupDataProvider::GroupInfo info; | |
| CHIP_ERROR err = mGroupDataProvider.GetGroupInfo(fabricIndex, groupID, info); | |
| // For backwards compatibility with groupcast, overwriting the existing group info can cause issues with | |
| // other flags that exist for the group. For example, if a group is created through the groupcast cluster | |
| // with HasAuxiliaryAcl set to true, and then this legacy AddGroup() call is used on that existing group | |
| // id, the exisitng HasAuxiliaryAcl value should remain unchanged. Here, we can explicitly set the info that | |
| // is being updated when the group exists already. When the group doesn't exist, a new GroupInfo can be made. | |
| if (err == CHIP_NO_ERROR) | |
| { | |
| info.SetName(groupName); | |
| } | |
| else if (err == CHIP_ERROR_NOT_FOUND) | |
| { | |
| info = GroupDataProvider::GroupInfo(groupID, groupName); | |
| } | |
| else | |
| { | |
| return Status::Failure; | |
| } | |
| // Add/update entry in the GroupTable | |
| if (err = mGroupDataProvider.SetGroupInfo(fabricIndex, info); err != CHIP_NO_ERROR) | |
| { | |
| ChipLogDetail(Zcl, "ERR: Failed to add mapping (end:%d, group:0x%x), err:%" CHIP_ERROR_FORMAT, mPath.mEndpointId, groupID, | |
| err.Format()); | |
| return Status::ResourceExhausted; | |
| } | |
| if (err = mGroupDataProvider.AddEndpoint(fabricIndex, groupID, mPath.mEndpointId); err != CHIP_NO_ERROR) | |
| { | |
| ChipLogDetail(Zcl, "ERR: Failed to add mapping (end:%d, group:0x%x), err:%" CHIP_ERROR_FORMAT, mPath.mEndpointId, groupID, | |
| err.Format()); | |
| // best-effort undo the mapping we added above, to try to be a bit more atomic. | |
| LogIfFailure(mGroupDataProvider.RemoveGroupInfo(fabricIndex, groupID)); | |
| return Status::ResourceExhausted; | |
| } | |
| NotifyGroupTableChanged(mContext); | |
| if (mGroupDataProvider.ConsumeAuxAclNotificationNeeded()) | |
| { | |
| AccessControl::EmitAuxiliaryAccessUpdated(mContext->interactionContext.eventsGenerator, subjectDescriptor); | |
| } | |
| return Status::Success; | |
| } | |
| } // namespace chip::app::Clusters |