From 5501b22a092cbcfc0e3f01a2daf3f76e4200ebcd Mon Sep 17 00:00:00 2001 From: Thomas Lea Date: Tue, 30 Jul 2024 10:32:57 -0500 Subject: [PATCH] Add AccessRestrictionList support --- examples/network-manager-app/linux/args.gni | 1 + examples/platform/linux/AppMain.cpp | 18 + examples/platform/linux/BUILD.gn | 2 + .../platform/linux/ExampleAccessRestriction.h | 66 +++ examples/platform/linux/Options.cpp | 60 +++ examples/platform/linux/Options.h | 8 + src/access/AccessConfig.h | 22 + src/access/AccessControl.cpp | 19 +- src/access/AccessControl.h | 19 + src/access/AccessRestriction.cpp | 268 ++++++++++ src/access/AccessRestriction.h | 288 +++++++++++ src/access/BUILD.gn | 28 ++ src/access/access.gni | 18 + src/access/tests/BUILD.gn | 5 + src/access/tests/TestAccessRestriction.cpp | 463 ++++++++++++++++++ src/app/CommandHandlerImpl.cpp | 1 + .../access-control-server.cpp | 215 +++++++- src/app/server/ArlStorage.cpp | 158 ++++++ src/app/server/ArlStorage.h | 148 ++++++ src/app/server/BUILD.gn | 10 + src/app/server/DefaultArlStorage.cpp | 203 ++++++++ src/app/server/DefaultArlStorage.h | 37 ++ src/app/server/Server.cpp | 11 + src/app/server/Server.h | 13 + src/lib/core/CHIPConfig.h | 21 + src/lib/support/DefaultStorageKeyAllocator.h | 9 + 26 files changed, 2104 insertions(+), 7 deletions(-) create mode 100644 examples/platform/linux/ExampleAccessRestriction.h create mode 100644 src/access/AccessConfig.h create mode 100644 src/access/AccessRestriction.cpp create mode 100644 src/access/AccessRestriction.h create mode 100644 src/access/access.gni create mode 100644 src/access/tests/TestAccessRestriction.cpp create mode 100644 src/app/server/ArlStorage.cpp create mode 100644 src/app/server/ArlStorage.h create mode 100644 src/app/server/DefaultArlStorage.cpp create mode 100644 src/app/server/DefaultArlStorage.h diff --git a/examples/network-manager-app/linux/args.gni b/examples/network-manager-app/linux/args.gni index 53bb53d2b1b2a1..97661ce32a8bf7 100644 --- a/examples/network-manager-app/linux/args.gni +++ b/examples/network-manager-app/linux/args.gni @@ -22,3 +22,4 @@ chip_project_config_include_dirs = [ ] chip_config_network_layer_ble = false +chip_enable_access_restrictions = true diff --git a/examples/platform/linux/AppMain.cpp b/examples/platform/linux/AppMain.cpp index 307b3428126db2..a8d1fa0792519d 100644 --- a/examples/platform/linux/AppMain.cpp +++ b/examples/platform/linux/AppMain.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -103,6 +104,10 @@ #include "AppMain.h" #include "CommissionableInit.h" +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS +#include "ExampleAccessRestriction.h" +#endif + #if CHIP_DEVICE_LAYER_TARGET_DARWIN #include #if CHIP_DEVICE_CONFIG_ENABLE_WIFI @@ -121,6 +126,7 @@ using namespace chip::DeviceLayer; using namespace chip::Inet; using namespace chip::Transport; using namespace chip::app::Clusters; +using namespace chip::Access; // Network comissioning implementation namespace { @@ -593,6 +599,18 @@ void ChipLinuxAppMainLoop(AppMainLoopImplementation * impl) chip::app::RuntimeOptionsProvider::Instance().SetSimulateNoInternalTime( LinuxDeviceOptions::GetInstance().mSimulateNoInternalTime); +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS + if (LinuxDeviceOptions::GetInstance().accessRestrictionEntries.HasValue()) + { + initParams.accessRestriction = new ExampleAccessRestriction(); + initParams.arlStorage = new app::DefaultArlStorage(); + for (const auto & entry : LinuxDeviceOptions::GetInstance().accessRestrictionEntries.Value()) + { + VerifyOrDie(AccessRestriction::CreateCommissioningEntry(entry) == CHIP_NO_ERROR); + } + } +#endif + // Init ZCL Data Model and CHIP App Server Server::GetInstance().Init(initParams); diff --git a/examples/platform/linux/BUILD.gn b/examples/platform/linux/BUILD.gn index 1fcee183f131b3..b37e73f6debb9c 100644 --- a/examples/platform/linux/BUILD.gn +++ b/examples/platform/linux/BUILD.gn @@ -13,6 +13,7 @@ # limitations under the License. import("//build_overrides/chip.gni") +import("//build_overrides/jsoncpp.gni") import("${chip_root}/examples/common/pigweed/pigweed_rpcs.gni") import("${chip_root}/src/app/common_flags.gni") import("${chip_root}/src/lib/core/core.gni") @@ -94,6 +95,7 @@ source_set("app-main") { "${chip_root}/src/controller:gen_check_chip_controller_headers", "${chip_root}/src/lib", "${chip_root}/src/platform/logging:default", + jsoncpp_root, ] deps = [ ":ota-test-event-trigger", diff --git a/examples/platform/linux/ExampleAccessRestriction.h b/examples/platform/linux/ExampleAccessRestriction.h new file mode 100644 index 00000000000000..bd1a7f29375593 --- /dev/null +++ b/examples/platform/linux/ExampleAccessRestriction.h @@ -0,0 +1,66 @@ +/* + * + * Copyright (c) 2024 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. + */ + +/* + * AccessRestriction implementation for Linux examples. + */ + +#pragma once + +#include + +namespace chip { +namespace Access { + +class ExampleAccessRestriction : public AccessRestriction +{ +public: + class ExampleAccessRestrictionEntryListener : public AccessRestriction::EntryListener + { + void OnEntryChanged(FabricIndex fabricIndex, size_t index, SharedPtr entry, + ChangeType changeType) override + { + ChipLogProgress(NotSpecified, "AccessRestriction Entry changed: fabricIndex %d, index %ld, changeType %d", fabricIndex, + index, static_cast(changeType)); + } + + void OnFabricRestrictionReviewUpdate(FabricIndex fabricIndex, uint64_t token, const char * instruction, + const char * redirectUrl) override + {} + }; + + ExampleAccessRestriction() : AccessRestriction() { AccessRestriction::AddListener(listener); } + + ~ExampleAccessRestriction() { AccessRestriction::RemoveListener(listener); } + +protected: + CHIP_ERROR DoRequestFabricRestrictionReview(const FabricIndex fabricIndex, uint64_t token, const std::vector & arl) + { + // this example simply removes all restrictions + while (Access::GetAccessControl().GetAccessRestriction()->DeleteEntry(0, fabricIndex) == CHIP_NO_ERROR) + ; + + return CHIP_NO_ERROR; + } + +private: + ExampleAccessRestrictionEntryListener listener; +}; + +} // namespace Access +} // namespace chip diff --git a/examples/platform/linux/Options.cpp b/examples/platform/linux/Options.cpp index 9b83d126c1f495..dc5fe55e5be4b9 100644 --- a/examples/platform/linux/Options.cpp +++ b/examples/platform/linux/Options.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -47,6 +48,11 @@ using namespace chip; using namespace chip::ArgParser; +using namespace chip::Platform; + +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS +using namespace chip::Access; +#endif namespace { LinuxDeviceOptions gDeviceOptions; @@ -82,6 +88,9 @@ enum kDeviceOption_TraceFile, kDeviceOption_TraceLog, kDeviceOption_TraceDecode, +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS + kDeviceOption_UseAccessRestrictions, +#endif kOptionCSRResponseCSRIncorrectType, kOptionCSRResponseCSRNonceIncorrectType, kOptionCSRResponseCSRNonceTooLong, @@ -154,6 +163,9 @@ OptionDef sDeviceOptionDefs[] = { { "trace_log", kArgumentRequired, kDeviceOption_TraceLog }, { "trace_decode", kArgumentRequired, kDeviceOption_TraceDecode }, #endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS + { "enable-access-restrictions", kArgumentRequired, kDeviceOption_UseAccessRestrictions }, +#endif // CHIP_CONFIG_USE_ACCESS_RESTRICTIONS { "cert_error_csr_incorrect_type", kNoArgument, kOptionCSRResponseCSRIncorrectType }, { "cert_error_csr_existing_keypair", kNoArgument, kOptionCSRResponseCSRExistingKeyPair }, { "cert_error_csr_nonce_incorrect_type", kNoArgument, kOptionCSRResponseCSRNonceIncorrectType }, @@ -280,6 +292,9 @@ const char * sDeviceOptionHelp = " --trace_decode <1/0>\n" " A value of 1 enables traces decoding, 0 disables this (default 0).\n" #endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED + " --enable-access-restrictions \n" + " Enable ACL cluster access restrictions with the provided JSON CommissioningARL. Example:\n" + " \"[{\\\"endpoint\\\": 1,\\\"cluster\\\": 2,\\\"restrictions\\\": [{\\\"type\\\": 0,\\\"id\\\": 3}]}]\"\n" " --cert_error_csr_incorrect_type\n" " Configure the CSRResponse to be built with an invalid CSR type.\n" " --cert_error_csr_existing_keypair\n" @@ -320,6 +335,39 @@ const char * sDeviceOptionHelp = #endif "\n"; +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS +bool ParseAccessRestrictionEntriesFromJson(const char * jsonString, std::vector> & entries) +{ + Json::Value root; + Json::Reader reader; + VerifyOrReturnValue(reader.parse(jsonString, root), false); + + for (Json::Value::const_iterator eIt = root.begin(); eIt != root.end(); eIt++) + { + auto entry = MakeShared(); + + entry->endpointNumber = (*eIt)["endpoint"].asInt(); + entry->clusterId = (*eIt)["cluster"].asInt(); + + Json::Value restrictions = (*eIt)["restrictions"]; + for (Json::Value::const_iterator rIt = restrictions.begin(); rIt != restrictions.end(); rIt++) + { + AccessRestriction::Restriction restriction; + restriction.restrictionType = static_cast((*rIt)["type"].asInt()); + if ((*rIt).isMember("id")) + { + restriction.id.SetValue((*rIt)["id"].asInt()); + } + entry->restrictions.push_back(restriction); + } + + entries.push_back(entry); + } + + return true; +} +#endif // CHIP_CONFIG_USE_ACCESS_RESTRICTIONS + bool Base64ArgToVector(const char * arg, size_t maxSize, std::vector & outVector) { size_t maxBase64Size = BASE64_ENCODED_LEN(maxSize); @@ -529,6 +577,18 @@ bool HandleOption(const char * aProgram, OptionSet * aOptions, int aIdentifier, break; #endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS + case kDeviceOption_UseAccessRestrictions: { + std::vector> accessRestrictionEntries; + retval = ParseAccessRestrictionEntriesFromJson(aValue, accessRestrictionEntries); + if (retval) + { + LinuxDeviceOptions::GetInstance().accessRestrictionEntries.SetValue(std::move(accessRestrictionEntries)); + } + } + break; +#endif // CHIP_CONFIG_USE_ACCESS_RESTRICTIONS + case kOptionCSRResponseCSRIncorrectType: LinuxDeviceOptions::GetInstance().mCSRResponseOptions.csrIncorrectType = true; break; diff --git a/examples/platform/linux/Options.h b/examples/platform/linux/Options.h index f921bee4ced554..895037664232c3 100644 --- a/examples/platform/linux/Options.h +++ b/examples/platform/linux/Options.h @@ -28,6 +28,7 @@ #include #include +#include #include #include #include @@ -38,6 +39,10 @@ #include #include +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS +#include +#endif + struct LinuxDeviceOptions { chip::PayloadContents payload; @@ -81,6 +86,9 @@ struct LinuxDeviceOptions #if CONFIG_BUILD_FOR_HOST_UNIT_TEST int32_t subscriptionCapacity = CHIP_IM_MAX_NUM_SUBSCRIPTIONS; int32_t subscriptionResumptionRetryIntervalSec = -1; +#endif +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS + chip::Optional>> accessRestrictionEntries; #endif static LinuxDeviceOptions & GetInstance(); }; diff --git a/src/access/AccessConfig.h b/src/access/AccessConfig.h new file mode 100644 index 00000000000000..c4cb6f51f3114d --- /dev/null +++ b/src/access/AccessConfig.h @@ -0,0 +1,22 @@ +/* + * + * Copyright (c) 2020-2024 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. + */ + +#pragma once + +#if CHIP_HAVE_CONFIG_H +#include +#endif diff --git a/src/access/AccessControl.cpp b/src/access/AccessControl.cpp index fcb5a43d975f8e..6d519f9a227adf 100644 --- a/src/access/AccessControl.cpp +++ b/src/access/AccessControl.cpp @@ -325,7 +325,11 @@ void AccessControl::RemoveEntryListener(EntryListener & listener) bool AccessControl::IsAccessRestrictionListSupported() const { - return false; // not yet supported +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS + return mAccessRestriction != nullptr; +#else + return false; +#endif } CHIP_ERROR AccessControl::Check(const SubjectDescriptor & subjectDescriptor, const RequestPath & requestPath, @@ -352,6 +356,19 @@ CHIP_ERROR AccessControl::Check(const SubjectDescriptor & subjectDescriptor, con VerifyOrReturnError(requestPath.requestType != RequestType::kRequestTypeUnknown, CHIP_ERROR_INVALID_ARGUMENT); } +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS + if (mAccessRestriction != nullptr) + { + CHIP_ERROR result = mAccessRestriction->Check(subjectDescriptor, requestPath); + if (result != CHIP_NO_ERROR) + { + ChipLogProgress(DataManagement, "AccessControl: %s", + (result == CHIP_ERROR_ACCESS_DENIED) ? "denied (restricted)" : "denied (restriction error)"); + return result; + } + } +#endif + { CHIP_ERROR result = mDelegate->Check(subjectDescriptor, requestPath, requestPrivilege); if (result != CHIP_ERROR_NOT_IMPLEMENTED) diff --git a/src/access/AccessControl.h b/src/access/AccessControl.h index a7c3472f5d99b4..6f2f29803f9cdf 100644 --- a/src/access/AccessControl.h +++ b/src/access/AccessControl.h @@ -18,6 +18,12 @@ #pragma once +#include + +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS +#include "AccessRestriction.h" +#endif + #include "Privilege.h" #include "RequestPath.h" #include "SubjectDescriptor.h" @@ -627,6 +633,13 @@ class AccessControl // Removes a listener from the listener list, if in the list. void RemoveEntryListener(EntryListener & listener); +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS + // Set an optional AcceessRestriction object for MNGD feature. + void SetAccessRestriction(AccessRestriction * accessRestriction) { mAccessRestriction = accessRestriction; } + + AccessRestriction * GetAccessRestriction() { return mAccessRestriction; } +#endif + /** * Check whether or not Access Restriction List is supported. * @@ -638,6 +651,8 @@ class AccessControl * Check whether access (by a subject descriptor, to a request path, * requiring a privilege) should be allowed or denied. * + * If an AccessRestriction object is set, it will be checked for additional access restrictions. + * * @retval #CHIP_ERROR_ACCESS_DENIED if denied. * @retval other errors should also be treated as denied. * @retval #CHIP_NO_ERROR if allowed. @@ -662,6 +677,10 @@ class AccessControl DeviceTypeResolver * mDeviceTypeResolver = nullptr; EntryListener * mEntryListener = nullptr; + +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS + AccessRestriction * mAccessRestriction; +#endif }; /** diff --git a/src/access/AccessRestriction.cpp b/src/access/AccessRestriction.cpp new file mode 100644 index 00000000000000..5fe8474803d56c --- /dev/null +++ b/src/access/AccessRestriction.cpp @@ -0,0 +1,268 @@ +/* + * + * Copyright (c) 2024 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 "AccessRestriction.h" + +#include +#include + +namespace chip { +namespace Access { + +std::vector> AccessRestriction::sCommissioningEntries; + +CHIP_ERROR AccessRestriction::CreateFabricEntries(const FabricIndex fabricIndex) +{ + for (auto & entry : sCommissioningEntries) + { + CreateEntry(nullptr, *entry, fabricIndex); + } + + return CHIP_NO_ERROR; +} + +void AccessRestriction::AddListener(EntryListener & listener) +{ + if (mListeners == nullptr) + { + mListeners = &listener; + listener.mNext = nullptr; + return; + } + + for (EntryListener * l = mListeners; /**/; l = l->mNext) + { + if (l == &listener) + { + return; + } + + if (l->mNext == nullptr) + { + l->mNext = &listener; + listener.mNext = nullptr; + return; + } + } +} + +void AccessRestriction::RemoveListener(EntryListener & listener) +{ + if (mListeners == &listener) + { + mListeners = listener.mNext; + listener.mNext = nullptr; + return; + } + + for (EntryListener * l = mListeners; l != nullptr; l = l->mNext) + { + if (l->mNext == &listener) + { + l->mNext = listener.mNext; + listener.mNext = nullptr; + return; + } + } +} + +SharedPtr AccessRestriction::GetEntry(FabricIndex fabricIndex, size_t index) +{ + if (mFabricEntries.find(fabricIndex) != mFabricEntries.end() && index < mFabricEntries[fabricIndex].size()) + { + return mFabricEntries[fabricIndex][index]; + } + + return nullptr; +} + +CHIP_ERROR AccessRestriction::CreateCommissioningEntry(SharedPtr entry) +{ + if (!entry->IsValid()) + { + return CHIP_ERROR_INVALID_ARGUMENT; + } + + sCommissioningEntries.push_back(entry); + return CHIP_NO_ERROR; +} + +CHIP_ERROR AccessRestriction::CreateEntry(size_t * index, const Entry & entry, FabricIndex fabricIndex) +{ + if (!entry.IsValid()) + { + return CHIP_ERROR_INVALID_ARGUMENT; + } + + auto localEntry = MakeShared(entry); + size_t newIndex; + + localEntry->fabricIndex = fabricIndex; + + if (mFabricEntries.find(fabricIndex) == mFabricEntries.end()) + { + mFabricEntries[fabricIndex] = std::vector>(); + mFabricEntries[fabricIndex].push_back(localEntry); + } + else + { + mFabricEntries[fabricIndex].push_back(localEntry); + } + + newIndex = mFabricEntries[fabricIndex].size() - 1; + + for (EntryListener * listener = mListeners; listener != nullptr; listener = listener->mNext) + { + listener->OnEntryChanged(fabricIndex, newIndex, localEntry, EntryListener::ChangeType::kAdded); + } + + if (index != nullptr) + { + *index = newIndex; + } + + return CHIP_NO_ERROR; +} + +CHIP_ERROR AccessRestriction::DeleteEntry(size_t index, const FabricIndex fabricIndex) +{ + auto entry = GetEntry(fabricIndex, index); + if (entry == nullptr) + { + return CHIP_ERROR_NOT_FOUND; + } + else + { + mFabricEntries[fabricIndex].erase(mFabricEntries[fabricIndex].begin() + index); + + for (EntryListener * listener = mListeners; listener != nullptr; listener = listener->mNext) + { + listener->OnEntryChanged(fabricIndex, index, entry, EntryListener::ChangeType::kRemoved); + } + } + + return CHIP_NO_ERROR; +} + +CHIP_ERROR AccessRestriction::UpdateEntry(size_t index, const Entry & entry, const FabricIndex fabricIndex) +{ + auto localEntry = GetEntry(fabricIndex, index); + if (localEntry != nullptr) + { + *localEntry = entry; + + for (EntryListener * listener = mListeners; listener != nullptr; listener = listener->mNext) + { + listener->OnEntryChanged(fabricIndex, index, localEntry, EntryListener::ChangeType::kUpdated); + } + + return CHIP_NO_ERROR; + } + + return CHIP_ERROR_NOT_FOUND; +} + +CHIP_ERROR AccessRestriction::Check(const SubjectDescriptor & subjectDescriptor, const RequestPath & requestPath) +{ + ChipLogProgress(DataManagement, "AccessRestriction: action %d", static_cast(requestPath.requestType)); + + if (requestPath.requestType == RequestType::kRequestTypeUnknown) + { + ChipLogError(DataManagement, "AccessRestriction: RequestPath type is unknown"); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + // wildcard event subscriptions are allowed. All other requests require an entity id + if (!requestPath.entityId.has_value()) + { + if (requestPath.requestType == RequestType::kEventReadOrSubscribeRequest) + { + return CHIP_NO_ERROR; + } + else + { + return CHIP_ERROR_INVALID_ARGUMENT; + } + } + + for (auto & entry : mFabricEntries[subjectDescriptor.fabricIndex]) + { + if (entry->endpointNumber != requestPath.endpoint || entry->clusterId != requestPath.cluster) + { + continue; + } + + for (auto & restriction : entry->restrictions) + { + // a missing id is a wildcard + bool idMatch = !restriction.id.HasValue() || restriction.id.Value() == requestPath.entityId.value(); + if (!idMatch) + { + continue; + } + + switch (restriction.restrictionType) + { + case Type::kAttributeAccessForbidden: + if (requestPath.requestType == RequestType::kAttributeReadRequest || + requestPath.requestType == RequestType::kAttributeWriteRequest) + { + return CHIP_ERROR_ACCESS_DENIED; + } + break; + case Type::kAttributeWriteForbidden: + if (requestPath.requestType == RequestType::kAttributeWriteRequest) + { + return CHIP_ERROR_ACCESS_DENIED; + } + break; + case Type::kCommandForbidden: + if (requestPath.requestType == RequestType::kCommandInvokeRequest) + { + return CHIP_ERROR_ACCESS_DENIED; + } + break; + case Type::kEventForbidden: + if (requestPath.requestType == RequestType::kEventReadOrSubscribeRequest) + { + return CHIP_ERROR_ACCESS_DENIED; + } + break; + } + } + } + + return CHIP_NO_ERROR; +} + +void AccessRestriction::ClearData() +{ + // iterate over mFabricEntries and call DeleteEntry for each entry + for (auto & fabricEntry : mFabricEntries) + { + for (size_t i = 0; i < fabricEntry.second.size(); i++) + { + DeleteEntry(i, fabricEntry.first); + } + } + + sCommissioningEntries.clear(); +} + +} // namespace Access +} // namespace chip diff --git a/src/access/AccessRestriction.h b/src/access/AccessRestriction.h new file mode 100644 index 00000000000000..f610677db39e9b --- /dev/null +++ b/src/access/AccessRestriction.h @@ -0,0 +1,288 @@ +/* + * + * Copyright (c) 2024 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. + */ + +#pragma once + +#include "RequestPath.h" +#include "SubjectDescriptor.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace chip { +namespace Access { + +using namespace chip::Protocols::InteractionModel; +using namespace chip::Platform; + +class AccessRestriction +{ +public: + static constexpr size_t kNumberOfFabrics = CHIP_CONFIG_MAX_FABRICS; + static constexpr size_t kEntriesPerFabric = CHIP_CONFIG_ACCESS_RESTRICTION_MAX_ENTRIES_PER_FABRIC; + static constexpr size_t kRestrictionsPerEntry = CHIP_CONFIG_ACCESS_RESTRICTION_MAX_RESTRICTIONS_PER_ENTRY; + + /** + * Defines the type of access restriction, which is used to determine the meaning of the restriction's id. + */ + enum class Type : uint8_t + { + kAttributeAccessForbidden = 0, + kAttributeWriteForbidden = 1, + kCommandForbidden = 2, + kEventForbidden = 3 + }; + + /** + * Defines a single restriction on an attribute, command, or event. + * + * If id is not set, the restriction applies to all attributes, commands, or events of the given type (wildcard). + */ + struct Restriction + { + Type restrictionType; + Optional id; + }; + + /** + * Defines a single entry in the access restriction list, which contains a list of restrictions + * for a cluster on an endpoint. + */ + struct Entry + { + FabricIndex fabricIndex; + EndpointId endpointNumber; + ClusterId clusterId; + std::vector restrictions; + + bool IsValid() const + { + return endpointNumber != 0 && clusterId != app::Clusters::NetworkCommissioning::Id && + clusterId != app::Clusters::Descriptor::Id; + } + }; + + /** + * Used to notify of changes in the access restriction list and active reviews. + */ + class EntryListener + { + public: + enum class ChangeType + { + kAdded = 1, + kRemoved = 2, + kUpdated = 3 + }; + + virtual ~EntryListener() = default; + + /** + * Notifies of a change in the access restriction list. + * + * @param [in] fabricIndex The index of the fabric in which the entry has changed. + * @param [in] index Index of entry to which has changed (relative to fabric). + * @param [in] entry The latest value of the entry which changed. + * @param [in] changeType The type of change that occurred. + */ + virtual void OnEntryChanged(FabricIndex fabricIndex, size_t index, SharedPtr entry, ChangeType changeType) = 0; + + /** + * Notifies of an update to an active review with instructions and an optional redirect URL. + * + * @param [in] fabricIndex The index of the fabric in which the entry has changed. + * @param [in] token The token of the review being updated (obtained from ReviewFabricRestrictionsResponse) + * @param [in] instruction The instructions to be displayed to the user. + * @param [in] redirectUrl An optional URL to redirect the user to for more information. May be null. + */ + virtual void OnFabricRestrictionReviewUpdate(FabricIndex fabricIndex, uint64_t token, const char * instruction, + const char * redirectUrl) = 0; + + private: + EntryListener * mNext = nullptr; + + friend class AccessRestriction; + }; + + AccessRestriction() = default; + virtual ~AccessRestriction() = default; + + AccessRestriction(const AccessRestriction &) = delete; + AccessRestriction & operator=(const AccessRestriction &) = delete; + + /** + * Create restriction entries for a fabric by populating from the commissioning entries. + * This should be called when the device is commissioned to a new fabric. + * + * @param [in] fabricIndex The index of the fabric for which to create entries. + */ + CHIP_ERROR CreateFabricEntries(const FabricIndex fabricIndex); + + /** + * Add a listener to be notified of changes in the access restriction list and active reviews. + * + * @param [in] listener The listener to add. + */ + void AddListener(EntryListener & listener); + + /** + * Remove a listener from being notified of changes in the access restriction list and active reviews. + * + * @param [in] listener The listener to remove. + */ + void RemoveListener(EntryListener & listener); + + /** + * Check whether access by a subject descriptor to a request path should be restricted (denied) for the given action. + * These restrictions are are only a part of overall access evaluation. + * + * If access is not restricted, CHIP_NO_ERROR will be returned. + * + * @retval #CHIP_ERROR_ACCESS_DENIED if access is denied. + * @retval other errors should also be treated as restricted/denied. + * @retval #CHIP_NO_ERROR if access is not restricted/denied. + */ + CHIP_ERROR Check(const SubjectDescriptor & subjectDescriptor, const RequestPath & requestPath); + + /** + * Request a review of the access restrictions for a fabric. + * + * @param [in] fabricIndex The index of the fabric requesting a review. + * @param [in] arl An optinal list of access restriction entries to review. If null, all entries will be reviewed. + * @param [out] token The unique token for the review, which can be matched to a review update event. + */ + CHIP_ERROR RequestFabricRestrictionReview(FabricIndex fabricIndex, const std::vector & arl, uint64_t & token) + { + token = ++mNextToken; + return DoRequestFabricRestrictionReview(fabricIndex, token, arl); + } + + using EntryIterator = std::vector>::iterator; + + /** + * Get iterator over the commissioning entries, which are used during commissioning + * and also installed as defaults for a fabric upon completion of commissioning. + * + * @param [out] begin iterator pointing to the beginning of the entries + * @param [out] end iterator pointing to the end of the entries + */ + static CHIP_ERROR CommissioningEntries(EntryIterator & begin, EntryIterator & end) + { + begin = sCommissioningEntries.begin(); + end = sCommissioningEntries.end(); + + return CHIP_NO_ERROR; + } + + /** + * Get iterator over entries in the access restriction list by fabric. + * + * @param [in] fabricIndex Iteration is confined to fabric + * @param [out] begin iterator pointing to the beginning of the entries + * @param [out] end iterator pointing to the end of the entries + */ + CHIP_ERROR Entries(const FabricIndex fabricIndex, EntryIterator & begin, EntryIterator & end) + { + if (mFabricEntries.find(fabricIndex) == mFabricEntries.end()) + { + return CHIP_ERROR_NOT_FOUND; + } + + begin = mFabricEntries[fabricIndex].begin(); + end = mFabricEntries[fabricIndex].end(); + + return CHIP_NO_ERROR; + } + + /** + * Add a restriction entry to the commissioning access restriction list. This list is automatically + * applied to fabrics upon completion of commissioning. This request will fail if there is already + * an entry with the same cluster and endpoint. + * + * @param [in] entry The entry to add to the commissioning access restriction list. + * @return CHIP_NO_ERROR if the entry was successfully created, or an error code. + */ + static CHIP_ERROR CreateCommissioningEntry(SharedPtr entry); + + /** + * Add a restriction entry to the access restriction list and notify any listeners. This request + * will fail if there is already an entry with the same cluster and endpoint. + * + * @param [out] index (If not nullptr) index of created entry (relative to fabric). + * @param [in] entry The entry to add to the access restriction list. + * @param [in] fabricIndex The index of the fabric in which the entry should be added. + * @return CHIP_NO_ERROR if the entry was successfully created, or an error code. + */ + CHIP_ERROR CreateEntry(size_t * index, const Entry & entry, const FabricIndex fabricIndex); + + /** + * Delete a restriction entry from the access restriction list and notify any listeners. + * + * @param [in] index Index of entry to delete. May be relative to fabric. + * @param [in] fabricIndex Fabric to which entry `index` is relative. + * @return CHIP_NO_ERROR if the entry was successfully deleted, or an error code. + */ + CHIP_ERROR DeleteEntry(size_t index, const FabricIndex fabricIndex); + + /** + * Update a restriction entry in the access restriction list and notify any listeners. + * + * @param [in] index Index of entry to delete. May be relative to fabric. + * @param [in] entry The updated entry to replace the existing entry. + * @param [in] fabricIndex The index of the fabric in which the entry should be updated. + * @return CHIP_NO_ERROR if the entry was successfully updated, or an error code. + */ + CHIP_ERROR UpdateEntry(size_t index, const Entry & entry, const FabricIndex fabricIndex); + +protected: + /** + * Initiate a review of the access restrictions for a fabric. This method should be implemented by the platform and be + * non-blocking. + * + * @param [in] fabricIndex The index of the fabric requesting a review. + * @param [in] token The unique token for the review, which can be matched to a review update event. + * @param [in] arl An optinal list of access restriction entries to review. If null, all entries will be reviewed. + * @return CHIP_NO_ERROR if the review was successfully requested, or an error code if the request failed. + */ + virtual CHIP_ERROR DoRequestFabricRestrictionReview(const FabricIndex fabricIndex, uint64_t token, + const std::vector & arl) = 0; + + /** + * Clear all access restriction data. + */ + void ClearData(); + +private: + SharedPtr GetEntry(FabricIndex fabricIndex, size_t index); + + static std::vector> sCommissioningEntries; + uint64_t mNextToken = 1; + EntryListener * mListeners = nullptr; + std::map>> mFabricEntries; +}; + +} // namespace Access +} // namespace chip diff --git a/src/access/BUILD.gn b/src/access/BUILD.gn index 8d2c4b504975ee..ae2a64d70906f8 100644 --- a/src/access/BUILD.gn +++ b/src/access/BUILD.gn @@ -13,6 +13,25 @@ # limitations under the License. import("//build_overrides/chip.gni") +import("${chip_root}/build/chip/buildconfig_header.gni") +import("${chip_root}/src/access/access.gni") + +buildconfig_header("access_buildconfig") { + header = "AccessBuildConfig.h" + header_dir = "access" + + defines = [ + "CHIP_CONFIG_USE_ACCESS_RESTRICTIONS=${chip_enable_access_restrictions}", + ] + + visibility = [ ":access_config" ] +} + +source_set("access_config") { + sources = [ "AccessConfig.h" ] + + deps = [ ":access_buildconfig" ] +} source_set("types") { sources = [ @@ -23,6 +42,7 @@ source_set("types") { ] public_deps = [ + ":access_config", "${chip_root}/src/lib/core", "${chip_root}/src/lib/core:types", ] @@ -43,10 +63,18 @@ static_library("access") { cflags = [ "-Wconversion" ] public_deps = [ + ":access_config", ":types", "${chip_root}/src/lib/core", "${chip_root}/src/lib/core:types", "${chip_root}/src/lib/support", "${chip_root}/src/platform", ] + + if (chip_enable_access_restrictions) { + sources += [ + "AccessRestriction.cpp", + "AccessRestriction.h", + ] + } } diff --git a/src/access/access.gni b/src/access/access.gni new file mode 100644 index 00000000000000..bd18f1387b66b9 --- /dev/null +++ b/src/access/access.gni @@ -0,0 +1,18 @@ +# Copyright (c) 2024 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. + +declare_args() { + # Enable ARL features of Access Control + chip_enable_access_restrictions = false +} diff --git a/src/access/tests/BUILD.gn b/src/access/tests/BUILD.gn index d8b43e6a17a01d..c790ad4066be73 100644 --- a/src/access/tests/BUILD.gn +++ b/src/access/tests/BUILD.gn @@ -15,6 +15,7 @@ import("//build_overrides/build.gni") import("//build_overrides/chip.gni") import("//build_overrides/pigweed.gni") +import("${chip_root}/src/access/access.gni") import("${chip_root}/build/chip/chip_test_suite.gni") @@ -29,4 +30,8 @@ chip_test_suite("tests") { "${chip_root}/src/lib/support:test_utils", "${dir_pw_unit_test}", ] + + if (chip_enable_access_restrictions) { + test_sources += [ "TestAccessRestriction.cpp" ] + } } diff --git a/src/access/tests/TestAccessRestriction.cpp b/src/access/tests/TestAccessRestriction.cpp new file mode 100644 index 00000000000000..3a3db93be3f640 --- /dev/null +++ b/src/access/tests/TestAccessRestriction.cpp @@ -0,0 +1,463 @@ +/* + * + * Copyright (c) 2024 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 "access/AccessControl.h" +#include "access/AccessRestriction.h" +#include "access/examples/ExampleAccessControlDelegate.h" + +#include + +#include +#include + +namespace chip { +namespace Access { + +class TestAccessRestrictionImpl : public AccessRestriction +{ + CHIP_ERROR DoRequestFabricRestrictionReview(const FabricIndex fabricIndex, uint64_t token, const std::vector & arl) + { + return CHIP_NO_ERROR; + } + +public: + void Clear() { ClearData(); } +}; + +AccessControl accessControl; +TestAccessRestrictionImpl accessRestriction; + +constexpr ClusterId kNetworkCommissioningCluster = 0x0000'0031; // must not be blocked by access restrictions on any endpoint +constexpr ClusterId kDescriptorCluster = 0x0000'001d; // must not be blocked by access restrictions on any endpoint +constexpr ClusterId kOnOffCluster = 0x0000'0006; + +constexpr NodeId kOperationalNodeId1 = 0x1111111111111111; +constexpr NodeId kOperationalNodeId2 = 0x2222222222222222; +constexpr NodeId kOperationalNodeId3 = 0x3333333333333333; + +struct AclEntryData +{ + FabricIndex fabricIndex = kUndefinedFabricIndex; + Privilege privilege = Privilege::kView; + AuthMode authMode = AuthMode::kNone; + NodeId subject; +}; + +constexpr AclEntryData aclEntryData[] = { + { + .fabricIndex = 1, + .privilege = Privilege::kAdminister, + .authMode = AuthMode::kCase, + .subject = kOperationalNodeId1, + }, + { + .fabricIndex = 2, + .privilege = Privilege::kAdminister, + .authMode = AuthMode::kCase, + .subject = kOperationalNodeId2, + }, +}; +constexpr size_t aclEntryDataCount = ArraySize(aclEntryData); + +struct CheckData +{ + SubjectDescriptor subjectDescriptor; + RequestPath requestPath; + Privilege privilege; + bool allow; +}; + +constexpr CheckData checkDataNoRestrictions[] = { + // Checks for implicit PASE + { .subjectDescriptor = { .fabricIndex = 0, .authMode = AuthMode::kPase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = 1, .endpoint = 1, .requestType = RequestType::kAttributeReadRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 0, .authMode = AuthMode::kPase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = 1, .endpoint = 1, .requestType = RequestType::kAttributeWriteRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kPase, .subject = kOperationalNodeId2 }, + .requestPath = { .cluster = 1, .endpoint = 1, .requestType = RequestType::kCommandInvokeRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 2, .authMode = AuthMode::kPase, .subject = kOperationalNodeId3 }, + .requestPath = { .cluster = 1, .endpoint = 1, .requestType = RequestType::kEventReadOrSubscribeRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, + // Checks for entry 0 + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = 1, .endpoint = 1, .requestType = RequestType::kAttributeReadRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = 1, .endpoint = 1, .requestType = RequestType::kAttributeWriteRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = 1, .endpoint = 1, .requestType = RequestType::kCommandInvokeRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = 1, .endpoint = 1, .requestType = RequestType::kEventReadOrSubscribeRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, + // Checks for entry 1 + { .subjectDescriptor = { .fabricIndex = 2, .authMode = AuthMode::kCase, .subject = kOperationalNodeId2 }, + .requestPath = { .cluster = 1, .endpoint = 1, .requestType = RequestType::kAttributeReadRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 2, .authMode = AuthMode::kCase, .subject = kOperationalNodeId2 }, + .requestPath = { .cluster = 1, .endpoint = 1, .requestType = RequestType::kAttributeWriteRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 2, .authMode = AuthMode::kCase, .subject = kOperationalNodeId2 }, + .requestPath = { .cluster = 1, .endpoint = 1, .requestType = RequestType::kCommandInvokeRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 2, .authMode = AuthMode::kCase, .subject = kOperationalNodeId2 }, + .requestPath = { .cluster = 1, .endpoint = 1, .requestType = RequestType::kEventReadOrSubscribeRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, +}; + +CHIP_ERROR LoadEntry(AccessControl::Entry & entry, const AclEntryData & entryData) +{ + ReturnErrorOnFailure(entry.SetAuthMode(entryData.authMode)); + ReturnErrorOnFailure(entry.SetFabricIndex(entryData.fabricIndex)); + ReturnErrorOnFailure(entry.SetPrivilege(entryData.privilege)); + ReturnErrorOnFailure(entry.AddSubject(nullptr, entryData.subject)); + return CHIP_NO_ERROR; +} + +CHIP_ERROR LoadAccessControl(AccessControl & ac, const AclEntryData * entryData, size_t count) +{ + AccessControl::Entry entry; + for (size_t i = 0; i < count; ++i, ++entryData) + { + ReturnErrorOnFailure(ac.PrepareEntry(entry)); + ReturnErrorOnFailure(LoadEntry(entry, *entryData)); + ReturnErrorOnFailure(ac.CreateEntry(nullptr, entry)); + } + return CHIP_NO_ERROR; +} + +void RunChecks(const CheckData * checkData, size_t count) +{ + for (size_t i = 0; i < count; i++) + { + CHIP_ERROR expectedResult = checkData[i].allow ? CHIP_NO_ERROR : CHIP_ERROR_ACCESS_DENIED; + EXPECT_EQ(accessControl.Check(checkData[i].subjectDescriptor, checkData[i].requestPath, checkData[i].privilege), + expectedResult); + } +} + +class DeviceTypeResolver : public AccessControl::DeviceTypeResolver +{ +public: + bool IsDeviceTypeOnEndpoint(DeviceTypeId deviceType, EndpointId endpoint) override { return false; } +} testDeviceTypeResolver; + +class TestAccessRestriction : public ::testing::Test +{ +public: // protected + void SetUp() override { accessRestriction.Clear(); } + static void SetUpTestSuite() + { + ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); + AccessControl::Delegate * delegate = Examples::GetAccessControlDelegate(); + SetAccessControl(accessControl); + GetAccessControl().SetAccessRestriction(&accessRestriction); + VerifyOrDie(GetAccessControl().Init(delegate, testDeviceTypeResolver) == CHIP_NO_ERROR); + EXPECT_EQ(LoadAccessControl(accessControl, aclEntryData, aclEntryDataCount), CHIP_NO_ERROR); + } + static void TearDownTestSuite() + { + GetAccessControl().Finish(); + ResetAccessControlToDefault(); + } +}; + +// basic data check without restrictions +TEST_F(TestAccessRestriction, MetaTest) +{ + for (const auto & checkData : checkDataNoRestrictions) + { + CHIP_ERROR expectedResult = checkData.allow ? CHIP_NO_ERROR : CHIP_ERROR_ACCESS_DENIED; + EXPECT_EQ(accessControl.Check(checkData.subjectDescriptor, checkData.requestPath, checkData.privilege), expectedResult); + } +} + +// ensure adding restrictons on endpoint 0 (any cluster) or for network commissioning and descriptor clusters fail +TEST_F(TestAccessRestriction, InvalidRestrictionsTest) +{ + AccessRestriction::Entry entry; + entry.fabricIndex = 1; + entry.clusterId = kOnOffCluster; + entry.restrictions.push_back({ .restrictionType = AccessRestriction::Type::kAttributeAccessForbidden }); + + // must not restrict endpoint 0 + entry.endpointNumber = 0; + EXPECT_EQ(accessRestriction.CreateEntry(nullptr, entry, 1), CHIP_ERROR_INVALID_ARGUMENT); + + // must not restrict network commissioning cluster + entry.endpointNumber = 1; + entry.clusterId = kNetworkCommissioningCluster; + EXPECT_EQ(accessRestriction.CreateEntry(nullptr, entry, 1), CHIP_ERROR_INVALID_ARGUMENT); + + // must not restrict descriptor cluster + entry.clusterId = kDescriptorCluster; + EXPECT_EQ(accessRestriction.CreateEntry(nullptr, entry, 1), CHIP_ERROR_INVALID_ARGUMENT); +} + +constexpr CheckData accessAttributeRestrictionTestData[] = { + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, .endpoint = 1, .requestType = RequestType::kAttributeWriteRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = false }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, .endpoint = 1, .requestType = RequestType::kAttributeReadRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = false }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, .endpoint = 1, .requestType = RequestType::kCommandInvokeRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, + .endpoint = 1, + .requestType = RequestType::kEventReadOrSubscribeRequest, + .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, +}; + +TEST_F(TestAccessRestriction, AccessAttributeRestrictionTest) +{ + AccessRestriction::Entry entry; + entry.fabricIndex = 1; + entry.endpointNumber = 1; + entry.clusterId = kOnOffCluster; + entry.restrictions.push_back({ .restrictionType = AccessRestriction::Type::kAttributeAccessForbidden }); + + // test wildcarded entity id + EXPECT_EQ(accessRestriction.CreateEntry(nullptr, entry, 1), CHIP_NO_ERROR); + RunChecks(accessAttributeRestrictionTestData, ArraySize(accessAttributeRestrictionTestData)); + + // test specific entity id + accessRestriction.Clear(); + entry.restrictions[0].id.SetValue(1); + EXPECT_EQ(accessRestriction.CreateEntry(nullptr, entry, 1), CHIP_NO_ERROR); + RunChecks(accessAttributeRestrictionTestData, ArraySize(accessAttributeRestrictionTestData)); +} + +constexpr CheckData writeAttributeRestrictionTestData[] = { + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, .endpoint = 1, .requestType = RequestType::kAttributeWriteRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = false }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, .endpoint = 1, .requestType = RequestType::kAttributeReadRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, .endpoint = 1, .requestType = RequestType::kCommandInvokeRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, + .endpoint = 1, + .requestType = RequestType::kEventReadOrSubscribeRequest, + .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, +}; + +TEST_F(TestAccessRestriction, WriteAttributeRestrictionTest) +{ + AccessRestriction::Entry entry; + entry.fabricIndex = 1; + entry.endpointNumber = 1; + entry.clusterId = kOnOffCluster; + entry.restrictions.push_back({ .restrictionType = AccessRestriction::Type::kAttributeWriteForbidden }); + + // test wildcarded entity id + EXPECT_EQ(accessRestriction.CreateEntry(nullptr, entry, 1), CHIP_NO_ERROR); + RunChecks(writeAttributeRestrictionTestData, ArraySize(writeAttributeRestrictionTestData)); + + // test specific entity id + accessRestriction.Clear(); + entry.restrictions[0].id.SetValue(1); + EXPECT_EQ(accessRestriction.CreateEntry(nullptr, entry, 1), CHIP_NO_ERROR); + RunChecks(writeAttributeRestrictionTestData, ArraySize(writeAttributeRestrictionTestData)); +} + +constexpr CheckData commandAttributeRestrictionTestData[] = { + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, .endpoint = 1, .requestType = RequestType::kAttributeWriteRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, .endpoint = 1, .requestType = RequestType::kAttributeReadRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, .endpoint = 1, .requestType = RequestType::kCommandInvokeRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = false }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, + .endpoint = 1, + .requestType = RequestType::kEventReadOrSubscribeRequest, + .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, +}; + +TEST_F(TestAccessRestriction, CommandRestrictionTest) +{ + AccessRestriction::Entry entry; + entry.fabricIndex = 1; + entry.endpointNumber = 1; + entry.clusterId = kOnOffCluster; + entry.restrictions.push_back({ .restrictionType = AccessRestriction::Type::kCommandForbidden }); + + // test wildcarded entity id + EXPECT_EQ(accessRestriction.CreateEntry(nullptr, entry, 1), CHIP_NO_ERROR); + RunChecks(commandAttributeRestrictionTestData, ArraySize(commandAttributeRestrictionTestData)); + + // test specific entity id + accessRestriction.Clear(); + entry.restrictions[0].id.SetValue(1); + EXPECT_EQ(accessRestriction.CreateEntry(nullptr, entry, 1), CHIP_NO_ERROR); + RunChecks(commandAttributeRestrictionTestData, ArraySize(commandAttributeRestrictionTestData)); +} + +constexpr CheckData eventAttributeRestrictionTestData[] = { + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, .endpoint = 1, .requestType = RequestType::kAttributeWriteRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, .endpoint = 1, .requestType = RequestType::kAttributeReadRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, .endpoint = 1, .requestType = RequestType::kCommandInvokeRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, + .endpoint = 1, + .requestType = RequestType::kEventReadOrSubscribeRequest, + .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = false }, +}; + +TEST_F(TestAccessRestriction, EventRestrictionTest) +{ + AccessRestriction::Entry entry; + entry.fabricIndex = 1; + entry.endpointNumber = 1; + entry.clusterId = kOnOffCluster; + entry.restrictions.push_back({ .restrictionType = AccessRestriction::Type::kEventForbidden }); + + // test wildcarded entity id + EXPECT_EQ(accessRestriction.CreateEntry(nullptr, entry, 1), CHIP_NO_ERROR); + RunChecks(eventAttributeRestrictionTestData, ArraySize(eventAttributeRestrictionTestData)); + + // test specific entity id + accessRestriction.Clear(); + entry.restrictions[0].id.SetValue(1); + EXPECT_EQ(accessRestriction.CreateEntry(nullptr, entry, 1), CHIP_NO_ERROR); + RunChecks(eventAttributeRestrictionTestData, ArraySize(eventAttributeRestrictionTestData)); +} + +constexpr CheckData combinedRestrictionTestData[] = { + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, .endpoint = 1, .requestType = RequestType::kAttributeWriteRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = false }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, .endpoint = 1, .requestType = RequestType::kAttributeReadRequest, .entityId = 2 }, + .privilege = Privilege::kAdminister, + .allow = false }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, .endpoint = 1, .requestType = RequestType::kAttributeWriteRequest, .entityId = 3 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, .endpoint = 1, .requestType = RequestType::kAttributeReadRequest, .entityId = 4 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, .endpoint = 1, .requestType = RequestType::kAttributeReadRequest, .entityId = 3 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, .endpoint = 1, .requestType = RequestType::kCommandInvokeRequest, .entityId = 4 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 }, + .requestPath = { .cluster = kOnOffCluster, + .endpoint = 1, + .requestType = RequestType::kEventReadOrSubscribeRequest, + .entityId = 5 }, + .privilege = Privilege::kAdminister, + .allow = true }, + { .subjectDescriptor = { .fabricIndex = 2, .authMode = AuthMode::kCase, .subject = kOperationalNodeId2 }, + .requestPath = { .cluster = kOnOffCluster, .endpoint = 1, .requestType = RequestType::kCommandInvokeRequest, .entityId = 1 }, + .privilege = Privilege::kAdminister, + .allow = false }, + { .subjectDescriptor = { .fabricIndex = 2, .authMode = AuthMode::kCase, .subject = kOperationalNodeId2 }, + .requestPath = { .cluster = kOnOffCluster, .endpoint = 1, .requestType = RequestType::kAttributeWriteRequest, .entityId = 2 }, + .privilege = Privilege::kAdminister, + .allow = true }, +}; + +TEST_F(TestAccessRestriction, CombinedRestrictionTest) +{ + // a restriction for all access to attribute 1 and 2, attributes 3 and 4 are allowed + AccessRestriction::Entry entry1; + entry1.fabricIndex = 1; + entry1.endpointNumber = 1; + entry1.clusterId = kOnOffCluster; + entry1.restrictions.push_back({ .restrictionType = AccessRestriction::Type::kAttributeWriteForbidden }); + entry1.restrictions[0].id.SetValue(1); + entry1.restrictions.push_back({ .restrictionType = AccessRestriction::Type::kAttributeAccessForbidden }); + entry1.restrictions[1].id.SetValue(2); + EXPECT_EQ(accessRestriction.CreateEntry(nullptr, entry1, 1), CHIP_NO_ERROR); + + // a restriction for fabric 2 that forbids command 1 and 2. Check that command 1 is blocked on invoke, but attribute 2 write is + // allowed + AccessRestriction::Entry entry2; + entry2.fabricIndex = 2; + entry2.endpointNumber = 1; + entry2.clusterId = kOnOffCluster; + entry2.restrictions.push_back({ .restrictionType = AccessRestriction::Type::kCommandForbidden }); + entry2.restrictions[0].id.SetValue(1); + entry2.restrictions.push_back({ .restrictionType = AccessRestriction::Type::kCommandForbidden }); + entry2.restrictions[1].id.SetValue(2); + EXPECT_EQ(accessRestriction.CreateEntry(nullptr, entry2, 2), CHIP_NO_ERROR); + + RunChecks(combinedRestrictionTestData, ArraySize(combinedRestrictionTestData)); +} + +} // namespace Access +} // namespace chip diff --git a/src/app/CommandHandlerImpl.cpp b/src/app/CommandHandlerImpl.cpp index 1945e7e5e69dc3..4757a4634f29a2 100644 --- a/src/app/CommandHandlerImpl.cpp +++ b/src/app/CommandHandlerImpl.cpp @@ -408,6 +408,7 @@ Status CommandHandlerImpl::ProcessCommandDataIB(CommandDataIB::Parser & aCommand .entityId = concretePath.mCommandId }; Access::Privilege requestPrivilege = RequiredPrivilege::ForInvokeCommand(concretePath); err = Access::GetAccessControl().Check(subjectDescriptor, requestPath, requestPrivilege); + if (err != CHIP_NO_ERROR) { if (err != CHIP_ERROR_ACCESS_DENIED) diff --git a/src/app/clusters/access-control-server/access-control-server.cpp b/src/app/clusters/access-control-server/access-control-server.cpp index 321f7aa92a483a..af97b21d809c36 100644 --- a/src/app/clusters/access-control-server/access-control-server.cpp +++ b/src/app/clusters/access-control-server/access-control-server.cpp @@ -17,6 +17,11 @@ #include +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS +#include +#include +#endif + #include #include @@ -41,14 +46,28 @@ using Entry = AccessControl::Entry; using EntryListener = AccessControl::EntryListener; using ExtensionEvent = Clusters::AccessControl::Events::AccessControlExtensionChanged::Type; +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS +using ArlChangedEvent = Clusters::AccessControl::Events::AccessRestrictionEntryChanged::Type; +using ArlReviewEvent = Clusters::AccessControl::Events::FabricRestrictionReviewUpdate::Type; +#endif + // TODO(#13590): generated code doesn't automatically handle max length so do it manually constexpr int kExtensionDataMaxLength = 128; constexpr uint16_t kClusterRevision = 1; +constexpr uint16_t kMaxInstructionStringLength = 512; + +constexpr uint16_t kMaxRedirectUrlStringLength = 256; + namespace { -class AccessControlAttribute : public AttributeAccessInterface, public EntryListener +class AccessControlAttribute : public AttributeAccessInterface, + public AccessControl::EntryListener +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS + , + public AccessRestriction::EntryListener +#endif { public: AccessControlAttribute() : AttributeAccessInterface(Optional(0), AccessControlCluster::Id) {} @@ -64,8 +83,16 @@ class AccessControlAttribute : public AttributeAccessInterface, public EntryList CHIP_ERROR Write(const ConcreteDataAttributePath & aPath, AttributeValueDecoder & aDecoder) override; public: - void OnEntryChanged(const SubjectDescriptor * subjectDescriptor, FabricIndex fabric, size_t index, const Entry * entry, - ChangeType changeType) override; + void OnEntryChanged(const SubjectDescriptor * subjectDescriptor, FabricIndex fabric, size_t index, + const AccessControl::Entry * entry, AccessControl::EntryListener::ChangeType changeType) override; + +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS + void OnEntryChanged(FabricIndex fabricIndex, size_t index, SharedPtr entry, + AccessRestriction::EntryListener::ChangeType changeType) override; + + void OnFabricRestrictionReviewUpdate(FabricIndex fabricIndex, uint64_t token, const char * instruction, + const char * redirectUrl) override; +#endif private: /// Business logic implementation of write, returns generic CHIP_ERROR. @@ -78,6 +105,10 @@ class AccessControlAttribute : public AttributeAccessInterface, public EntryList CHIP_ERROR ReadExtension(AttributeValueEncoder & aEncoder); CHIP_ERROR WriteAcl(const ConcreteDataAttributePath & aPath, AttributeValueDecoder & aDecoder); CHIP_ERROR WriteExtension(const ConcreteDataAttributePath & aPath, AttributeValueDecoder & aDecoder); +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS + CHIP_ERROR ReadCommissioningArl(AttributeValueEncoder & aEncoder); + CHIP_ERROR ReadArl(AttributeValueEncoder & aEncoder); +#endif } sAttribute; CHIP_ERROR LogExtensionChangedEvent(const AccessControlCluster::Structs::AccessControlExtensionStruct::Type & item, @@ -159,6 +190,12 @@ CHIP_ERROR AccessControlAttribute::ReadImpl(const ConcreteReadAttributePath & aP ReturnErrorOnFailure(GetAccessControl().GetMaxEntriesPerFabric(value)); return aEncoder.Encode(static_cast(value)); } +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS + case AccessControlCluster::Attributes::CommissioningARL::Id: + return ReadCommissioningArl(aEncoder); + case AccessControlCluster::Attributes::Arl::Id: + return ReadArl(aEncoder); +#endif case AccessControlCluster::Attributes::ClusterRevision::Id: return aEncoder.Encode(kClusterRevision); } @@ -375,7 +412,7 @@ CHIP_ERROR AccessControlAttribute::WriteExtension(const ConcreteDataAttributePat } void AccessControlAttribute::OnEntryChanged(const SubjectDescriptor * subjectDescriptor, FabricIndex fabric, size_t index, - const Entry * entry, ChangeType changeType) + const AccessControl::Entry * entry, 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 @@ -389,11 +426,11 @@ void AccessControlAttribute::OnEntryChanged(const SubjectDescriptor * subjectDes CHIP_ERROR err; AclEvent event{ .changeType = ChangeTypeEnum::kChanged, .fabricIndex = subjectDescriptor->fabricIndex }; - if (changeType == ChangeType::kAdded) + if (changeType == AccessControl::EntryListener::ChangeType::kAdded) { event.changeType = ChangeTypeEnum::kAdded; } - else if (changeType == ChangeType::kRemoved) + else if (changeType == AccessControl::EntryListener::ChangeType::kRemoved) { event.changeType = ChangeTypeEnum::kRemoved; } @@ -428,6 +465,96 @@ void AccessControlAttribute::OnEntryChanged(const SubjectDescriptor * subjectDes ChipLogError(DataManagement, "AccessControlCluster: event failed %" CHIP_ERROR_FORMAT, err.Format()); } +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS +CHIP_ERROR AccessControlAttribute::ReadCommissioningArl(AttributeValueEncoder & aEncoder) +{ + auto accessRestriction = GetAccessControl().GetAccessRestriction(); + if (accessRestriction == nullptr) + { + return CHIP_ERROR_NOT_IMPLEMENTED; + } + + return aEncoder.EncodeList([&](const auto & encoder) -> CHIP_ERROR { + AccessRestriction::EntryIterator begin; + AccessRestriction::EntryIterator end; + ReturnErrorOnFailure(accessRestriction->CommissioningEntries(begin, end)); + + for (AccessRestriction::EntryIterator it = begin; it != end; ++it) + { + ArlStorage::EncodableEntry encodableEntry(*it); + ReturnErrorOnFailure(encoder.Encode(encodableEntry)); + } + return CHIP_NO_ERROR; + }); +} + +CHIP_ERROR AccessControlAttribute::ReadArl(AttributeValueEncoder & aEncoder) +{ + auto accessRestriction = GetAccessControl().GetAccessRestriction(); + if (accessRestriction == nullptr) + { + return CHIP_ERROR_NOT_IMPLEMENTED; + } + + return aEncoder.EncodeList([&](const auto & encoder) -> CHIP_ERROR { + for (auto & info : Server::GetInstance().GetFabricTable()) + { + auto fabric = info.GetFabricIndex(); + AccessRestriction::EntryIterator begin; + AccessRestriction::EntryIterator end; + ReturnErrorOnFailure(accessRestriction->Entries(fabric, begin, end)); + + for (AccessRestriction::EntryIterator it = begin; it != end; ++it) + { + ArlStorage::EncodableEntry encodableEntry(*it); + ReturnErrorOnFailure(encoder.Encode(encodableEntry)); + } + } + return CHIP_NO_ERROR; + }); +} + +void AccessControlAttribute::OnEntryChanged(FabricIndex fabricIndex, size_t index, SharedPtr entry, + AccessRestriction::EntryListener::ChangeType changeType) +{ + CHIP_ERROR err; + ArlChangedEvent event{ .fabricIndex = fabricIndex }; + + EventNumber eventNumber; + SuccessOrExit(err = LogEvent(event, 0, eventNumber)); + + return; + +exit: + ChipLogError(DataManagement, "AccessControlCluster: restriction event failed %" CHIP_ERROR_FORMAT, err.Format()); +} + +void AccessControlAttribute::OnFabricRestrictionReviewUpdate(FabricIndex fabricIndex, uint64_t token, const char * instruction, + const char * redirectUrl) +{ + CHIP_ERROR err; + ArlReviewEvent event{ .token = token, .fabricIndex = fabricIndex }; + + if (instruction != nullptr) + { + event.instruction.SetNonNull(chip::CharSpan(instruction, strnlen(instruction, kMaxInstructionStringLength))); + } + + if (redirectUrl != nullptr) + { + event.redirectURL.SetNonNull(chip::CharSpan(redirectUrl, strnlen(redirectUrl, kMaxRedirectUrlStringLength))); + } + + EventNumber eventNumber; + SuccessOrExit(err = LogEvent(event, 0, eventNumber)); + + return; + +exit: + ChipLogError(DataManagement, "AccessControlCluster: review event failed %" CHIP_ERROR_FORMAT, err.Format()); +} +#endif + CHIP_ERROR ChipErrorToImErrorMap(CHIP_ERROR err) { // Map some common errors into an underlying IM error @@ -473,6 +600,16 @@ CHIP_ERROR AccessControlAttribute::Write(const ConcreteDataAttributePath & aPath return ChipErrorToImErrorMap(WriteImpl(aPath, aDecoder)); } +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS +void OnPlatformEventHandler(const DeviceLayer::ChipDeviceEvent * event, intptr_t arg) +{ + if (event->Type == DeviceLayer::DeviceEventType::kCommissioningComplete) + { + Access::GetAccessControl().GetAccessRestriction()->CreateFabricEntries(event->CommissioningComplete.fabricIndex); + } +} +#endif + } // namespace void MatterAccessControlPluginServerInitCallback() @@ -481,4 +618,70 @@ void MatterAccessControlPluginServerInitCallback() AttributeAccessInterfaceRegistry::Instance().Register(&sAttribute); GetAccessControl().AddEntryListener(sAttribute); + +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS + auto accessRestriction = GetAccessControl().GetAccessRestriction(); + if (accessRestriction != nullptr) + { + accessRestriction->AddListener(sAttribute); + } + + DeviceLayer::PlatformMgrImpl().AddEventHandler(OnPlatformEventHandler); +#endif +} + +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS +bool emberAfAccessControlClusterReviewFabricRestrictionsCallback( + CommandHandler * commandObj, const ConcreteCommandPath & commandPath, + const Clusters::AccessControl::Commands::ReviewFabricRestrictions::DecodableType & commandData) +{ + if (commandPath.mEndpointId != 0) + { + ChipLogError(DataManagement, "AccessControlCluster: invalid endpoint in ReviewFabricRestrictions request"); + return true; + } + + uint64_t token; + std::vector entries; + auto entryIter = commandData.arl.begin(); + while (entryIter.Next()) + { + AccessRestriction::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()) + { + AccessRestriction::Restriction restriction; + restriction.restrictionType = static_cast(restrictionIter.GetValue().type); + if (!restrictionIter.GetValue().id.IsNull()) + { + restriction.id.SetValue(restrictionIter.GetValue().id.Value()); + } + entry.restrictions.push_back(restriction); + } + + entries.push_back(entry); + } + + CHIP_ERROR err = GetAccessControl().GetAccessRestriction()->RequestFabricRestrictionReview( + commandObj->GetAccessingFabricIndex(), entries, token); + + if (err == CHIP_NO_ERROR) + { + Clusters::AccessControl::Commands::ReviewFabricRestrictionsResponse::Type response; + response.token = token; + commandObj->AddResponse(commandPath, response); + } + else + { + ChipLogError(DataManagement, "AccessControlCluster: restriction check failed: %" CHIP_ERROR_FORMAT, err.Format()); + + // return error to client? + } + + return true; } +#endif diff --git a/src/app/server/ArlStorage.cpp b/src/app/server/ArlStorage.cpp new file mode 100644 index 00000000000000..983f9a16ad8cd6 --- /dev/null +++ b/src/app/server/ArlStorage.cpp @@ -0,0 +1,158 @@ +/* + * + * Copyright (c) 2024 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 + +#include + +using namespace chip; +using namespace chip::app; +using namespace chip::Access; + +using Entry = AccessRestriction::Entry; +using EntryListener = AccessRestriction::EntryListener; +using StagingRestrictionType = Clusters::AccessControl::AccessRestrictionTypeEnum; +using StagingRestriction = Clusters::AccessControl::Structs::AccessRestrictionStruct::Type; + +namespace { + +CHIP_ERROR Convert(StagingRestrictionType from, AccessRestriction::Type & to) +{ + switch (from) + { + case StagingRestrictionType::kAttributeAccessForbidden: + to = AccessRestriction::Type::kAttributeAccessForbidden; + break; + case StagingRestrictionType::kAttributeWriteForbidden: + to = AccessRestriction::Type::kAttributeWriteForbidden; + break; + case StagingRestrictionType::kCommandForbidden: + to = AccessRestriction::Type::kCommandForbidden; + break; + case StagingRestrictionType::kEventForbidden: + to = AccessRestriction::Type::kEventForbidden; + break; + default: + return CHIP_ERROR_INVALID_ARGUMENT; + } + return CHIP_NO_ERROR; +} + +CHIP_ERROR Convert(AccessRestriction::Type from, StagingRestrictionType & to) +{ + switch (from) + { + case AccessRestriction::Type::kAttributeAccessForbidden: + to = StagingRestrictionType::kAttributeAccessForbidden; + break; + case AccessRestriction::Type::kAttributeWriteForbidden: + to = StagingRestrictionType::kAttributeWriteForbidden; + break; + case AccessRestriction::Type::kCommandForbidden: + to = StagingRestrictionType::kCommandForbidden; + break; + case AccessRestriction::Type::kEventForbidden: + to = StagingRestrictionType::kEventForbidden; + break; + default: + return CHIP_ERROR_INVALID_ARGUMENT; + } + return CHIP_NO_ERROR; +} + +} // namespace + +namespace chip { +namespace app { + +CHIP_ERROR ArlStorage::DecodableEntry::Decode(TLV::TLVReader & reader) +{ + ReturnErrorOnFailure(mStagingEntry.Decode(reader)); + + mEntry.fabricIndex = mStagingEntry.GetFabricIndex(); + mEntry.endpointNumber = mStagingEntry.endpoint; + mEntry.clusterId = mStagingEntry.cluster; + + auto iterator = mStagingEntry.restrictions.begin(); + while (iterator.Next()) + { + auto & tmp = iterator.GetValue(); + AccessRestriction::Restriction restriction; + ReturnErrorOnFailure(Convert(tmp.type, restriction.restrictionType)); + + if (!tmp.id.IsNull()) + { + restriction.id.SetValue(tmp.id.Value()); + } + + mEntry.restrictions.push_back(restriction); + } + ReturnErrorOnFailure(iterator.GetStatus()); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR ArlStorage::EncodableEntry::EncodeForRead(TLV::TLVWriter & writer, TLV::Tag tag, FabricIndex fabric) const +{ + ReturnErrorOnFailure(Stage()); + ReturnErrorOnFailure(mStagingEntry.EncodeForRead(writer, tag, fabric)); + return CHIP_NO_ERROR; +} + +CHIP_ERROR ArlStorage::EncodableEntry::EncodeForWrite(TLV::TLVWriter & writer, TLV::Tag tag) const +{ + ReturnErrorOnFailure(Stage()); + ReturnErrorOnFailure(mStagingEntry.EncodeForWrite(writer, tag)); + return CHIP_NO_ERROR; +} + +CHIP_ERROR ArlStorage::EncodableEntry::Stage() const +{ + mStagingEntry.fabricIndex = mEntry->fabricIndex; + mStagingEntry.endpoint = mEntry->endpointNumber; + mStagingEntry.cluster = mEntry->clusterId; + + { + size_t count = mEntry->restrictions.size(); + if (count > 0 && count <= CHIP_CONFIG_ACCESS_RESTRICTION_MAX_RESTRICTIONS_PER_ENTRY) + { + for (size_t i = 0; i < count; i++) + { + auto restriction = mEntry->restrictions[i]; + StagingRestriction tmp; + ReturnErrorOnFailure(Convert(restriction.restrictionType, tmp.type)); + + if (restriction.id.HasValue()) + { + tmp.id.SetNonNull(restriction.id.Value()); + } + + mStagingRestrictions[i] = tmp; + } + mStagingEntry.restrictions = Span(mStagingRestrictions, count); + } + else + { + return CHIP_ERROR_INVALID_ARGUMENT; + } + } + + return CHIP_NO_ERROR; +} + +} // namespace app +} // namespace chip diff --git a/src/app/server/ArlStorage.h b/src/app/server/ArlStorage.h new file mode 100644 index 00000000000000..34fb616d226e4f --- /dev/null +++ b/src/app/server/ArlStorage.h @@ -0,0 +1,148 @@ +/* + * + * Copyright (c) 2024 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. + */ + +#pragma once + +#include +#include +#include + +#include + +namespace chip { +namespace app { + +/** + * Storage specifically for access restriction entries, which correspond to + * the ARL attribute of the access control cluster. + * + * An object of this class should be initialized directly after the access + * control module is initialized, as it will populate entries in the system + * module from storage, and also install a listener in the system module to + * keep storage up to date as entries change. + * + * This class also provides facilities for converting between access restriction + * entries (as used by the system module) and access restriction entries (as used + * by the generated cluster code). + */ +class ArlStorage +{ +public: + /** + * Used for decoding access restriction entries. + * + * Typically used temporarily on the stack to decode: + * - source: TLV + * - staging: generated cluster level code + * - destination: system level access restriction entry + */ + class DecodableEntry + { + using Entry = Access::AccessRestriction::Entry; + using StagingEntry = Clusters::AccessControl::Structs::AccessRestrictionEntryStruct::DecodableType; + + public: + DecodableEntry() = default; + + /** + * Reader decodes into a staging entry, which is then unstaged + * into a member entry. + */ + CHIP_ERROR Decode(TLV::TLVReader & reader); + + Entry & GetEntry() { return mEntry; } + + const Entry & GetEntry() const { return mEntry; } + + public: + static constexpr bool kIsFabricScoped = true; + + void SetFabricIndex(FabricIndex fabricIndex) { mEntry.fabricIndex = fabricIndex; } + + private: + Entry mEntry; + + StagingEntry mStagingEntry; + }; + + /** + * Used for encoding access restriction entries. + * + * Typically used temporarily on the stack to encode: + * - source: system level access restriction entry + * - staging: generated cluster level code + * - destination: TLV + */ + class EncodableEntry + { + using Entry = Access::AccessRestriction::Entry; + using StagingEntry = Clusters::AccessControl::Structs::AccessRestrictionEntryStruct::Type; + using StagingRestriction = Clusters::AccessControl::Structs::AccessRestrictionStruct::Type; + + public: + EncodableEntry(std::shared_ptr entry) : mEntry(entry) {} + + /** + * Constructor-provided entry is staged into a staging entry, + * which is then encoded into a writer. + */ + CHIP_ERROR EncodeForRead(TLV::TLVWriter & writer, TLV::Tag tag, FabricIndex fabric) const; + + /** + * Constructor-provided entry is staged into a staging entry, + * which is then encoded into a writer. + */ + CHIP_ERROR EncodeForWrite(TLV::TLVWriter & writer, TLV::Tag tag) const; + + /** + * Constructor-provided entry is staged into a staging entry. + */ + CHIP_ERROR Stage() const; + + StagingEntry & GetStagingEntry() { return mStagingEntry; } + + const StagingEntry & GetStagingEntry() const { return mStagingEntry; } + + public: + static constexpr bool kIsFabricScoped = true; + + FabricIndex GetFabricIndex() const { return mEntry->fabricIndex; } + + private: + std::shared_ptr mEntry; + + mutable StagingEntry mStagingEntry; + mutable StagingRestriction mStagingRestrictions[CHIP_CONFIG_ACCESS_RESTRICTION_MAX_RESTRICTIONS_PER_ENTRY]; + }; + + virtual ~ArlStorage() = default; + + /** + * Initialize should be called after chip::Access::AccessControl is initialized. + * + * Implementations should take this opportunity to populate AccessControl with ARL entries + * loaded from persistent storage. A half-open range of fabrics [first, last) is provided + * so this can be done on a per-fabric basis. + * + * Implementations should also install an entry change listener on AccessControl to maintain + * ARL entries in persistent storage as they are changed. + */ + virtual CHIP_ERROR Init(PersistentStorageDelegate & persistentStorage, ConstFabricIterator first, ConstFabricIterator last) = 0; +}; + +} // namespace app +} // namespace chip diff --git a/src/app/server/BUILD.gn b/src/app/server/BUILD.gn index 401356d7b753a4..1a82cd911f0c51 100644 --- a/src/app/server/BUILD.gn +++ b/src/app/server/BUILD.gn @@ -13,6 +13,7 @@ # limitations under the License. import("//build_overrides/chip.gni") +import("${chip_root}/src/access/access.gni") import("${chip_root}/src/app/common_flags.gni") import("${chip_root}/src/app/icd/icd.gni") @@ -79,4 +80,13 @@ static_library("server") { [ "${chip_root}/src/app/icd/server:default-check-in-back-off" ] } } + + if (chip_enable_access_restrictions) { + sources += [ + "ArlStorage.cpp", + "ArlStorage.h", + "DefaultArlStorage.cpp", + "DefaultArlStorage.h", + ] + } } diff --git a/src/app/server/DefaultArlStorage.cpp b/src/app/server/DefaultArlStorage.cpp new file mode 100644 index 00000000000000..5b3ece3ada7606 --- /dev/null +++ b/src/app/server/DefaultArlStorage.cpp @@ -0,0 +1,203 @@ +/* + * + * Copyright (c) 2024 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 + +#include +#include + +using namespace chip; +using namespace chip::app; +using namespace chip::Access; + +using EncodableEntry = ArlStorage::EncodableEntry; +using Entry = AccessRestriction::Entry; +using EntryListener = AccessRestriction::EntryListener; +using StagingRestriction = Clusters::AccessControl::Structs::AccessRestrictionEntryStruct::Type; +using Restriction = AccessRestriction::Restriction; + +namespace { + +/* +Size calculation for TLV encoded entry. + +Because EncodeForWrite is used without an accessing fabric, the fabric index is +not encoded. However, let's assume it is. This yields 17 bytes total overhead, +but it's wise to add a few more for safety. + +Each restriction requires 11 bytes. + +DATA C T L V NOTES +structure (anonymous) 1 0x15 + field 0 endpoint 1 1 1 + field 1 cluster 1 1 4 + field 2 restrictions 1 1 + structure (anonymous) 1 per restriction + field 0 type 1 1 1 + field 1 id 1 1 4 + end structure 1 + end list 1 0x18 + field 254 fabric index 1 1 1 not written +end structure 1 0x18 +*/ + +constexpr int kEncodedEntryOverheadBytes = 17 + 8; +constexpr int kEncodedEntryRestrictionBytes = 11 * CHIP_CONFIG_ACCESS_RESTRICTION_MAX_RESTRICTIONS_PER_ENTRY; +constexpr int kEncodedEntryTotalBytes = kEncodedEntryOverheadBytes + kEncodedEntryRestrictionBytes; + +class : public EntryListener +{ +public: + void OnEntryChanged(FabricIndex fabricIndex, size_t index, SharedPtr entry, ChangeType changeType) override + { + CHIP_ERROR err; + + uint8_t buffer[kEncodedEntryTotalBytes] = { 0 }; + + VerifyOrExit(mPersistentStorage != nullptr, err = CHIP_ERROR_INCORRECT_STATE); + + if (changeType == ChangeType::kRemoved) + { + // Shuffle down entries past index, then delete entry at last index. + while (true) + { + uint16_t size = static_cast(sizeof(buffer)); + err = mPersistentStorage->SyncGetKeyValue( + DefaultStorageKeyAllocator::AccessControlArlEntry(fabricIndex, index + 1).KeyName(), buffer, size); + if (err == CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND) + { + break; + } + SuccessOrExit(err); + SuccessOrExit(err = mPersistentStorage->SyncSetKeyValue( + DefaultStorageKeyAllocator::AccessControlArlEntry(fabricIndex, index).KeyName(), buffer, size)); + index++; + } + SuccessOrExit(err = mPersistentStorage->SyncDeleteKeyValue( + DefaultStorageKeyAllocator::AccessControlArlEntry(fabricIndex, index).KeyName())); + } + else + { + // Write added/updated entry at index. + VerifyOrExit(entry != nullptr, err = CHIP_ERROR_INCORRECT_STATE); + TLV::TLVWriter writer; + writer.Init(buffer); + EncodableEntry encodableEntry(entry); + SuccessOrExit(err = encodableEntry.EncodeForWrite(writer, TLV::AnonymousTag())); + SuccessOrExit(err = mPersistentStorage->SyncSetKeyValue( + DefaultStorageKeyAllocator::AccessControlArlEntry(fabricIndex, index).KeyName(), buffer, + static_cast(writer.GetLengthWritten()))); + } + + return; + + exit: + ChipLogError(DataManagement, "DefaultArlStorage: failed %" CHIP_ERROR_FORMAT, err.Format()); + } + + void OnFabricRestrictionReviewUpdate(FabricIndex fabricIndex, uint64_t token, const char * instruction, + const char * redirectUrl) override + {} + + // Must initialize before use. + void Init(PersistentStorageDelegate & persistentStorage) { mPersistentStorage = &persistentStorage; } + +private: + PersistentStorageDelegate * mPersistentStorage = nullptr; + +} sEntryListener; + +} // namespace + +namespace chip { +namespace app { + +CHIP_ERROR DefaultArlStorage::Init(PersistentStorageDelegate & persistentStorage, ConstFabricIterator first, + ConstFabricIterator last) +{ + ChipLogProgress(DataManagement, "DefaultArlStorage: initializing"); + + CHIP_ERROR err; + + [[maybe_unused]] size_t count = 0; + + for (size_t index = 0; /**/; ++index) + { + uint8_t buffer[kEncodedEntryTotalBytes] = { 0 }; + uint16_t size = static_cast(sizeof(buffer)); + err = persistentStorage.SyncGetKeyValue(DefaultStorageKeyAllocator::AccessControlCommissioningArlEntry(index).KeyName(), + buffer, size); + if (err == CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND) + { + break; + } + SuccessOrExit(err); + + TLV::TLVReader reader; + reader.Init(buffer, size); + SuccessOrExit(err = reader.Next()); + + DecodableEntry decodableEntry; + SuccessOrExit(err = decodableEntry.Decode(reader)); + + SuccessOrExit(err = GetAccessControl().GetAccessRestriction()->CreateCommissioningEntry( + MakeShared(decodableEntry.GetEntry()))); + count++; + } + + for (auto it = first; it != last; ++it) + { + auto fabric = it->GetFabricIndex(); + for (size_t index = 0; /**/; ++index) + { + uint8_t buffer[kEncodedEntryTotalBytes] = { 0 }; + uint16_t size = static_cast(sizeof(buffer)); + err = persistentStorage.SyncGetKeyValue(DefaultStorageKeyAllocator::AccessControlArlEntry(fabric, index).KeyName(), + buffer, size); + if (err == CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND) + { + break; + } + SuccessOrExit(err); + + TLV::TLVReader reader; + reader.Init(buffer, size); + SuccessOrExit(err = reader.Next()); + + DecodableEntry decodableEntry; + SuccessOrExit(err = decodableEntry.Decode(reader)); + + Entry & entry = decodableEntry.GetEntry(); + SuccessOrExit(err = GetAccessControl().GetAccessRestriction()->CreateEntry(nullptr, entry, fabric)); + count++; + } + } + + ChipLogProgress(DataManagement, "DefaultArlStorage: %u entries loaded", (unsigned) count); + + sEntryListener.Init(persistentStorage); + GetAccessControl().GetAccessRestriction()->AddListener(sEntryListener); + + return CHIP_NO_ERROR; + +exit: + ChipLogError(DataManagement, "DefaultArlStorage: failed %" CHIP_ERROR_FORMAT, err.Format()); + return err; +} + +} // namespace app +} // namespace chip diff --git a/src/app/server/DefaultArlStorage.h b/src/app/server/DefaultArlStorage.h new file mode 100644 index 00000000000000..ebdc23bbf65913 --- /dev/null +++ b/src/app/server/DefaultArlStorage.h @@ -0,0 +1,37 @@ +/* + * + * Copyright (c) 2024 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. + */ + +#pragma once + +#include + +namespace chip { +namespace app { + +class DefaultArlStorage : public ArlStorage +{ +public: + /** + * Initialize must be called. It loads ARL entries for all fabrics from persistent storage, + * then installs a listener for the access restriction system module to maintain ARL entries in + * persistent storage so they remain in sync with entries in the access restriction system module. + */ + CHIP_ERROR Init(PersistentStorageDelegate & persistentStorage, ConstFabricIterator first, ConstFabricIterator last) override; +}; + +} // namespace app +} // namespace chip diff --git a/src/app/server/Server.cpp b/src/app/server/Server.cpp index 22cd274ba87e39..fbdae0ccb213f8 100644 --- a/src/app/server/Server.cpp +++ b/src/app/server/Server.cpp @@ -180,6 +180,17 @@ CHIP_ERROR Server::Init(const ServerInitParams & initParams) mAclStorage = initParams.aclStorage; SuccessOrExit(err = mAclStorage->Init(*mDeviceStorage, mFabrics.begin(), mFabrics.end())); +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS + if (initParams.accessRestriction != nullptr && initParams.arlStorage != nullptr) + { + mAccessRestriction = initParams.accessRestriction; + mArlStorage = initParams.arlStorage; + + mAccessControl.SetAccessRestriction(mAccessRestriction); + SuccessOrExit(err = mArlStorage->Init(*mDeviceStorage, mFabrics.begin(), mFabrics.end())); + } +#endif + mGroupsProvider = initParams.groupDataProvider; SetGroupDataProvider(mGroupsProvider); diff --git a/src/app/server/Server.h b/src/app/server/Server.h index 2f6126a4ace635..577f077886bd3d 100644 --- a/src/app/server/Server.h +++ b/src/app/server/Server.h @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -163,6 +164,16 @@ struct ServerInitParams // ACL storage: MUST be injected. Used to store ACL entries in persistent storage. Must NOT // be initialized before being provided. app::AclStorage * aclStorage = nullptr; + +#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS + // Access Restriction implementation: MUST be injected if MNGD feature enabled. Used to enforce + // access restrictions that are managed by the device. + Access::AccessRestriction * accessRestriction = nullptr; + // ARL storage: MUST be injected if MNGD feature enabled. Used to store ACL entries in + // persistent storage. Must NOT be initialized before being provided. + app::ArlStorage * arlStorage = nullptr; +#endif + // Network native params can be injected depending on the // selected Endpoint implementation void * endpointNativeParams = nullptr; @@ -678,6 +689,8 @@ class Server Access::AccessControl mAccessControl; app::AclStorage * mAclStorage; + Access::AccessRestriction * mAccessRestriction; + app::ArlStorage * mArlStorage; TestEventTriggerDelegate * mTestEventTriggerDelegate; Crypto::OperationalKeystore * mOperationalKeystore; diff --git a/src/lib/core/CHIPConfig.h b/src/lib/core/CHIPConfig.h index e9c317dfc79d5c..a811f8b6012394 100644 --- a/src/lib/core/CHIPConfig.h +++ b/src/lib/core/CHIPConfig.h @@ -1206,6 +1206,27 @@ extern const char CHIP_NON_PRODUCTION_MARKER[]; "Please enable at least one of CHIP_CONFIG_EXAMPLE_ACCESS_CONTROL_FAST_COPY_SUPPORT or CHIP_CONFIG_EXAMPLE_ACCESS_CONTROL_FLEXIBLE_COPY_SUPPORT" #endif +/** + * @def CHIP_CONFIG_ACCESS_RESTRICTION_MAX_ENTRIES_PER_FABRIC + * + * Defines the maximum number of access restriction list entries per + * fabric in the access control code's ARL attribute. + */ +#ifndef CHIP_CONFIG_ACCESS_RESTRICTION_MAX_ENTRIES_PER_FABRIC +#define CHIP_CONFIG_ACCESS_RESTRICTION_MAX_ENTRIES_PER_FABRIC 10 +#endif + +/** + * @def CHIP_CONFIG_ACCESS_RESTRICTION_MAX_RESTRICTIONS_PER_ENTRY + * + * Defines the maximum number of access restrictions for each entry + * in the ARL attribute (each entry is for a specific cluster on an + * endpoint on a fabric). + */ +#ifndef CHIP_CONFIG_ACCESS_RESTRICTION_MAX_RESTRICTIONS_PER_ENTRY +#define CHIP_CONFIG_ACCESS_RESTRICTION_MAX_RESTRICTIONS_PER_ENTRY 10 +#endif + /** * @def CHIP_CONFIG_CASE_SESSION_RESUME_CACHE_SIZE * diff --git a/src/lib/support/DefaultStorageKeyAllocator.h b/src/lib/support/DefaultStorageKeyAllocator.h index 9ed8a2f56cfd77..421d8ba83ebb43 100644 --- a/src/lib/support/DefaultStorageKeyAllocator.h +++ b/src/lib/support/DefaultStorageKeyAllocator.h @@ -129,6 +129,15 @@ class DefaultStorageKeyAllocator } static StorageKeyName AccessControlExtensionEntry(FabricIndex fabric) { return StorageKeyName::Formatted("f/%x/ac/1", fabric); } + static StorageKeyName AccessControlCommissioningArlEntry(size_t index) + { + return StorageKeyName::Formatted("g/car/0/%x", static_cast(index)); + } + + static StorageKeyName AccessControlArlEntry(FabricIndex fabric, size_t index) + { + return StorageKeyName::Formatted("f/%x/ar/0/%x", fabric, static_cast(index)); + } // Group Message Counters static StorageKeyName GroupDataCounter() { return StorageKeyName::FromConst("g/gdc"); }