| /** |
| * |
| * Copyright (c) 2021-2025 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 <access/AccessConfig.h> |
| #if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS |
| #include <access/AccessRestrictionProvider.h> |
| #include <app/clusters/access-control-server/ArlEncoder.h> |
| #endif |
| |
| #include <app-common/zap-generated/cluster-objects.h> |
| #include <app/clusters/access-control-server/access-control-cluster.h> |
| #include <app/server-cluster/AttributeListBuilder.h> |
| #include <app/server-cluster/DefaultServerCluster.h> |
| #include <app/server/Server.h> |
| |
| #include <app/AttributeAccessInterface.h> |
| #include <app/AttributeAccessInterfaceRegistry.h> |
| #include <app/CommandHandler.h> |
| #include <app/ConcreteCommandPath.h> |
| #include <app/EventLogging.h> |
| #include <app/data-model/Encode.h> |
| #include <app/reporting/reporting.h> |
| #include <app/server/AclStorage.h> |
| #include <app/server/Server.h> |
| |
| #include <clusters/AccessControl/ClusterId.h> |
| #include <clusters/AccessControl/Metadata.h> |
| #include <lib/support/TypeTraits.h> |
| |
| using namespace chip; |
| using namespace chip::app; |
| using namespace chip::app::Clusters::AccessControl; |
| using namespace chip::app::Clusters::AccessControl::Commands; |
| using namespace chip::app::Clusters::AccessControl::Attributes; |
| using namespace chip::DeviceLayer; |
| using namespace chip::Access; |
| |
| using AclEvent = Events::AccessControlEntryChanged::Type; |
| |
| #if CHIP_CONFIG_ENABLE_ACL_EXTENSIONS |
| using ExtensionEvent = Events::AccessControlExtensionChanged::Type; |
| |
| // TODO(#13590): generated code doesn't automatically handle max length so do it manually |
| constexpr int kExtensionDataMaxLength = 128; |
| #endif // CHIP_CONFIG_ENABLE_ACL_EXTENSIONS |
| |
| #if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS |
| using ArlReviewEvent = Events::FabricRestrictionReviewUpdate::Type; |
| #endif // CHIP_CONFIG_USE_ACCESS_RESTRICTIONS |
| |
| namespace { |
| CHIP_ERROR ReadAcl(FabricTable & fabricTable, Access::AccessControl & accessControl, AttributeValueEncoder & aEncoder) |
| { |
| AccessControl::EntryIterator iterator; |
| AccessControl::Entry entry; |
| AclStorage::EncodableEntry encodableEntry(entry); |
| return aEncoder.EncodeList([&](const auto & encoder) -> CHIP_ERROR { |
| for (auto & info : fabricTable) |
| { |
| auto fabric = info.GetFabricIndex(); |
| ReturnErrorOnFailure(accessControl.Entries(fabric, iterator)); |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| while ((err = iterator.Next(entry)) == CHIP_NO_ERROR) |
| { |
| ReturnErrorOnFailure(encoder.Encode(encodableEntry)); |
| } |
| VerifyOrReturnError(err == CHIP_NO_ERROR || err == CHIP_ERROR_SENTINEL, err); |
| } |
| return CHIP_NO_ERROR; |
| }); |
| } |
| |
| CHIP_ERROR IsValidAclEntryList(const DataModel::DecodableList<AclStorage::DecodableEntry> & list) |
| { |
| auto validationIterator = list.begin(); |
| while (validationIterator.Next()) |
| { |
| VerifyOrReturnError(validationIterator.GetValue().GetEntry().IsValid(), CHIP_ERROR_INVALID_ARGUMENT); |
| } |
| |
| return validationIterator.GetStatus(); |
| } |
| |
| CHIP_ERROR WriteAcl(Access::AccessControl & accessControl, const ConcreteDataAttributePath & aPath, |
| AttributeValueDecoder & aDecoder) |
| { |
| FabricIndex accessingFabricIndex = aDecoder.AccessingFabricIndex(); |
| |
| size_t oldCount; |
| ReturnErrorOnFailure(accessControl.GetEntryCount(accessingFabricIndex, oldCount)); |
| size_t maxCount; |
| ReturnErrorOnFailure(accessControl.GetMaxEntriesPerFabric(maxCount)); |
| |
| if (!aPath.IsListItemOperation()) |
| { |
| DataModel::DecodableList<AclStorage::DecodableEntry> list; |
| ReturnErrorOnFailure(aDecoder.Decode(list)); |
| |
| size_t newCount; |
| ReturnErrorOnFailure(list.ComputeSize(&newCount)); |
| |
| VerifyOrReturnError(newCount <= maxCount, CHIP_IM_GLOBAL_STATUS(ResourceExhausted)); |
| |
| // Validating all ACL entries in the ReplaceAll list before Updating or Deleting any entries. If any of the entries has an |
| // invalid field, the whole "ReplaceAll" list will be rejected. |
| ReturnErrorOnFailure(IsValidAclEntryList(list)); |
| |
| auto iterator = list.begin(); |
| size_t i = 0; |
| while (iterator.Next()) |
| { |
| if (i < oldCount) |
| { |
| ReturnErrorOnFailure(accessControl.UpdateEntry(&aDecoder.GetSubjectDescriptor(), accessingFabricIndex, i, |
| iterator.GetValue().GetEntry())); |
| } |
| else |
| { |
| ReturnErrorOnFailure(accessControl.CreateEntry(&aDecoder.GetSubjectDescriptor(), accessingFabricIndex, nullptr, |
| iterator.GetValue().GetEntry())); |
| } |
| ++i; |
| } |
| ReturnErrorOnFailure(iterator.GetStatus()); |
| |
| while (i < oldCount) |
| { |
| --oldCount; |
| ReturnErrorOnFailure(accessControl.DeleteEntry(&aDecoder.GetSubjectDescriptor(), accessingFabricIndex, oldCount)); |
| } |
| return CHIP_NO_ERROR; |
| } |
| |
| if (aPath.mListOp == ConcreteDataAttributePath::ListOperation::AppendItem) |
| { |
| VerifyOrReturnError((oldCount + 1) <= maxCount, CHIP_IM_GLOBAL_STATUS(ResourceExhausted)); |
| |
| AclStorage::DecodableEntry decodableEntry; |
| ReturnErrorOnFailure(aDecoder.Decode(decodableEntry)); |
| |
| return accessControl.CreateEntry(&aDecoder.GetSubjectDescriptor(), accessingFabricIndex, nullptr, |
| decodableEntry.GetEntry()); |
| } |
| |
| return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE; |
| } |
| |
| #if CHIP_CONFIG_ENABLE_ACL_EXTENSIONS |
| CHIP_ERROR ReadExtension(PersistentStorageDelegate & storage, FabricTable & fabrics, AttributeValueEncoder & aEncoder) |
| { |
| return aEncoder.EncodeList([&](const auto & encoder) -> CHIP_ERROR { |
| for (auto & fabric : fabrics) |
| { |
| uint8_t buffer[kExtensionDataMaxLength] = { 0 }; |
| uint16_t size = static_cast<uint16_t>(sizeof(buffer)); |
| CHIP_ERROR errStorage = storage.SyncGetKeyValue( |
| DefaultStorageKeyAllocator::AccessControlExtensionEntry(fabric.GetFabricIndex()).KeyName(), buffer, size); |
| VerifyOrReturnError(errStorage != CHIP_ERROR_BUFFER_TOO_SMALL, CHIP_ERROR_INCORRECT_STATE); |
| if (errStorage == CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND) |
| { |
| continue; |
| } |
| ReturnErrorOnFailure(errStorage); |
| Structs::AccessControlExtensionStruct::Type item = { |
| .data = ByteSpan(buffer, size), |
| .fabricIndex = fabric.GetFabricIndex(), |
| }; |
| ReturnErrorOnFailure(encoder.Encode(item)); |
| } |
| return CHIP_NO_ERROR; |
| }); |
| } |
| |
| CHIP_ERROR |
| GenerateExtensionChangedEvent(const Structs::AccessControlExtensionStruct::Type & item, |
| const Access::SubjectDescriptor & subjectDescriptor, ChangeTypeEnum changeType, |
| ServerClusterContext * context) |
| { |
| ExtensionEvent event{ .changeType = changeType, .fabricIndex = subjectDescriptor.fabricIndex }; |
| |
| if (subjectDescriptor.authMode == Access::AuthMode::kCase) |
| { |
| event.adminNodeID.SetNonNull(subjectDescriptor.subject); |
| } |
| else if (subjectDescriptor.authMode == Access::AuthMode::kPase) |
| { |
| event.adminPasscodeID.SetNonNull(PAKEKeyIdFromNodeId(subjectDescriptor.subject)); |
| } |
| |
| event.latestValue.SetNonNull(item); |
| |
| return context->interactionContext.eventsGenerator.GenerateEvent(event, kRootEndpointId).has_value() ? CHIP_NO_ERROR |
| : CHIP_ERROR_INTERNAL; |
| } |
| |
| CHIP_ERROR CheckExtensionEntryDataFormat(const ByteSpan & data) |
| { |
| CHIP_ERROR err; |
| |
| TLV::TLVReader reader; |
| reader.Init(data); |
| |
| auto containerType = TLV::kTLVType_List; |
| err = reader.Next(containerType, TLV::AnonymousTag()); |
| VerifyOrReturnError(err == CHIP_NO_ERROR, CHIP_IM_GLOBAL_STATUS(ConstraintError)); |
| |
| err = reader.EnterContainer(containerType); |
| VerifyOrReturnError(err == CHIP_NO_ERROR, CHIP_IM_GLOBAL_STATUS(ConstraintError)); |
| |
| while ((err = reader.Next()) == CHIP_NO_ERROR) |
| { |
| VerifyOrReturnError(TLV::IsProfileTag(reader.GetTag()), CHIP_IM_GLOBAL_STATUS(ConstraintError)); |
| } |
| VerifyOrReturnError(err == CHIP_END_OF_TLV, CHIP_IM_GLOBAL_STATUS(ConstraintError)); |
| |
| err = reader.ExitContainer(containerType); |
| VerifyOrReturnError(err == CHIP_NO_ERROR, CHIP_IM_GLOBAL_STATUS(ConstraintError)); |
| |
| err = reader.Next(); |
| VerifyOrReturnError(err == CHIP_END_OF_TLV, CHIP_IM_GLOBAL_STATUS(ConstraintError)); |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| CHIP_ERROR WriteExtension(PersistentStorageDelegate & storage, const ConcreteDataAttributePath & aPath, |
| AttributeValueDecoder & aDecoder, ServerClusterContext * context) |
| { |
| FabricIndex accessingFabricIndex = aDecoder.AccessingFabricIndex(); |
| |
| uint8_t buffer[kExtensionDataMaxLength] = { 0 }; |
| uint16_t size = static_cast<uint16_t>(sizeof(buffer)); |
| CHIP_ERROR errStorage = storage.SyncGetKeyValue( |
| DefaultStorageKeyAllocator::AccessControlExtensionEntry(accessingFabricIndex).KeyName(), buffer, size); |
| |
| // Every operation MUST generate an event. eventChangeType and eventItem will be set by the |
| // processing logic and an event will be generated at the final step. |
| ChangeTypeEnum eventChangeType; |
| Structs::AccessControlExtensionStruct::Type eventItem; |
| |
| VerifyOrReturnError(errStorage != CHIP_ERROR_BUFFER_TOO_SMALL, CHIP_ERROR_INCORRECT_STATE); |
| VerifyOrReturnError(errStorage == CHIP_NO_ERROR || errStorage == CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND, errStorage); |
| |
| if (!aPath.IsListItemOperation()) |
| { |
| DataModel::DecodableList<Structs::AccessControlExtensionStruct::DecodableType> list; |
| ReturnErrorOnFailure(aDecoder.Decode(list)); |
| |
| size_t count = 0; |
| ReturnErrorOnFailure(list.ComputeSize(&count)); |
| |
| if (count == 0) |
| { |
| VerifyOrReturnError(errStorage != CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND, CHIP_NO_ERROR); |
| ReturnErrorOnFailure(storage.SyncDeleteKeyValue( |
| DefaultStorageKeyAllocator::AccessControlExtensionEntry(accessingFabricIndex).KeyName())); |
| eventItem = { |
| .data = ByteSpan(buffer, size), |
| .fabricIndex = accessingFabricIndex, |
| }; |
| eventChangeType = ChangeTypeEnum::kRemoved; |
| } |
| else if (count == 1) |
| { |
| auto iterator = list.begin(); |
| if (!iterator.Next()) |
| { |
| ReturnErrorOnFailure(iterator.GetStatus()); |
| // If counted an item, iterator doesn't return it, iterator has no error, that's bad. |
| return CHIP_ERROR_INCORRECT_STATE; |
| } |
| auto & item = iterator.GetValue(); |
| // TODO(#13590): generated code doesn't automatically handle max length so do it manually |
| VerifyOrReturnError(item.data.size() <= kExtensionDataMaxLength, CHIP_IM_GLOBAL_STATUS(ConstraintError)); |
| |
| ReturnErrorOnFailure(CheckExtensionEntryDataFormat(item.data)); |
| |
| ReturnErrorOnFailure( |
| storage.SyncSetKeyValue(DefaultStorageKeyAllocator::AccessControlExtensionEntry(accessingFabricIndex).KeyName(), |
| item.data.data(), static_cast<uint16_t>(item.data.size()))); |
| eventItem = item; |
| eventChangeType = |
| errStorage == CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND ? ChangeTypeEnum::kAdded : ChangeTypeEnum::kChanged; |
| } |
| else |
| { |
| return CHIP_IM_GLOBAL_STATUS(ConstraintError); |
| } |
| } |
| else if (aPath.mListOp == ConcreteDataAttributePath::ListOperation::AppendItem) |
| { |
| VerifyOrReturnError(errStorage == CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND, CHIP_IM_GLOBAL_STATUS(ConstraintError)); |
| Structs::AccessControlExtensionStruct::DecodableType item; |
| ReturnErrorOnFailure(aDecoder.Decode(item)); |
| // TODO(#13590): generated code doesn't automatically handle max length so do it manually |
| VerifyOrReturnError(item.data.size() <= kExtensionDataMaxLength, CHIP_IM_GLOBAL_STATUS(ConstraintError)); |
| |
| ReturnErrorOnFailure(CheckExtensionEntryDataFormat(item.data)); |
| |
| ReturnErrorOnFailure( |
| storage.SyncSetKeyValue(DefaultStorageKeyAllocator::AccessControlExtensionEntry(accessingFabricIndex).KeyName(), |
| item.data.data(), static_cast<uint16_t>(item.data.size()))); |
| eventItem = item; |
| eventChangeType = ChangeTypeEnum::kAdded; |
| } |
| else |
| { |
| return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE; |
| } |
| |
| return GenerateExtensionChangedEvent(eventItem, aDecoder.GetSubjectDescriptor(), eventChangeType, context); |
| } |
| #endif |
| |
| #if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS |
| CHIP_ERROR ReadCommissioningArl(Access::AccessControl & accessControl, AttributeValueEncoder & aEncoder) |
| { |
| auto accessRestrictionProvider = accessControl.GetAccessRestrictionProvider(); |
| |
| return aEncoder.EncodeList([&](const auto & encoder) -> CHIP_ERROR { |
| if (accessRestrictionProvider != nullptr) |
| { |
| auto entries = accessRestrictionProvider->GetCommissioningEntries(); |
| |
| for (auto & entry : entries) |
| { |
| ArlEncoder::CommissioningEncodableEntry encodableEntry(entry); |
| ReturnErrorOnFailure(encoder.Encode(encodableEntry)); |
| } |
| } |
| return CHIP_NO_ERROR; |
| }); |
| } |
| |
| CHIP_ERROR ReadArl(Access::AccessControl & accessControl, FabricTable & fabricTable, AttributeValueEncoder & aEncoder) |
| { |
| auto accessRestrictionProvider = accessControl.GetAccessRestrictionProvider(); |
| |
| return aEncoder.EncodeList([&](const auto & encoder) -> CHIP_ERROR { |
| if (accessRestrictionProvider != nullptr) |
| { |
| for (const auto & info : fabricTable) |
| { |
| auto fabric = info.GetFabricIndex(); |
| // get entries for fabric |
| std::vector<AccessRestrictionProvider::Entry> entries; |
| ReturnErrorOnFailure(accessRestrictionProvider->GetEntries(fabric, entries)); |
| for (const auto & entry : entries) |
| { |
| ArlEncoder::EncodableEntry encodableEntry(entry); |
| ReturnErrorOnFailure(encoder.Encode(encodableEntry)); |
| } |
| } |
| } |
| return CHIP_NO_ERROR; |
| }); |
| } |
| #endif |
| |
| CHIP_ERROR ChipErrorToImErrorMap(CHIP_ERROR err) |
| { |
| // Map some common errors into an underlying IM error |
| // Separate logging is done to not lose the original error location in case such |
| // this are available. |
| Protocols::InteractionModel::Status statusOfErr; |
| |
| if (err == CHIP_ERROR_INVALID_ARGUMENT || err == CHIP_ERROR_NOT_FOUND) |
| { |
| // Not found is generally also illegal argument: caused a lookup into an invalid location, |
| // like invalid subjects or targets. |
| statusOfErr = Protocols::InteractionModel::Status::ConstraintError; |
| } |
| else if (err == CHIP_ERROR_NO_MEMORY) |
| { |
| statusOfErr = Protocols::InteractionModel::Status::ResourceExhausted; |
| } |
| else |
| { |
| return err; |
| } |
| |
| ChipLogError(DataManagement, "Mapped %" CHIP_ERROR_FORMAT " into " ChipLogFormatIMStatus, err.Format(), |
| ChipLogValueIMStatus(statusOfErr)); |
| |
| return CHIP_ERROR_IM_GLOBAL_STATUS_VALUE(statusOfErr); |
| } |
| |
| CHIP_ERROR WriteImpl(Access::AccessControl & accessControl, PersistentStorageDelegate & storage, |
| const DataModel::WriteAttributeRequest & request, AttributeValueDecoder & decoder, |
| ServerClusterContext * context) |
| { |
| switch (request.path.mAttributeId) |
| { |
| case Acl::Id: |
| return WriteAcl(accessControl, request.path, decoder); |
| #if CHIP_CONFIG_ENABLE_ACL_EXTENSIONS |
| case Extension::Id: |
| return WriteExtension(storage, request.path, decoder, context); |
| #endif |
| default: |
| return CHIP_IM_GLOBAL_STATUS(UnsupportedWrite); |
| } |
| } |
| } // namespace |
| |
| namespace chip { |
| namespace app { |
| namespace Clusters { |
| |
| CHIP_ERROR AccessControlCluster::Startup(ServerClusterContext & context) |
| { |
| ChipLogProgress(DataManagement, "AccessControlCluster: initializing"); |
| ReturnErrorOnFailure(DefaultServerCluster::Startup(context)); |
| |
| mClusterContext.accessControl.AddEntryListener(*this); |
| |
| #if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS |
| auto accessRestrictionProvider = mClusterContext.accessControl.GetAccessRestrictionProvider(); |
| if (accessRestrictionProvider != nullptr) |
| { |
| accessRestrictionProvider->AddListener(*this); |
| } |
| #endif |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| void AccessControlCluster::Shutdown(ClusterShutdownType shutdownType) |
| { |
| ChipLogProgress(DataManagement, "AccessControlCluster: shutdown"); |
| |
| #if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS |
| auto accessRestrictionProvider = mClusterContext.accessControl.GetAccessRestrictionProvider(); |
| if (accessRestrictionProvider != nullptr) |
| { |
| accessRestrictionProvider->RemoveListener(*this); |
| } |
| #endif |
| |
| mClusterContext.accessControl.RemoveEntryListener(*this); |
| DefaultServerCluster::Shutdown(shutdownType); |
| } |
| |
| DataModel::ActionReturnStatus AccessControlCluster::ReadAttribute(const DataModel::ReadAttributeRequest & request, |
| AttributeValueEncoder & encoder) |
| { |
| // in many cases attributes are numeric. Do a fallback to handle the general case |
| // This is to save some flash: |
| // - value is considered a uint32_value and encoded as such |
| // - valueFetchError is the underlying CHIP_ERROR that we get when fetching `value` |
| size_t value = 0; |
| CHIP_ERROR valueFetchError = CHIP_NO_ERROR; |
| const Access::AccessControl & ctrl = mClusterContext.accessControl; |
| |
| switch (request.path.mAttributeId) |
| { |
| case AccessControl::Attributes::Acl::Id: |
| return ReadAcl(mClusterContext.fabricTable, mClusterContext.accessControl, encoder); |
| #if CHIP_CONFIG_ENABLE_ACL_EXTENSIONS |
| case AccessControl::Attributes::Extension::Id: |
| return ReadExtension(mClusterContext.persistentStorage, mClusterContext.fabricTable, encoder); |
| #endif // CHIP_CONFIG_ENABLE_ACL_EXTENSIONS |
| #if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS |
| case AccessControl::Attributes::CommissioningARL::Id: |
| return ReadCommissioningArl(mClusterContext.accessControl, encoder); |
| case AccessControl::Attributes::Arl::Id: |
| return ReadArl(mClusterContext.accessControl, mClusterContext.fabricTable, encoder); |
| #endif |
| case AccessControl::Attributes::SubjectsPerAccessControlEntry::Id: |
| valueFetchError = ctrl.GetMaxSubjectsPerEntry(value); |
| break; |
| case AccessControl::Attributes::TargetsPerAccessControlEntry::Id: |
| valueFetchError = ctrl.GetMaxTargetsPerEntry(value); |
| break; |
| case AccessControl::Attributes::AccessControlEntriesPerFabric::Id: |
| valueFetchError = ctrl.GetMaxEntriesPerFabric(value); |
| break; |
| case AccessControl::Attributes::FeatureMap::Id: { |
| value = 0; |
| |
| #if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS |
| value |= to_underlying(Clusters::AccessControl::Feature::kManagedDevice); |
| #endif // CHIP_CONFIG_USE_ACCESS_RESTRICTIONS |
| |
| #if CHIP_CONFIG_ENABLE_ACL_EXTENSIONS |
| value |= to_underlying(Clusters::AccessControl::Feature::kExtension); |
| #endif // CHIP_CONFIG_ENABLE_ACL_EXTENSIONS |
| |
| break; |
| } |
| case AccessControl::Attributes::ClusterRevision::Id: |
| value = kRevision; |
| break; |
| default: |
| valueFetchError = CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute); |
| break; |
| } |
| |
| ReturnErrorOnFailure(valueFetchError); |
| return encoder.Encode(static_cast<uint32_t>(value)); |
| } |
| |
| DataModel::ActionReturnStatus AccessControlCluster::WriteAttribute(const DataModel::WriteAttributeRequest & request, |
| AttributeValueDecoder & decoder) |
| { |
| return NotifyAttributeChangedIfSuccess( |
| request.path.mAttributeId, |
| ChipErrorToImErrorMap( |
| WriteImpl(mClusterContext.accessControl, mClusterContext.persistentStorage, request, decoder, mContext))); |
| } |
| |
| CHIP_ERROR AccessControlCluster::Attributes(const ConcreteClusterPath & path, |
| ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder) |
| { |
| AttributeListBuilder listBuilder(builder); |
| |
| #if CHIP_CONFIG_ENABLE_ACL_EXTENSIONS || CHIP_CONFIG_USE_ACCESS_RESTRICTIONS |
| AttributeListBuilder::OptionalAttributeEntry kOptionalAttributes[] = { |
| #if CHIP_CONFIG_ENABLE_ACL_EXTENSIONS |
| { .enabled = true, .metadata = Attributes::Extension::kMetadataEntry }, |
| #endif |
| #if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS |
| { .enabled = true, .metadata = Attributes::CommissioningARL::kMetadataEntry }, |
| { .enabled = true, .metadata = Attributes::Arl::kMetadataEntry }, |
| #endif |
| }; |
| |
| return listBuilder.Append(Span(AccessControl::Attributes::kMandatoryMetadata), Span(kOptionalAttributes)); |
| #else |
| return listBuilder.Append(Span(AccessControl::Attributes::kMandatoryMetadata), {}); |
| #endif |
| } |
| |
| CHIP_ERROR AccessControlCluster::EventInfo(const ConcreteEventPath & path, DataModel::EventEntry & eventInfo) |
| { |
| eventInfo.readPrivilege = Access::Privilege::kAdminister; |
| return CHIP_NO_ERROR; |
| } |
| |
| #if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS |
| |
| CHIP_ERROR AccessControlCluster::AcceptedCommands(const ConcreteClusterPath & path, |
| ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> & builder) |
| { |
| static constexpr DataModel::AcceptedCommandEntry kAcceptedCommands[] = { |
| Commands::ReviewFabricRestrictions::kMetadataEntry, |
| }; |
| return builder.ReferenceExisting(kAcceptedCommands); |
| } |
| |
| CHIP_ERROR AccessControlCluster::GeneratedCommands(const ConcreteClusterPath & path, ReadOnlyBufferBuilder<CommandId> & builder) |
| { |
| static constexpr CommandId kGeneratedCommands[] = { |
| Commands::ReviewFabricRestrictionsResponse::Id, |
| }; |
| return builder.ReferenceExisting(kGeneratedCommands); |
| } |
| |
| std::optional<DataModel::ActionReturnStatus> AccessControlCluster::InvokeCommand(const DataModel::InvokeRequest & request, |
| TLV::TLVReader & input_arguments, |
| CommandHandler * handler) |
| { |
| switch (request.path.mCommandId) |
| { |
| case AccessControl::Commands::ReviewFabricRestrictions::Id: { |
| AccessControl::Commands::ReviewFabricRestrictions::DecodableType data; |
| ReturnErrorOnFailure(data.Decode(input_arguments, handler->GetAccessingFabricIndex())); |
| return HandleReviewFabricRestrictions(handler, request.path, data); |
| } |
| default: |
| return Protocols::InteractionModel::Status::UnsupportedCommand; |
| } |
| } |
| |
| std::optional<DataModel::ActionReturnStatus> AccessControlCluster::HandleReviewFabricRestrictions( |
| CommandHandler * commandObj, const ConcreteCommandPath & commandPath, |
| const Clusters::AccessControl::Commands::ReviewFabricRestrictions::DecodableType & commandData) |
| { |
| if (commandPath.mEndpointId != kRootEndpointId) |
| { |
| ChipLogError(DataManagement, "AccessControlCluster: invalid endpoint in ReviewFabricRestrictions request"); |
| return Protocols::InteractionModel::Status::InvalidCommand; |
| } |
| |
| uint64_t token; |
| std::vector<AccessRestrictionProvider::Entry> entries; |
| auto entryIter = commandData.arl.begin(); |
| while (entryIter.Next()) |
| { |
| AccessRestrictionProvider::Entry entry; |
| entry.fabricIndex = commandObj->GetAccessingFabricIndex(); |
| entry.endpointNumber = entryIter.GetValue().endpoint; |
| entry.clusterId = entryIter.GetValue().cluster; |
| |
| auto restrictionIter = entryIter.GetValue().restrictions.begin(); |
| while (restrictionIter.Next()) |
| { |
| AccessRestrictionProvider::Restriction restriction; |
| if (ArlEncoder::Convert(restrictionIter.GetValue().type, restriction.restrictionType) != CHIP_NO_ERROR) |
| { |
| ChipLogError(DataManagement, "AccessControlCluster: invalid restriction type conversion"); |
| return Protocols::InteractionModel::Status::InvalidCommand; |
| } |
| |
| if (!restrictionIter.GetValue().id.IsNull()) |
| { |
| restriction.id.SetValue(restrictionIter.GetValue().id.Value()); |
| } |
| entry.restrictions.push_back(restriction); |
| } |
| |
| if (restrictionIter.GetStatus() != CHIP_NO_ERROR) |
| { |
| ChipLogError(DataManagement, "AccessControlCluster: invalid ARL data"); |
| return Protocols::InteractionModel::Status::InvalidCommand; |
| } |
| |
| entries.push_back(entry); |
| } |
| |
| if (entryIter.GetStatus() != CHIP_NO_ERROR) |
| { |
| ChipLogError(DataManagement, "AccessControlCluster: invalid ARL data"); |
| return Protocols::InteractionModel::Status::InvalidCommand; |
| } |
| |
| CHIP_ERROR err = mClusterContext.accessControl.GetAccessRestrictionProvider()->RequestFabricRestrictionReview( |
| commandObj->GetAccessingFabricIndex(), entries, token); |
| |
| if (err != CHIP_NO_ERROR) |
| { |
| ChipLogError(DataManagement, "AccessControlCluster: restriction review failed: %" CHIP_ERROR_FORMAT, err.Format()); |
| return Protocols::InteractionModel::ClusterStatusCode(err); |
| } |
| |
| Clusters::AccessControl::Commands::ReviewFabricRestrictionsResponse::Type response; |
| response.token = token; |
| commandObj->AddResponse(commandPath, response); |
| return std::nullopt; |
| } |
| |
| void AccessControlCluster::MarkCommissioningRestrictionListChanged() |
| { |
| NotifyAttributeChanged(AccessControl::Attributes::CommissioningARL::Id); |
| } |
| |
| void AccessControlCluster::MarkRestrictionListChanged(FabricIndex fabricIndex) |
| { |
| NotifyAttributeChanged(AccessControl::Attributes::Arl::Id); |
| } |
| |
| void AccessControlCluster::OnFabricRestrictionReviewUpdate(FabricIndex fabricIndex, uint64_t token, Optional<CharSpan> instruction, |
| Optional<CharSpan> arlRequestFlowUrl) |
| { |
| ArlReviewEvent event{ .token = token, .fabricIndex = fabricIndex }; |
| |
| event.instruction = instruction; |
| event.ARLRequestFlowUrl = arlRequestFlowUrl; |
| |
| mContext->interactionContext.eventsGenerator.GenerateEvent(event, kRootEndpointId); |
| } |
| |
| #endif // CHIP_CONFIG_USE_ACCESS_RESTRICTIONS |
| |
| void AccessControlCluster::OnEntryChanged(const SubjectDescriptor * subjectDescriptor, FabricIndex fabric, size_t index, |
| const Access::AccessControl::Entry * entry, |
| Access::AccessControl::EntryListener::ChangeType changeType) |
| { |
| // NOTE: If the entry was changed internally by the system (e.g. creating |
| // entries at startup from persistent storage, or deleting entries when a |
| // fabric is removed), then there won't be a subject descriptor, and also |
| // it won't be appropriate to create an event. |
| VerifyOrReturn((mContext != nullptr) && (subjectDescriptor != nullptr)); |
| AclEvent event{ .changeType = ChangeTypeEnum::kChanged, .fabricIndex = subjectDescriptor->fabricIndex }; |
| |
| if (changeType == Access::AccessControl::EntryListener::ChangeType::kAdded) |
| { |
| event.changeType = ChangeTypeEnum::kAdded; |
| } |
| else if (changeType == Access::AccessControl::EntryListener::ChangeType::kRemoved) |
| { |
| event.changeType = ChangeTypeEnum::kRemoved; |
| } |
| |
| if (subjectDescriptor->authMode == Access::AuthMode::kCase) |
| { |
| event.adminNodeID.SetNonNull(subjectDescriptor->subject); |
| } |
| else if (subjectDescriptor->authMode == Access::AuthMode::kPase) |
| { |
| event.adminPasscodeID.SetNonNull(PAKEKeyIdFromNodeId(subjectDescriptor->subject)); |
| } |
| |
| if (entry != nullptr) |
| { |
| AclStorage::EncodableEntry encodableEntry(*entry); |
| CHIP_ERROR err = encodableEntry.Stage(); |
| if (err != CHIP_NO_ERROR) |
| { |
| ChipLogError(DataManagement, "AccessControlCluster: event failed %" CHIP_ERROR_FORMAT, err.Format()); |
| return; |
| } |
| |
| event.latestValue.SetNonNull(encodableEntry.GetStagingEntry()); |
| // NOTE: EncodableEntry can only be constructed from ref so we need to use it within the right scope |
| // after we determined the entry is not null. This is why we repeat the generate event call. |
| mContext->interactionContext.eventsGenerator.GenerateEvent(event, kRootEndpointId); |
| return; |
| } |
| |
| mContext->interactionContext.eventsGenerator.GenerateEvent(event, kRootEndpointId); |
| } |
| |
| } // namespace Clusters |
| } // namespace app |
| } // namespace chip |