Managed ACL: Add AccessRestrictionList support (#34932)
* Add AccessRestrictionList support
* Update src/access/AccessConfig.h
Co-authored-by: C Freeman <cecille@google.com>
* Reworked data manipulators and other cleanup
* Fixed encode/decode so reading CommissioningARL and Arl attributes work
* Reworked ARL storage
Previously ARL related data was persisted in KVS. This has been
removed and now the responsibility for managing/maintaining the
related data (CommissioningARL and ARL attributes) is up to the
app to set on AccessRestrictionProvider class.
* Review fixes
cleanup ArlEncoder interface.
return error to client if arl review request fails
return token to client in FabricRestrictionReviewUpdate
* Fixed GetEntries vector pointer arg
* Updated core restriction logic/integration
* Restyled by clang-format
* fixed include check for renamed AccessRestrictionProvider.h file
* M-ACL updates
- refactored AccessControl::Check into CheckACL and CheckARL
- added placeholders for the upcoming CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL
- extracted ARL exception processing to standalone class for better
testing
* Add plumbing for subject descriptor IsCommissioning field
- Make session manager update that state on a message-per-message basis
- Add tests
Missing test: MRP test against a not-yet-committed fabric over CASE showing
that IsCommissioning is true.
* Fix crash
* Use new IsCommissioning in ARL check
* Updates for review comments
* restyled
* Review updates
- fixed return type for some command failures
- enhanced unit tests
* restyled
* Updated ARL tests per review comments
* work around nuttx and jsoncpp contention
* Review comments and nuttx build failure fix attempt
* review updates
---------
Co-authored-by: C Freeman <cecille@google.com>
Co-authored-by: Restyled.io <commits@restyled.io>
Co-authored-by: tennessee.carmelveilleux@gmail.com <tennessee.carmelveilleux@gmail.com>
diff --git a/examples/network-manager-app/linux/args.gni b/examples/network-manager-app/linux/args.gni
index 53bb53d..e97ddb1 100644
--- a/examples/network-manager-app/linux/args.gni
+++ b/examples/network-manager-app/linux/args.gni
@@ -22,3 +22,6 @@
]
chip_config_network_layer_ble = false
+
+# This enables AccessRestrictionList (ARL) support used by the NIM sample app
+chip_enable_access_restrictions = true
diff --git a/examples/network-manager-app/network-manager-common/network-manager-app.matter b/examples/network-manager-app/network-manager-common/network-manager-app.matter
index 6278382..57118d3 100644
--- a/examples/network-manager-app/network-manager-common/network-manager-app.matter
+++ b/examples/network-manager-app/network-manager-common/network-manager-app.matter
@@ -1623,16 +1623,23 @@
server cluster AccessControl {
emits event AccessControlEntryChanged;
emits event AccessControlExtensionChanged;
+ emits event AccessRestrictionEntryChanged;
+ emits event FabricRestrictionReviewUpdate;
callback attribute acl;
callback attribute extension;
callback attribute subjectsPerAccessControlEntry;
callback attribute targetsPerAccessControlEntry;
callback attribute accessControlEntriesPerFabric;
+ callback attribute commissioningARL;
+ callback attribute arl;
callback attribute generatedCommandList;
callback attribute acceptedCommandList;
callback attribute attributeList;
- ram attribute featureMap default = 0;
+ ram attribute featureMap default = 1;
callback attribute clusterRevision;
+
+ handle command ReviewFabricRestrictions;
+ handle command ReviewFabricRestrictionsResponse;
}
server cluster BasicInformation {
diff --git a/examples/network-manager-app/network-manager-common/network-manager-app.zap b/examples/network-manager-app/network-manager-common/network-manager-app.zap
index 1d27e3c..240cd49 100644
--- a/examples/network-manager-app/network-manager-common/network-manager-app.zap
+++ b/examples/network-manager-app/network-manager-common/network-manager-app.zap
@@ -314,6 +314,24 @@
"define": "ACCESS_CONTROL_CLUSTER",
"side": "server",
"enabled": 1,
+ "commands": [
+ {
+ "name": "ReviewFabricRestrictions",
+ "code": 0,
+ "mfgCode": null,
+ "source": "client",
+ "isIncoming": 1,
+ "isEnabled": 1
+ },
+ {
+ "name": "ReviewFabricRestrictionsResponse",
+ "code": 1,
+ "mfgCode": null,
+ "source": "server",
+ "isIncoming": 0,
+ "isEnabled": 1
+ }
+ ],
"attributes": [
{
"name": "ACL",
@@ -396,6 +414,38 @@
"reportableChange": 0
},
{
+ "name": "CommissioningARL",
+ "code": 5,
+ "mfgCode": null,
+ "side": "server",
+ "type": "array",
+ "included": 1,
+ "storageOption": "External",
+ "singleton": 0,
+ "bounded": 0,
+ "defaultValue": null,
+ "reportable": 1,
+ "minInterval": 1,
+ "maxInterval": 65534,
+ "reportableChange": 0
+ },
+ {
+ "name": "ARL",
+ "code": 6,
+ "mfgCode": null,
+ "side": "server",
+ "type": "array",
+ "included": 1,
+ "storageOption": "External",
+ "singleton": 0,
+ "bounded": 0,
+ "defaultValue": "",
+ "reportable": 1,
+ "minInterval": 1,
+ "maxInterval": 65534,
+ "reportableChange": 0
+ },
+ {
"name": "GeneratedCommandList",
"code": 65528,
"mfgCode": null,
@@ -453,7 +503,7 @@
"storageOption": "RAM",
"singleton": 0,
"bounded": 0,
- "defaultValue": "0",
+ "defaultValue": "1",
"reportable": 1,
"minInterval": 1,
"maxInterval": 65534,
@@ -490,6 +540,20 @@
"mfgCode": null,
"side": "server",
"included": 1
+ },
+ {
+ "name": "AccessRestrictionEntryChanged",
+ "code": 2,
+ "mfgCode": null,
+ "side": "server",
+ "included": 1
+ },
+ {
+ "name": "FabricRestrictionReviewUpdate",
+ "code": 3,
+ "mfgCode": null,
+ "side": "server",
+ "included": 1
}
]
},
diff --git a/examples/platform/linux/AppMain.cpp b/examples/platform/linux/AppMain.cpp
index 307b342..074f078 100644
--- a/examples/platform/linux/AppMain.cpp
+++ b/examples/platform/linux/AppMain.cpp
@@ -103,6 +103,10 @@
#include "AppMain.h"
#include "CommissionableInit.h"
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+#include "ExampleAccessRestrictionProvider.h"
+#endif
+
#if CHIP_DEVICE_LAYER_TARGET_DARWIN
#include <platform/Darwin/NetworkCommissioningDriver.h>
#if CHIP_DEVICE_CONFIG_ENABLE_WIFI
@@ -121,6 +125,7 @@
using namespace chip::Inet;
using namespace chip::Transport;
using namespace chip::app::Clusters;
+using namespace chip::Access;
// Network comissioning implementation
namespace {
@@ -180,6 +185,10 @@
app::Clusters::NetworkCommissioning::Instance sEthernetNetworkCommissioningInstance(kRootEndpointId, &sEthernetDriver);
#endif // CHIP_APP_MAIN_HAS_ETHERNET_DRIVER
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+auto exampleAccessRestrictionProvider = std::make_unique<ExampleAccessRestrictionProvider>();
+#endif
+
void EnableThreadNetworkCommissioning()
{
#if CHIP_APP_MAIN_HAS_THREAD_DRIVER
@@ -593,9 +602,27 @@
chip::app::RuntimeOptionsProvider::Instance().SetSimulateNoInternalTime(
LinuxDeviceOptions::GetInstance().mSimulateNoInternalTime);
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+ initParams.accessRestrictionProvider = exampleAccessRestrictionProvider.get();
+#endif
+
// Init ZCL Data Model and CHIP App Server
Server::GetInstance().Init(initParams);
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+ if (LinuxDeviceOptions::GetInstance().commissioningArlEntries.HasValue())
+ {
+ exampleAccessRestrictionProvider->SetCommissioningEntries(
+ LinuxDeviceOptions::GetInstance().commissioningArlEntries.Value());
+ }
+
+ if (LinuxDeviceOptions::GetInstance().arlEntries.HasValue())
+ {
+ // This example use of the ARL feature proactively installs the provided entries on fabric index 1
+ exampleAccessRestrictionProvider->SetEntries(1, LinuxDeviceOptions::GetInstance().arlEntries.Value());
+ }
+#endif
+
#if CONFIG_BUILD_FOR_HOST_UNIT_TEST
// Set ReadHandler Capacity for Subscriptions
chip::app::InteractionModelEngine::GetInstance()->SetHandlerCapacityForSubscriptions(
diff --git a/examples/platform/linux/BUILD.gn b/examples/platform/linux/BUILD.gn
index 1fcee18..336534a 100644
--- a/examples/platform/linux/BUILD.gn
+++ b/examples/platform/linux/BUILD.gn
@@ -19,6 +19,10 @@
import("${chip_root}/src/lib/lib.gni")
import("${chip_root}/src/tracing/tracing_args.gni")
+if (current_os != "nuttx") {
+ import("//build_overrides/jsoncpp.gni")
+}
+
declare_args() {
chip_enable_smoke_co_trigger = false
chip_enable_boolean_state_configuration_trigger = false
@@ -101,6 +105,10 @@
"${chip_root}/src/app/server",
]
+ if (current_os != "nuttx") {
+ public_deps += [ jsoncpp_root ]
+ }
+
if (chip_enable_pw_rpc) {
defines += [ "PW_RPC_ENABLED" ]
}
diff --git a/examples/platform/linux/ExampleAccessRestrictionProvider.h b/examples/platform/linux/ExampleAccessRestrictionProvider.h
new file mode 100644
index 0000000..731a8ae
--- /dev/null
+++ b/examples/platform/linux/ExampleAccessRestrictionProvider.h
@@ -0,0 +1,55 @@
+/*
+ *
+ * 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 <access/AccessRestrictionProvider.h>
+#include <app-common/zap-generated/cluster-objects.h>
+#include <app/EventLogging.h>
+
+namespace chip {
+namespace Access {
+
+class ExampleAccessRestrictionProvider : public AccessRestrictionProvider
+{
+public:
+ ExampleAccessRestrictionProvider() : AccessRestrictionProvider() {}
+
+ ~ExampleAccessRestrictionProvider() {}
+
+protected:
+ CHIP_ERROR DoRequestFabricRestrictionReview(const FabricIndex fabricIndex, uint64_t token, const std::vector<Entry> & arl)
+ {
+ // this example simply removes all restrictions and will generate AccessRestrictionEntryChanged events
+ Access::GetAccessControl().GetAccessRestrictionProvider()->SetEntries(fabricIndex, std::vector<Entry>{});
+
+ chip::app::Clusters::AccessControl::Events::FabricRestrictionReviewUpdate::Type event{ .token = token,
+ .fabricIndex = fabricIndex };
+ EventNumber eventNumber;
+ ReturnErrorOnFailure(chip::app::LogEvent(event, kRootEndpointId, eventNumber));
+
+ return CHIP_NO_ERROR;
+ }
+};
+
+} // namespace Access
+} // namespace chip
diff --git a/examples/platform/linux/Options.cpp b/examples/platform/linux/Options.cpp
index 9b83d12..6f8afea 100644
--- a/examples/platform/linux/Options.cpp
+++ b/examples/platform/linux/Options.cpp
@@ -26,6 +26,7 @@
#include <app/server/OnboardingCodesUtil.h>
#include <crypto/CHIPCryptoPAL.h>
+#include <json/json.h>
#include <lib/core/CHIPError.h>
#include <lib/support/Base64.h>
#include <lib/support/BytesToHex.h>
@@ -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,10 @@
kDeviceOption_TraceFile,
kDeviceOption_TraceLog,
kDeviceOption_TraceDecode,
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+ kDeviceOption_CommissioningArlEntries,
+ kDeviceOption_ArlEntries,
+#endif
kOptionCSRResponseCSRIncorrectType,
kOptionCSRResponseCSRNonceIncorrectType,
kOptionCSRResponseCSRNonceTooLong,
@@ -154,6 +164,10 @@
{ "trace_log", kArgumentRequired, kDeviceOption_TraceLog },
{ "trace_decode", kArgumentRequired, kDeviceOption_TraceDecode },
#endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+ { "commissioning-arl-entries", kArgumentRequired, kDeviceOption_CommissioningArlEntries },
+ { "arl-entries", kArgumentRequired, kDeviceOption_ArlEntries },
+#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 +294,14 @@
" --trace_decode <1/0>\n"
" A value of 1 enables traces decoding, 0 disables this (default 0).\n"
#endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+ " --commissioning-arl-entries <CommissioningARL JSON>\n"
+ " Enable ACL cluster access restrictions used during commissioning with the provided JSON. Example:\n"
+ " \"[{\\\"endpoint\\\": 1,\\\"cluster\\\": 1105,\\\"restrictions\\\": [{\\\"type\\\": 0,\\\"id\\\": 0}]}]\"\n"
+ " --arl-entries <ARL JSON>\n"
+ " Enable ACL cluster access restrictions applied to fabric index 1 with the provided JSON. Example:\n"
+ " \"[{\\\"endpoint\\\": 1,\\\"cluster\\\": 1105,\\\"restrictions\\\": [{\\\"type\\\": 0,\\\"id\\\": 0}]}]\"\n"
+#endif // CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
" --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 +342,39 @@
#endif
"\n";
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+bool ParseAccessRestrictionEntriesFromJson(const char * jsonString, std::vector<AccessRestrictionProvider::Entry> & 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++)
+ {
+ AccessRestrictionProvider::Entry entry;
+
+ entry.endpointNumber = static_cast<EndpointId>((*eIt)["endpoint"].asUInt());
+ entry.clusterId = static_cast<ClusterId>((*eIt)["cluster"].asUInt());
+
+ Json::Value restrictions = (*eIt)["restrictions"];
+ for (Json::Value::const_iterator rIt = restrictions.begin(); rIt != restrictions.end(); rIt++)
+ {
+ AccessRestrictionProvider::Restriction restriction;
+ restriction.restrictionType = static_cast<AccessRestrictionProvider::Type>((*rIt)["type"].asUInt());
+ if ((*rIt).isMember("id"))
+ {
+ restriction.id.SetValue((*rIt)["id"].asUInt());
+ }
+ 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<uint8_t> & outVector)
{
size_t maxBase64Size = BASE64_ENCODED_LEN(maxSize);
@@ -529,6 +584,28 @@
break;
#endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+ // TODO(#35189): change to use a path to JSON files instead
+ case kDeviceOption_CommissioningArlEntries: {
+ std::vector<AccessRestrictionProvider::Entry> entries;
+ retval = ParseAccessRestrictionEntriesFromJson(aValue, entries);
+ if (retval)
+ {
+ LinuxDeviceOptions::GetInstance().commissioningArlEntries.SetValue(std::move(entries));
+ }
+ }
+ break;
+ case kDeviceOption_ArlEntries: {
+ std::vector<AccessRestrictionProvider::Entry> entries;
+ retval = ParseAccessRestrictionEntriesFromJson(aValue, entries);
+ if (retval)
+ {
+ LinuxDeviceOptions::GetInstance().arlEntries.SetValue(std::move(entries));
+ }
+ }
+ 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 f921bee..11a9061 100644
--- a/examples/platform/linux/Options.h
+++ b/examples/platform/linux/Options.h
@@ -28,6 +28,7 @@
#include <string>
#include <vector>
+#include <access/AccessConfig.h>
#include <inet/InetInterface.h>
#include <lib/core/CHIPError.h>
#include <lib/core/Optional.h>
@@ -38,6 +39,10 @@
#include <credentials/DeviceAttestationCredsProvider.h>
#include <testing/CustomCSRResponse.h>
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+#include <access/AccessRestrictionProvider.h>
+#endif
+
struct LinuxDeviceOptions
{
chip::PayloadContents payload;
@@ -82,6 +87,10 @@
int32_t subscriptionCapacity = CHIP_IM_MAX_NUM_SUBSCRIPTIONS;
int32_t subscriptionResumptionRetryIntervalSec = -1;
#endif
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+ chip::Optional<std::vector<chip::Access::AccessRestrictionProvider::Entry>> commissioningArlEntries;
+ chip::Optional<std::vector<chip::Access::AccessRestrictionProvider::Entry>> arlEntries;
+#endif
static LinuxDeviceOptions & GetInstance();
};
diff --git a/scripts/tools/check_includes_config.py b/scripts/tools/check_includes_config.py
index 2af375d..b5195f4 100644
--- a/scripts/tools/check_includes_config.py
+++ b/scripts/tools/check_includes_config.py
@@ -185,4 +185,5 @@
'src/app/icd/client/DefaultICDStorageKey.h': {'vector'},
'src/controller/CHIPDeviceController.cpp': {'string'},
'src/qrcodetool/setup_payload_commands.cpp': {'string'},
+ 'src/access/AccessRestrictionProvider.h': {'vector', 'map'},
}
diff --git a/src/access/AccessConfig.h b/src/access/AccessConfig.h
new file mode 100644
index 0000000..b9318a1
--- /dev/null
+++ b/src/access/AccessConfig.h
@@ -0,0 +1,22 @@
+/*
+ *
+ * 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
+
+#if CHIP_HAVE_CONFIG_H
+#include <access/AccessBuildConfig.h>
+#endif
diff --git a/src/access/AccessControl.cpp b/src/access/AccessControl.cpp
index fcb5a43..8302fb0 100644
--- a/src/access/AccessControl.cpp
+++ b/src/access/AccessControl.cpp
@@ -181,7 +181,7 @@
return 'w';
case RequestType::kCommandInvokeRequest:
return 'i';
- case RequestType::kEventReadOrSubscribeRequest:
+ case RequestType::kEventReadRequest:
return 'e';
default:
return '?';
@@ -325,7 +325,11 @@
bool AccessControl::IsAccessRestrictionListSupported() const
{
- return false; // not yet supported
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+ return mAccessRestrictionProvider != nullptr;
+#else
+ return false;
+#endif
}
CHIP_ERROR AccessControl::Check(const SubjectDescriptor & subjectDescriptor, const RequestPath & requestPath,
@@ -333,6 +337,21 @@
{
VerifyOrReturnError(IsInitialized(), CHIP_ERROR_INCORRECT_STATE);
+ CHIP_ERROR result = CheckACL(subjectDescriptor, requestPath, requestPrivilege);
+
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+ if (result == CHIP_NO_ERROR)
+ {
+ result = CheckARL(subjectDescriptor, requestPath, requestPrivilege);
+ }
+#endif
+
+ return result;
+}
+
+CHIP_ERROR AccessControl::CheckACL(const SubjectDescriptor & subjectDescriptor, const RequestPath & requestPath,
+ Privilege requestPrivilege)
+{
#if CHIP_PROGRESS_LOGGING && CHIP_CONFIG_ACCESS_CONTROL_POLICY_LOGGING_VERBOSITY > 1
{
constexpr size_t kMaxCatsToLog = 6;
@@ -347,11 +366,6 @@
}
#endif // CHIP_PROGRESS_LOGGING && CHIP_CONFIG_ACCESS_CONTROL_POLICY_LOGGING_VERBOSITY > 1
- if (IsAccessRestrictionListSupported())
- {
- VerifyOrReturnError(requestPath.requestType != RequestType::kRequestTypeUnknown, CHIP_ERROR_INVALID_ARGUMENT);
- }
-
{
CHIP_ERROR result = mDelegate->Check(subjectDescriptor, requestPath, requestPrivilege);
if (result != CHIP_ERROR_NOT_IMPLEMENTED)
@@ -368,6 +382,7 @@
(result == CHIP_ERROR_ACCESS_DENIED) ? "denied" : "error");
}
#endif // CHIP_CONFIG_ACCESS_CONTROL_POLICY_LOGGING_VERBOSITY > 0
+
return result;
}
}
@@ -497,6 +512,45 @@
return CHIP_ERROR_ACCESS_DENIED;
}
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+CHIP_ERROR AccessControl::CheckARL(const SubjectDescriptor & subjectDescriptor, const RequestPath & requestPath,
+ Privilege requestPrivilege)
+{
+ CHIP_ERROR result = CHIP_NO_ERROR;
+
+ VerifyOrReturnError(requestPath.requestType != RequestType::kRequestTypeUnknown, CHIP_ERROR_INVALID_ARGUMENT);
+
+ if (!IsAccessRestrictionListSupported())
+ {
+ // Access Restriction support is compiled in, but not configured/enabled. Nothing to restrict.
+ return CHIP_NO_ERROR;
+ }
+
+ if (subjectDescriptor.isCommissioning)
+ {
+ result = mAccessRestrictionProvider->CheckForCommissioning(subjectDescriptor, requestPath);
+ }
+ else
+ {
+ result = mAccessRestrictionProvider->Check(subjectDescriptor, requestPath);
+ }
+
+ if (result != CHIP_NO_ERROR)
+ {
+ ChipLogProgress(DataManagement, "AccessControl: %s",
+#if 0
+ // TODO(#35177): new error code coming when access check plumbing are fixed in callers
+ (result == CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL) ? "denied (restricted)" : "denied (restriction error)");
+#else
+ (result == CHIP_ERROR_ACCESS_DENIED) ? "denied (restricted)" : "denied (restriction error)");
+#endif
+ return result;
+ }
+
+ return result;
+}
+#endif
+
#if CHIP_ACCESS_CONTROL_DUMP_ENABLED
CHIP_ERROR AccessControl::Dump(const Entry & entry)
{
diff --git a/src/access/AccessControl.h b/src/access/AccessControl.h
index a7c3472..df98686 100644
--- a/src/access/AccessControl.h
+++ b/src/access/AccessControl.h
@@ -18,6 +18,12 @@
#pragma once
+#include <access/AccessConfig.h>
+
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+#include "AccessRestrictionProvider.h"
+#endif
+
#include "Privilege.h"
#include "RequestPath.h"
#include "SubjectDescriptor.h"
@@ -627,6 +633,16 @@
// 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 SetAccessRestrictionProvider(AccessRestrictionProvider * accessRestrictionProvider)
+ {
+ mAccessRestrictionProvider = accessRestrictionProvider;
+ }
+
+ AccessRestrictionProvider * GetAccessRestrictionProvider() { return mAccessRestrictionProvider; }
+#endif
+
/**
* Check whether or not Access Restriction List is supported.
*
@@ -638,6 +654,8 @@
* Check whether access (by a subject descriptor, to a request path,
* requiring a privilege) should be allowed or denied.
*
+ * If an AccessRestrictionProvider 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.
@@ -656,12 +674,29 @@
void NotifyEntryChanged(const SubjectDescriptor * subjectDescriptor, FabricIndex fabric, size_t index, const Entry * entry,
EntryListener::ChangeType changeType);
+ /**
+ * Check ACL for whether access (by a subject descriptor, to a request path,
+ * requiring a privilege) should be allowed or denied.
+ */
+ CHIP_ERROR CheckACL(const SubjectDescriptor & subjectDescriptor, const RequestPath & requestPath, Privilege requestPrivilege);
+
+ /**
+ * Check CommissioningARL or ARL (as appropriate) for whether access (by a
+ * subject descriptor, to a request path, requiring a privilege) should
+ * be allowed or denied.
+ */
+ CHIP_ERROR CheckARL(const SubjectDescriptor & subjectDescriptor, const RequestPath & requestPath, Privilege requestPrivilege);
+
private:
Delegate * mDelegate = nullptr;
DeviceTypeResolver * mDeviceTypeResolver = nullptr;
EntryListener * mEntryListener = nullptr;
+
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+ AccessRestrictionProvider * mAccessRestrictionProvider;
+#endif
};
/**
diff --git a/src/access/AccessRestrictionProvider.cpp b/src/access/AccessRestrictionProvider.cpp
new file mode 100644
index 0000000..23e8082
--- /dev/null
+++ b/src/access/AccessRestrictionProvider.cpp
@@ -0,0 +1,249 @@
+/*
+ *
+ * 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 "AccessRestrictionProvider.h"
+
+#include <algorithm>
+#include <lib/core/Global.h>
+
+using namespace chip::Platform;
+
+namespace chip {
+namespace Access {
+
+void AccessRestrictionProvider::AddListener(Listener & listener)
+{
+ if (mListeners == nullptr)
+ {
+ mListeners = &listener;
+ listener.mNext = nullptr;
+ return;
+ }
+
+ for (Listener * l = mListeners; /**/; l = l->mNext)
+ {
+ if (l == &listener)
+ {
+ return;
+ }
+
+ if (l->mNext == nullptr)
+ {
+ l->mNext = &listener;
+ listener.mNext = nullptr;
+ return;
+ }
+ }
+}
+
+void AccessRestrictionProvider::RemoveListener(Listener & listener)
+{
+ if (mListeners == &listener)
+ {
+ mListeners = listener.mNext;
+ listener.mNext = nullptr;
+ return;
+ }
+
+ for (Listener * l = mListeners; l != nullptr; l = l->mNext)
+ {
+ if (l->mNext == &listener)
+ {
+ l->mNext = listener.mNext;
+ listener.mNext = nullptr;
+ return;
+ }
+ }
+}
+
+CHIP_ERROR AccessRestrictionProvider::SetCommissioningEntries(const std::vector<Entry> & entries)
+{
+ for (auto & entry : entries)
+ {
+ if (!mExceptionChecker.AreRestrictionsAllowed(entry.endpointNumber, entry.clusterId))
+ {
+ ChipLogError(DataManagement, "AccessRestrictionProvider: invalid entry");
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+ }
+
+ mCommissioningEntries = entries;
+
+ for (Listener * listener = mListeners; listener != nullptr; listener = listener->mNext)
+ {
+ listener->MarkCommissioningRestrictionListChanged();
+ }
+
+ return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR AccessRestrictionProvider::SetEntries(const FabricIndex fabricIndex, const std::vector<Entry> & entries)
+{
+ std::vector<Entry> updatedEntries;
+
+ for (auto & entry : entries)
+ {
+ if (!mExceptionChecker.AreRestrictionsAllowed(entry.endpointNumber, entry.clusterId))
+ {
+ ChipLogError(DataManagement, "AccessRestrictionProvider: invalid entry");
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ Entry updatedEntry = entry;
+ updatedEntry.fabricIndex = fabricIndex;
+ updatedEntries.push_back(updatedEntry);
+ }
+
+ mFabricEntries[fabricIndex] = std::move(updatedEntries);
+
+ for (Listener * listener = mListeners; listener != nullptr; listener = listener->mNext)
+ {
+ listener->MarkRestrictionListChanged(fabricIndex);
+ }
+
+ return CHIP_NO_ERROR;
+}
+
+bool AccessRestrictionProvider::StandardAccessRestrictionExceptionChecker::AreRestrictionsAllowed(EndpointId endpoint,
+ ClusterId cluster)
+{
+ if (endpoint != kRootEndpointId &&
+ (cluster == app::Clusters::WiFiNetworkManagement::Id || cluster == app::Clusters::ThreadBorderRouterManagement::Id ||
+ cluster == app::Clusters::ThreadNetworkDirectory::Id))
+ {
+ return true;
+ }
+
+ return false;
+}
+
+CHIP_ERROR AccessRestrictionProvider::CheckForCommissioning(const SubjectDescriptor & subjectDescriptor,
+ const RequestPath & requestPath)
+{
+ return DoCheck(mCommissioningEntries, subjectDescriptor, requestPath);
+}
+
+CHIP_ERROR AccessRestrictionProvider::Check(const SubjectDescriptor & subjectDescriptor, const RequestPath & requestPath)
+{
+ return DoCheck(mFabricEntries[subjectDescriptor.fabricIndex], subjectDescriptor, requestPath);
+}
+
+CHIP_ERROR AccessRestrictionProvider::DoCheck(const std::vector<Entry> & entries, const SubjectDescriptor & subjectDescriptor,
+ const RequestPath & requestPath)
+{
+ if (!mExceptionChecker.AreRestrictionsAllowed(requestPath.endpoint, requestPath.cluster))
+ {
+ ChipLogProgress(DataManagement, "AccessRestrictionProvider: skipping checks for unrestrictable request path");
+ return CHIP_NO_ERROR;
+ }
+
+ ChipLogProgress(DataManagement, "AccessRestrictionProvider: action %d", to_underlying(requestPath.requestType));
+
+ if (requestPath.requestType == RequestType::kRequestTypeUnknown)
+ {
+ ChipLogError(DataManagement, "AccessRestrictionProvider: RequestPath type is unknown");
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ // wildcard event subscriptions are allowed since wildcard is only used when setting up the subscription and
+ // we want that request to succeed (when generating the report, this method will be called with the specific
+ // event id). All other requests require an entity id
+ if (!requestPath.entityId.has_value())
+ {
+ if (requestPath.requestType == RequestType::kEventReadRequest)
+ {
+ return CHIP_NO_ERROR;
+ }
+ else
+ {
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+ }
+
+ for (auto & entry : entries)
+ {
+ 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)
+ {
+#if 0
+ // TODO(#35177): use new ARL error code when access checks are fixed
+ return CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL;
+#else
+ return CHIP_ERROR_ACCESS_DENIED;
+#endif
+ }
+ break;
+ case Type::kAttributeWriteForbidden:
+ if (requestPath.requestType == RequestType::kAttributeWriteRequest)
+ {
+#if 0
+ // TODO(#35177): use new ARL error code when access checks are fixed
+ return CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL;
+#else
+ return CHIP_ERROR_ACCESS_DENIED;
+#endif
+ }
+ break;
+ case Type::kCommandForbidden:
+ if (requestPath.requestType == RequestType::kCommandInvokeRequest)
+ {
+#if 0
+ // TODO(#35177): use new ARL error code when access checks are fixed
+ return CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL;
+#else
+ return CHIP_ERROR_ACCESS_DENIED;
+#endif
+ }
+ break;
+ case Type::kEventForbidden:
+ if (requestPath.requestType == RequestType::kEventReadRequest)
+ {
+#if 0
+ // TODO(#35177): use new ARL error code when access checks are fixed
+ return CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL;
+#else
+ return CHIP_ERROR_ACCESS_DENIED;
+#endif
+ }
+ break;
+ }
+ }
+ }
+
+ return CHIP_NO_ERROR;
+}
+
+} // namespace Access
+} // namespace chip
diff --git a/src/access/AccessRestrictionProvider.h b/src/access/AccessRestrictionProvider.h
new file mode 100644
index 0000000..705d9c3
--- /dev/null
+++ b/src/access/AccessRestrictionProvider.h
@@ -0,0 +1,275 @@
+/*
+ *
+ * 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 "Privilege.h"
+#include "RequestPath.h"
+#include "SubjectDescriptor.h"
+#include <algorithm>
+#include <app-common/zap-generated/cluster-objects.h>
+#include <cstdint>
+#include <lib/core/CHIPError.h>
+#include <lib/core/DataModelTypes.h>
+#include <lib/core/Optional.h>
+#include <lib/support/CHIPMem.h>
+#include <map>
+#include <memory>
+#include <protocols/interaction_model/Constants.h>
+#include <vector>
+
+namespace chip {
+namespace Access {
+
+class AccessRestrictionProvider
+{
+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<uint32_t> 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<Restriction> restrictions;
+ };
+
+ /**
+ * Defines the interface for a checker for access restriction exceptions.
+ */
+ class AccessRestrictionExceptionChecker
+ {
+ public:
+ virtual ~AccessRestrictionExceptionChecker() = default;
+
+ /**
+ * Check if any restrictions are allowed to be applied on the given endpoint and cluster
+ * because of constraints against their use in ARLs.
+ *
+ * @retval true if ARL checks are allowed to be applied to the cluster on the endpoint, false otherwise
+ */
+ virtual bool AreRestrictionsAllowed(EndpointId endpoint, ClusterId cluster) = 0;
+ };
+
+ /**
+ * Define a standard implementation of the AccessRestrictionExceptionChecker interface
+ * which is the default implementation used by AccessResrictionProvider.
+ */
+ class StandardAccessRestrictionExceptionChecker : public AccessRestrictionExceptionChecker
+ {
+ public:
+ StandardAccessRestrictionExceptionChecker() = default;
+ ~StandardAccessRestrictionExceptionChecker() = default;
+
+ bool AreRestrictionsAllowed(EndpointId endpoint, ClusterId cluster) override;
+ };
+
+ /**
+ * Used to notify of changes in the access restriction list and active reviews.
+ */
+ class Listener
+ {
+ public:
+ virtual ~Listener() = default;
+
+ /**
+ * Notifies of a change in the commissioning access restriction list.
+ */
+ virtual void MarkCommissioningRestrictionListChanged() = 0;
+
+ /**
+ * Notifies of a change in the access restriction list.
+ *
+ * @param [in] fabricIndex The index of the fabric in which the list has changed.
+ */
+ virtual void MarkRestrictionListChanged(FabricIndex fabricIndex) = 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 Optional instructions to be displayed to the user.
+ * @param [in] redirectUrl An optional URL to redirect the user to for more information.
+ */
+ virtual void OnFabricRestrictionReviewUpdate(FabricIndex fabricIndex, uint64_t token, Optional<CharSpan> instruction,
+ Optional<CharSpan> redirectUrl) = 0;
+
+ private:
+ Listener * mNext = nullptr;
+
+ friend class AccessRestrictionProvider;
+ };
+
+ AccessRestrictionProvider() = default;
+ virtual ~AccessRestrictionProvider() = default;
+
+ AccessRestrictionProvider(const AccessRestrictionProvider &) = delete;
+ AccessRestrictionProvider & operator=(const AccessRestrictionProvider &) = delete;
+
+ /**
+ * Set the restriction entries that are to be used during commissioning when there is no accessing fabric.
+ *
+ * @param [in] entries The entries to set.
+ */
+ CHIP_ERROR SetCommissioningEntries(const std::vector<Entry> & entries);
+
+ /**
+ * Set the restriction entries for a fabric.
+ *
+ * @param [in] fabricIndex The index of the fabric for which to create entries.
+ * @param [in] entries The entries to set for the fabric.
+ */
+ CHIP_ERROR SetEntries(const FabricIndex, const std::vector<Entry> & entries);
+
+ /**
+ * 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(Listener & 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(Listener & listener);
+
+ /**
+ * Check whether access by a subject descriptor to a request path should be restricted (denied) for the given action
+ * during commissioning by using the CommissioningEntries.
+ *
+ * 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 CheckForCommissioning(const SubjectDescriptor & subjectDescriptor, const RequestPath & requestPath);
+
+ /**
+ * 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<Entry> & arl, uint64_t & token)
+ {
+ token = mNextToken++;
+ return DoRequestFabricRestrictionReview(fabricIndex, token, arl);
+ }
+
+ /**
+ * Get the commissioning restriction entries.
+ *
+ * @retval the commissioning restriction entries.
+ */
+ const std::vector<Entry> & GetCommissioningEntries() const { return mCommissioningEntries; }
+
+ /**
+ * Get the restriction entries for a fabric.
+ *
+ * @param [in] fabricIndex the index of the fabric for which to get entries.
+ * @param [out] entries vector to hold the entries.
+ */
+ CHIP_ERROR GetEntries(const FabricIndex fabricIndex, std::vector<Entry> & entries) const
+ {
+ auto it = mFabricEntries.find(fabricIndex);
+ if (it == mFabricEntries.end())
+ {
+ return CHIP_ERROR_NOT_FOUND;
+ }
+
+ entries = (it->second);
+
+ return CHIP_NO_ERROR;
+ }
+
+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<Entry> & arl) = 0;
+
+private:
+ /**
+ * Perform the access restriction check using the given entries.
+ */
+ CHIP_ERROR DoCheck(const std::vector<Entry> & entries, const SubjectDescriptor & subjectDescriptor,
+ const RequestPath & requestPath);
+
+ uint64_t mNextToken = 1;
+ Listener * mListeners = nullptr;
+ StandardAccessRestrictionExceptionChecker mExceptionChecker;
+ std::vector<Entry> mCommissioningEntries;
+ std::map<FabricIndex, std::vector<Entry>> mFabricEntries;
+};
+
+} // namespace Access
+} // namespace chip
diff --git a/src/access/BUILD.gn b/src/access/BUILD.gn
index 8d2c4b5..3f3aaaf 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 @@
]
public_deps = [
+ ":access_config",
"${chip_root}/src/lib/core",
"${chip_root}/src/lib/core:types",
]
@@ -43,10 +63,18 @@
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 += [
+ "AccessRestrictionProvider.cpp",
+ "AccessRestrictionProvider.h",
+ ]
+ }
}
diff --git a/src/access/RequestPath.h b/src/access/RequestPath.h
index af791d7..920d5fe 100644
--- a/src/access/RequestPath.h
+++ b/src/access/RequestPath.h
@@ -30,7 +30,7 @@
kAttributeReadRequest,
kAttributeWriteRequest,
kCommandInvokeRequest,
- kEventReadOrSubscribeRequest
+ kEventReadRequest
};
struct RequestPath
diff --git a/src/access/SubjectDescriptor.h b/src/access/SubjectDescriptor.h
index ec6abec..9cde410 100644
--- a/src/access/SubjectDescriptor.h
+++ b/src/access/SubjectDescriptor.h
@@ -42,6 +42,10 @@
// CASE Authenticated Tags (CATs) only valid if auth mode is CASE.
CATValues cats;
+
+ // Whether the subject is currently a pending commissionee. See `IsCommissioning`
+ // definition in Core Specification's ACL Architecture pseudocode.
+ bool isCommissioning = false;
};
} // namespace Access
diff --git a/src/access/access.gni b/src/access/access.gni
new file mode 100644
index 0000000..bd18f13
--- /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 d8b43e6..64226ad 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_root}/src/lib/support:test_utils",
"${dir_pw_unit_test}",
]
+
+ if (chip_enable_access_restrictions) {
+ test_sources += [ "TestAccessRestrictionProvider.cpp" ]
+ }
}
diff --git a/src/access/tests/TestAccessRestrictionProvider.cpp b/src/access/tests/TestAccessRestrictionProvider.cpp
new file mode 100644
index 0000000..ddf58ae
--- /dev/null
+++ b/src/access/tests/TestAccessRestrictionProvider.cpp
@@ -0,0 +1,722 @@
+/*
+ *
+ * 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/AccessRestrictionProvider.h"
+#include "access/examples/ExampleAccessControlDelegate.h"
+
+#include <pw_unit_test/framework.h>
+
+#include <app-common/zap-generated/ids/Attributes.h>
+#include <lib/core/CHIPCore.h>
+#include <lib/core/StringBuilderAdapters.h>
+namespace chip {
+namespace Access {
+
+class TestAccessRestrictionProvider : public AccessRestrictionProvider
+{
+ CHIP_ERROR DoRequestFabricRestrictionReview(const FabricIndex fabricIndex, uint64_t token, const std::vector<Entry> & arl)
+ {
+ return CHIP_NO_ERROR;
+ }
+};
+
+AccessControl accessControl;
+TestAccessRestrictionProvider accessRestrictionProvider;
+
+constexpr ClusterId kNetworkCommissioningCluster = app::Clusters::NetworkCommissioning::Id;
+constexpr ClusterId kDescriptorCluster = app::Clusters::Descriptor::Id;
+constexpr ClusterId kOnOffCluster = app::Clusters::OnOff::Id;
+
+// Clusters allowed to have restrictions
+constexpr ClusterId kWiFiNetworkManagementCluster = app::Clusters::WiFiNetworkManagement::Id;
+constexpr ClusterId kThreadBorderRouterMgmtCluster = app::Clusters::ThreadBorderRouterManagement::Id;
+constexpr ClusterId kThreadNetworkDirectoryCluster = app::Clusters::ThreadNetworkDirectory::Id;
+
+constexpr NodeId kOperationalNodeId1 = 0x1111111111111111;
+constexpr NodeId kOperationalNodeId2 = 0x2222222222222222;
+constexpr NodeId kOperationalNodeId3 = 0x3333333333333333;
+
+bool operator==(const AccessRestrictionProvider::Restriction & lhs, const AccessRestrictionProvider::Restriction & rhs)
+{
+ return lhs.restrictionType == rhs.restrictionType && lhs.id == rhs.id;
+}
+
+bool operator==(const AccessRestrictionProvider::Entry & lhs, const AccessRestrictionProvider::Entry & rhs)
+{
+ return lhs.fabricIndex == rhs.fabricIndex && lhs.endpointNumber == rhs.endpointNumber && lhs.clusterId == rhs.clusterId &&
+ lhs.restrictions == rhs.restrictions;
+}
+
+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::kEventReadRequest, .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::kEventReadRequest, .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::kEventReadRequest, .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
+ {
+ accessRestrictionProvider.SetCommissioningEntries(std::vector<AccessRestrictionProvider::Entry>());
+ accessRestrictionProvider.SetEntries(0, std::vector<AccessRestrictionProvider::Entry>());
+ accessRestrictionProvider.SetEntries(1, std::vector<AccessRestrictionProvider::Entry>());
+ accessRestrictionProvider.SetEntries(2, std::vector<AccessRestrictionProvider::Entry>());
+ }
+
+ static void SetUpTestSuite()
+ {
+ ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR);
+ AccessControl::Delegate * delegate = Examples::GetAccessControlDelegate();
+ SetAccessControl(accessControl);
+ GetAccessControl().SetAccessRestrictionProvider(&accessRestrictionProvider);
+ 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 failure when adding restrictons on endpoint 0 (any cluster, including those allowed on other endpoints)
+TEST_F(TestAccessRestriction, InvalidRestrictionsOnEndpointZeroTest)
+{
+ std::vector<AccessRestrictionProvider::Entry> entries;
+ AccessRestrictionProvider::Entry entry;
+ entry.endpointNumber = 0;
+ entry.fabricIndex = 1;
+ entry.restrictions.push_back({ .restrictionType = AccessRestrictionProvider::Type::kAttributeAccessForbidden });
+
+ entry.clusterId = kDescriptorCluster;
+ entries.push_back(entry);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries), CHIP_ERROR_INVALID_ARGUMENT);
+
+ entries.clear();
+ entry.clusterId = kNetworkCommissioningCluster;
+ entries.push_back(entry);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries), CHIP_ERROR_INVALID_ARGUMENT);
+
+ entries.clear();
+ entry.clusterId = kWiFiNetworkManagementCluster;
+ entries.push_back(entry);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries), CHIP_ERROR_INVALID_ARGUMENT);
+
+ entries.clear();
+ entry.clusterId = kThreadBorderRouterMgmtCluster;
+ entries.push_back(entry);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries), CHIP_ERROR_INVALID_ARGUMENT);
+
+ entries.clear();
+ entry.clusterId = kThreadNetworkDirectoryCluster;
+ entries.push_back(entry);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries), CHIP_ERROR_INVALID_ARGUMENT);
+
+ // also test a cluster on endpoint 0 that isnt in the special allowed list
+ entries.clear();
+ entry.clusterId = kOnOffCluster;
+ entries.push_back(entry);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries), CHIP_ERROR_INVALID_ARGUMENT);
+}
+
+// ensure no failure adding restrictions on endpoint 1 for allowed clusters only:
+// wifi network management, thread border router, thread network directory
+TEST_F(TestAccessRestriction, ValidRestrictionsOnEndpointOneTest)
+{
+ std::vector<AccessRestrictionProvider::Entry> entries;
+ AccessRestrictionProvider::Entry entry;
+ entry.endpointNumber = 1;
+ entry.fabricIndex = 1;
+ entry.restrictions.push_back({ .restrictionType = AccessRestrictionProvider::Type::kAttributeAccessForbidden });
+
+ entry.clusterId = kWiFiNetworkManagementCluster;
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries), CHIP_NO_ERROR);
+
+ entries.clear();
+ entry.clusterId = kThreadBorderRouterMgmtCluster;
+ entries.push_back(entry);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries), CHIP_NO_ERROR);
+
+ entries.clear();
+ entry.clusterId = kThreadNetworkDirectoryCluster;
+ entries.push_back(entry);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries), CHIP_NO_ERROR);
+
+ // also test a cluster on endpoint 1 that isnt in the special allowed list
+ entries.clear();
+ entry.clusterId = kOnOffCluster;
+ entries.push_back(entry);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries), CHIP_ERROR_INVALID_ARGUMENT);
+}
+
+TEST_F(TestAccessRestriction, InvalidRestrictionsOnEndpointOneTest)
+{
+ std::vector<AccessRestrictionProvider::Entry> entries;
+ AccessRestrictionProvider::Entry entry;
+ entry.endpointNumber = 1;
+ entry.fabricIndex = 1;
+ entry.restrictions.push_back({ .restrictionType = AccessRestrictionProvider::Type::kAttributeAccessForbidden });
+ entry.clusterId = kOnOffCluster;
+ entries.push_back(entry);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries), CHIP_ERROR_INVALID_ARGUMENT);
+}
+
+constexpr CheckData accessAttributeRestrictionTestData[] = {
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kAttributeWriteRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = false },
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kAttributeReadRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = false },
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kCommandInvokeRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = true },
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kEventReadRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = true },
+};
+
+TEST_F(TestAccessRestriction, AccessAttributeRestrictionTest)
+{
+ std::vector<AccessRestrictionProvider::Entry> entries;
+ AccessRestrictionProvider::Entry entry;
+ entry.fabricIndex = 1;
+ entry.endpointNumber = 1;
+ entry.clusterId = kWiFiNetworkManagementCluster;
+ entry.restrictions.push_back({ .restrictionType = AccessRestrictionProvider::Type::kAttributeAccessForbidden });
+
+ // test wildcarded entity id
+ entries.push_back(entry);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries), CHIP_NO_ERROR);
+ RunChecks(accessAttributeRestrictionTestData, ArraySize(accessAttributeRestrictionTestData));
+
+ // test specific entity id
+ entries.clear();
+ entry.restrictions[0].id.SetValue(1);
+ entries.push_back(entry);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries), CHIP_NO_ERROR);
+ RunChecks(accessAttributeRestrictionTestData, ArraySize(accessAttributeRestrictionTestData));
+}
+
+constexpr CheckData writeAttributeRestrictionTestData[] = {
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kAttributeWriteRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = false },
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kAttributeReadRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = true },
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kCommandInvokeRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = true },
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kEventReadRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = true },
+};
+
+TEST_F(TestAccessRestriction, WriteAttributeRestrictionTest)
+{
+ std::vector<AccessRestrictionProvider::Entry> entries;
+ AccessRestrictionProvider::Entry entry;
+ entry.fabricIndex = 1;
+ entry.endpointNumber = 1;
+ entry.clusterId = kWiFiNetworkManagementCluster;
+ entry.restrictions.push_back({ .restrictionType = AccessRestrictionProvider::Type::kAttributeWriteForbidden });
+
+ // test wildcarded entity id
+ entries.push_back(entry);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries), CHIP_NO_ERROR);
+ RunChecks(writeAttributeRestrictionTestData, ArraySize(writeAttributeRestrictionTestData));
+
+ // test specific entity id
+ entries.clear();
+ entry.restrictions[0].id.SetValue(1);
+ entries.push_back(entry);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries), CHIP_NO_ERROR);
+ RunChecks(writeAttributeRestrictionTestData, ArraySize(writeAttributeRestrictionTestData));
+}
+
+constexpr CheckData commandAttributeRestrictionTestData[] = {
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kAttributeWriteRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = true },
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kAttributeReadRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = true },
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kCommandInvokeRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = false },
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kEventReadRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = true },
+};
+
+TEST_F(TestAccessRestriction, CommandRestrictionTest)
+{
+ std::vector<AccessRestrictionProvider::Entry> entries;
+ AccessRestrictionProvider::Entry entry;
+ entry.fabricIndex = 1;
+ entry.endpointNumber = 1;
+ entry.clusterId = kWiFiNetworkManagementCluster;
+ entry.restrictions.push_back({ .restrictionType = AccessRestrictionProvider::Type::kCommandForbidden });
+
+ // test wildcarded entity id
+ entries.push_back(entry);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries), CHIP_NO_ERROR);
+ RunChecks(commandAttributeRestrictionTestData, ArraySize(commandAttributeRestrictionTestData));
+
+ // test specific entity id
+ entries.clear();
+ entry.restrictions[0].id.SetValue(1);
+ entries.push_back(entry);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries), CHIP_NO_ERROR);
+ RunChecks(commandAttributeRestrictionTestData, ArraySize(commandAttributeRestrictionTestData));
+}
+
+constexpr CheckData eventAttributeRestrictionTestData[] = {
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kAttributeWriteRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = true },
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kAttributeReadRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = true },
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kCommandInvokeRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = true },
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kEventReadRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = false },
+};
+
+TEST_F(TestAccessRestriction, EventRestrictionTest)
+{
+ std::vector<AccessRestrictionProvider::Entry> entries;
+ AccessRestrictionProvider::Entry entry;
+ entry.fabricIndex = 1;
+ entry.endpointNumber = 1;
+ entry.clusterId = kWiFiNetworkManagementCluster;
+ entry.restrictions.push_back({ .restrictionType = AccessRestrictionProvider::Type::kEventForbidden });
+
+ // test wildcarded entity id
+ entries.push_back(entry);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries), CHIP_NO_ERROR);
+ RunChecks(eventAttributeRestrictionTestData, ArraySize(eventAttributeRestrictionTestData));
+
+ // test specific entity id
+ entries.clear();
+ entry.restrictions[0].id.SetValue(1);
+ entries.push_back(entry);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries), CHIP_NO_ERROR);
+ RunChecks(eventAttributeRestrictionTestData, ArraySize(eventAttributeRestrictionTestData));
+}
+
+constexpr CheckData combinedRestrictionTestData[] = {
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kAttributeWriteRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = false },
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kAttributeReadRequest,
+ .entityId = 2 },
+ .privilege = Privilege::kAdminister,
+ .allow = false },
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kAttributeWriteRequest,
+ .entityId = 3 },
+ .privilege = Privilege::kAdminister,
+ .allow = true },
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kAttributeReadRequest,
+ .entityId = 4 },
+ .privilege = Privilege::kAdminister,
+ .allow = true },
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kAttributeReadRequest,
+ .entityId = 3 },
+ .privilege = Privilege::kAdminister,
+ .allow = true },
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kCommandInvokeRequest,
+ .entityId = 4 },
+ .privilege = Privilege::kAdminister,
+ .allow = true },
+ { .subjectDescriptor = { .fabricIndex = 1, .authMode = AuthMode::kCase, .subject = kOperationalNodeId1 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kEventReadRequest,
+ .entityId = 5 },
+ .privilege = Privilege::kAdminister,
+ .allow = true },
+ { .subjectDescriptor = { .fabricIndex = 2, .authMode = AuthMode::kCase, .subject = kOperationalNodeId2 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kCommandInvokeRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = false },
+ { .subjectDescriptor = { .fabricIndex = 2, .authMode = AuthMode::kCase, .subject = kOperationalNodeId2 },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .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
+ std::vector<AccessRestrictionProvider::Entry> entries1;
+ AccessRestrictionProvider::Entry entry1;
+ entry1.fabricIndex = 1;
+ entry1.endpointNumber = 1;
+ entry1.clusterId = kWiFiNetworkManagementCluster;
+ entry1.restrictions.push_back({ .restrictionType = AccessRestrictionProvider::Type::kAttributeWriteForbidden });
+ entry1.restrictions[0].id.SetValue(1);
+ entry1.restrictions.push_back({ .restrictionType = AccessRestrictionProvider::Type::kAttributeAccessForbidden });
+ entry1.restrictions[1].id.SetValue(2);
+ entries1.push_back(entry1);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries1), 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
+ std::vector<AccessRestrictionProvider::Entry> entries2;
+ AccessRestrictionProvider::Entry entry2;
+ entry2.fabricIndex = 2;
+ entry2.endpointNumber = 1;
+ entry2.clusterId = kWiFiNetworkManagementCluster;
+ entry2.restrictions.push_back({ .restrictionType = AccessRestrictionProvider::Type::kCommandForbidden });
+ entry2.restrictions[0].id.SetValue(1);
+ entry2.restrictions.push_back({ .restrictionType = AccessRestrictionProvider::Type::kCommandForbidden });
+ entry2.restrictions[1].id.SetValue(2);
+ entries2.push_back(entry2);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(2, entries2), CHIP_NO_ERROR);
+
+ RunChecks(combinedRestrictionTestData, ArraySize(combinedRestrictionTestData));
+}
+
+TEST_F(TestAccessRestriction, AttributeStorageSeperationTest)
+{
+ std::vector<AccessRestrictionProvider::Entry> commissioningEntries;
+ AccessRestrictionProvider::Entry entry1;
+ entry1.fabricIndex = 1;
+ entry1.endpointNumber = 1;
+ entry1.clusterId = kWiFiNetworkManagementCluster;
+ entry1.restrictions.push_back({ .restrictionType = AccessRestrictionProvider::Type::kAttributeWriteForbidden });
+ entry1.restrictions[0].id.SetValue(1);
+ commissioningEntries.push_back(entry1);
+ EXPECT_EQ(accessRestrictionProvider.SetCommissioningEntries(commissioningEntries), CHIP_NO_ERROR);
+
+ std::vector<AccessRestrictionProvider::Entry> entries;
+ AccessRestrictionProvider::Entry entry2;
+ entry2.fabricIndex = 2;
+ entry2.endpointNumber = 2;
+ entry2.clusterId = kThreadBorderRouterMgmtCluster;
+ entry2.restrictions.push_back({ .restrictionType = AccessRestrictionProvider::Type::kCommandForbidden });
+ entry2.restrictions[0].id.SetValue(2);
+ entries.push_back(entry2);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(2, entries), CHIP_NO_ERROR);
+
+ auto commissioningEntriesFetched = accessRestrictionProvider.GetCommissioningEntries();
+ std::vector<AccessRestrictionProvider::Entry> arlEntriesFetched;
+ EXPECT_EQ(accessRestrictionProvider.GetEntries(2, arlEntriesFetched), CHIP_NO_ERROR);
+ EXPECT_EQ(commissioningEntriesFetched[0], entry1);
+ EXPECT_EQ(commissioningEntriesFetched.size(), static_cast<size_t>(1));
+ EXPECT_EQ(arlEntriesFetched[0], entry2);
+ EXPECT_EQ(arlEntriesFetched.size(), static_cast<size_t>(1));
+ EXPECT_FALSE(commissioningEntriesFetched[0] == arlEntriesFetched[0]);
+}
+
+constexpr CheckData listSelectionDuringCommissioningData[] = {
+ { .subjectDescriptor = { .fabricIndex = 1,
+ .authMode = AuthMode::kCase,
+ .subject = kOperationalNodeId1,
+ .isCommissioning = true },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kAttributeReadRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = true },
+ { .subjectDescriptor = { .fabricIndex = 1,
+ .authMode = AuthMode::kCase,
+ .subject = kOperationalNodeId1,
+ .isCommissioning = true },
+ .requestPath = { .cluster = kThreadBorderRouterMgmtCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kAttributeReadRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = false },
+ { .subjectDescriptor = { .fabricIndex = 1,
+ .authMode = AuthMode::kCase,
+ .subject = kOperationalNodeId1,
+ .isCommissioning = false },
+ .requestPath = { .cluster = kWiFiNetworkManagementCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kAttributeReadRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = false },
+ { .subjectDescriptor = { .fabricIndex = 1,
+ .authMode = AuthMode::kCase,
+ .subject = kOperationalNodeId1,
+ .isCommissioning = false },
+ .requestPath = { .cluster = kThreadBorderRouterMgmtCluster,
+ .endpoint = 1,
+ .requestType = RequestType::kAttributeReadRequest,
+ .entityId = 1 },
+ .privilege = Privilege::kAdminister,
+ .allow = true },
+};
+
+TEST_F(TestAccessRestriction, ListSelectiondDuringCommissioningTest)
+{
+ // during commissioning, read is allowed on WifiNetworkManagement and disallowed on ThreadBorderRouterMgmt
+ // after commissioning, read is disallowed on WifiNetworkManagement and allowed on ThreadBorderRouterMgmt
+
+ std::vector<AccessRestrictionProvider::Entry> entries;
+ AccessRestrictionProvider::Entry entry1;
+ entry1.fabricIndex = 1;
+ entry1.endpointNumber = 1;
+ entry1.clusterId = kThreadBorderRouterMgmtCluster;
+ entry1.restrictions.push_back({ .restrictionType = AccessRestrictionProvider::Type::kAttributeAccessForbidden });
+ entry1.restrictions[0].id.SetValue(1);
+ entries.push_back(entry1);
+ EXPECT_EQ(accessRestrictionProvider.SetCommissioningEntries(entries), CHIP_NO_ERROR);
+
+ entries.clear();
+ AccessRestrictionProvider::Entry entry2;
+ entry2.fabricIndex = 1;
+ entry2.endpointNumber = 1;
+ entry2.clusterId = kWiFiNetworkManagementCluster;
+ entry2.restrictions.push_back({ .restrictionType = AccessRestrictionProvider::Type::kAttributeAccessForbidden });
+ entry2.restrictions[0].id.SetValue(1);
+ entries.push_back(entry2);
+ EXPECT_EQ(accessRestrictionProvider.SetEntries(1, entries), CHIP_NO_ERROR);
+
+ RunChecks(listSelectionDuringCommissioningData, ArraySize(listSelectionDuringCommissioningData));
+}
+
+} // namespace Access
+} // namespace chip
diff --git a/src/app/EventManagement.cpp b/src/app/EventManagement.cpp
index 4c5faab..2419d56 100644
--- a/src/app/EventManagement.cpp
+++ b/src/app/EventManagement.cpp
@@ -556,7 +556,7 @@
Access::RequestPath requestPath{ .cluster = event.mClusterId,
.endpoint = event.mEndpointId,
- .requestType = Access::RequestType::kEventReadOrSubscribeRequest,
+ .requestType = Access::RequestType::kEventReadRequest,
.entityId = event.mEventId };
Access::Privilege requestPrivilege = RequiredPrivilege::ForReadEvent(path);
CHIP_ERROR accessControlError =
diff --git a/src/app/InteractionModelEngine.cpp b/src/app/InteractionModelEngine.cpp
index 64d30bc..88c1777 100644
--- a/src/app/InteractionModelEngine.cpp
+++ b/src/app/InteractionModelEngine.cpp
@@ -544,7 +544,7 @@
{
Access::RequestPath requestPath{ .cluster = aPath.mClusterId,
.endpoint = aPath.mEndpointId,
- .requestType = Access::RequestType::kEventReadOrSubscribeRequest };
+ .requestType = Access::RequestType::kEventReadRequest };
// leave requestPath.entityId optional value unset to indicate wildcard
CHIP_ERROR err = Access::GetAccessControl().Check(aSubjectDescriptor, requestPath, aNeededPrivilege);
return (err == CHIP_NO_ERROR);
@@ -555,7 +555,7 @@
{
Access::RequestPath requestPath{ .cluster = aPath.mClusterId,
.endpoint = aPath.mEndpointId,
- .requestType = Access::RequestType::kEventReadOrSubscribeRequest,
+ .requestType = Access::RequestType::kEventReadRequest,
.entityId = aPath.mEventId };
CHIP_ERROR err = Access::GetAccessControl().Check(aSubjectDescriptor, requestPath, RequiredPrivilege::ForReadEvent(aPath));
return (err == CHIP_NO_ERROR);
diff --git a/src/app/chip_data_model.gni b/src/app/chip_data_model.gni
index 01d47a4..a1d9a81 100644
--- a/src/app/chip_data_model.gni
+++ b/src/app/chip_data_model.gni
@@ -436,6 +436,12 @@
"${_app_root}/clusters/${cluster}/PresetStructWithOwnedMembers.h",
"${_app_root}/clusters/${cluster}/thermostat-delegate.h",
]
+ } else if (cluster == "access-control-server") {
+ sources += [
+ "${_app_root}/clusters/${cluster}/${cluster}.cpp",
+ "${_app_root}/clusters/${cluster}/ArlEncoder.cpp",
+ "${_app_root}/clusters/${cluster}/ArlEncoder.h",
+ ]
} else {
sources += [ "${_app_root}/clusters/${cluster}/${cluster}.cpp" ]
}
diff --git a/src/app/clusters/access-control-server/ArlEncoder.cpp b/src/app/clusters/access-control-server/ArlEncoder.cpp
new file mode 100644
index 0000000..810c734
--- /dev/null
+++ b/src/app/clusters/access-control-server/ArlEncoder.cpp
@@ -0,0 +1,145 @@
+/*
+ *
+ * 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 "ArlEncoder.h"
+
+using namespace chip;
+using namespace chip::app;
+using namespace chip::Access;
+
+using Entry = AccessRestrictionProvider::Entry;
+using EntryListener = AccessRestrictionProvider::Listener;
+using StagingRestrictionType = Clusters::AccessControl::AccessRestrictionTypeEnum;
+using StagingRestriction = Clusters::AccessControl::Structs::AccessRestrictionStruct::Type;
+
+namespace {
+
+CHIP_ERROR StageEntryRestrictions(const std::vector<AccessRestrictionProvider::Restriction> & source,
+ StagingRestriction destination[], size_t destinationCount)
+{
+ size_t count = source.size();
+ if (count > 0 && count <= destinationCount)
+ {
+ for (size_t i = 0; i < count; i++)
+ {
+ const auto & restriction = source[i];
+ ReturnErrorOnFailure(ArlEncoder::Convert(restriction.restrictionType, destination[i].type));
+
+ if (restriction.id.HasValue())
+ {
+ destination[i].id.SetNonNull(restriction.id.Value());
+ }
+ }
+ }
+ else
+ {
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+
+ return CHIP_NO_ERROR;
+}
+
+} // namespace
+
+namespace chip {
+namespace app {
+
+CHIP_ERROR ArlEncoder::Convert(Clusters::AccessControl::AccessRestrictionTypeEnum from,
+ Access::AccessRestrictionProvider::Type & to)
+{
+ switch (from)
+ {
+ case StagingRestrictionType::kAttributeAccessForbidden:
+ to = AccessRestrictionProvider::Type::kAttributeAccessForbidden;
+ break;
+ case StagingRestrictionType::kAttributeWriteForbidden:
+ to = AccessRestrictionProvider::Type::kAttributeWriteForbidden;
+ break;
+ case StagingRestrictionType::kCommandForbidden:
+ to = AccessRestrictionProvider::Type::kCommandForbidden;
+ break;
+ case StagingRestrictionType::kEventForbidden:
+ to = AccessRestrictionProvider::Type::kEventForbidden;
+ break;
+ default:
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+ return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR ArlEncoder::Convert(Access::AccessRestrictionProvider::Type from,
+ Clusters::AccessControl::AccessRestrictionTypeEnum & to)
+{
+ switch (from)
+ {
+ case AccessRestrictionProvider::Type::kAttributeAccessForbidden:
+ to = StagingRestrictionType::kAttributeAccessForbidden;
+ break;
+ case AccessRestrictionProvider::Type::kAttributeWriteForbidden:
+ to = StagingRestrictionType::kAttributeWriteForbidden;
+ break;
+ case AccessRestrictionProvider::Type::kCommandForbidden:
+ to = StagingRestrictionType::kCommandForbidden;
+ break;
+ case AccessRestrictionProvider::Type::kEventForbidden:
+ to = StagingRestrictionType::kEventForbidden;
+ break;
+ default:
+ return CHIP_ERROR_INVALID_ARGUMENT;
+ }
+ return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR ArlEncoder::CommissioningEncodableEntry::Encode(TLV::TLVWriter & writer, TLV::Tag tag) const
+{
+ ReturnErrorOnFailure(Stage());
+ ReturnErrorOnFailure(mStagingEntry.Encode(writer, tag));
+ return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR ArlEncoder::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 ArlEncoder::CommissioningEncodableEntry::Stage() const
+{
+ mStagingEntry.endpoint = mEntry.endpointNumber;
+ mStagingEntry.cluster = mEntry.clusterId;
+ ReturnErrorOnFailure(StageEntryRestrictions(mEntry.restrictions, mStagingRestrictions,
+ sizeof(mStagingRestrictions) / sizeof(mStagingRestrictions[0])));
+ mStagingEntry.restrictions = Span<StagingRestriction>(mStagingRestrictions, mEntry.restrictions.size());
+
+ return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR ArlEncoder::EncodableEntry::Stage() const
+{
+ mStagingEntry.fabricIndex = mEntry.fabricIndex;
+ mStagingEntry.endpoint = mEntry.endpointNumber;
+ mStagingEntry.cluster = mEntry.clusterId;
+ ReturnErrorOnFailure(StageEntryRestrictions(mEntry.restrictions, mStagingRestrictions,
+ sizeof(mStagingRestrictions) / sizeof(mStagingRestrictions[0])));
+ mStagingEntry.restrictions = Span<StagingRestriction>(mStagingRestrictions, mEntry.restrictions.size());
+
+ return CHIP_NO_ERROR;
+}
+
+} // namespace app
+} // namespace chip
diff --git a/src/app/clusters/access-control-server/ArlEncoder.h b/src/app/clusters/access-control-server/ArlEncoder.h
new file mode 100644
index 0000000..8050bf4
--- /dev/null
+++ b/src/app/clusters/access-control-server/ArlEncoder.h
@@ -0,0 +1,113 @@
+/*
+ *
+ * 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 <access/AccessRestrictionProvider.h>
+#include <app-common/zap-generated/cluster-objects.h>
+#include <credentials/FabricTable.h>
+
+namespace chip {
+namespace app {
+
+/**
+ * This class 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 ArlEncoder
+{
+public:
+ ArlEncoder() = default;
+ ~ArlEncoder() = default;
+
+ static CHIP_ERROR Convert(Clusters::AccessControl::AccessRestrictionTypeEnum from,
+ Access::AccessRestrictionProvider::Type & to);
+
+ static CHIP_ERROR Convert(Access::AccessRestrictionProvider::Type from,
+ Clusters::AccessControl::AccessRestrictionTypeEnum & to);
+
+ /**
+ * Used for encoding commissionable access restriction entries.
+ *
+ * Typically used temporarily on the stack to encode:
+ * - source: system level access restriction entry
+ * - staging: generated cluster level code
+ */
+ class CommissioningEncodableEntry
+ {
+ using Entry = Access::AccessRestrictionProvider::Entry;
+ using StagingEntry = Clusters::AccessControl::Structs::CommissioningAccessRestrictionEntryStruct::Type;
+ using StagingRestriction = Clusters::AccessControl::Structs::AccessRestrictionStruct::Type;
+
+ public:
+ CommissioningEncodableEntry(const Entry & entry) : mEntry(entry) {}
+
+ /**
+ * Encode the constructor-provided entry into the TLV writer.
+ */
+ CHIP_ERROR Encode(TLV::TLVWriter & aWriter, TLV::Tag aTag) const;
+
+ static constexpr bool kIsFabricScoped = false;
+
+ private:
+ CHIP_ERROR Stage() const;
+
+ Entry mEntry;
+ mutable StagingEntry mStagingEntry;
+ mutable StagingRestriction mStagingRestrictions[CHIP_CONFIG_ACCESS_RESTRICTION_MAX_RESTRICTIONS_PER_ENTRY];
+ };
+
+ /**
+ * 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
+ */
+ class EncodableEntry
+ {
+ using Entry = Access::AccessRestrictionProvider::Entry;
+ using StagingEntry = Clusters::AccessControl::Structs::AccessRestrictionEntryStruct::Type;
+ using StagingRestriction = Clusters::AccessControl::Structs::AccessRestrictionStruct::Type;
+
+ public:
+ EncodableEntry(const Entry & entry) : mEntry(entry) {}
+
+ /**
+ * Encode the constructor-provided entry into the TLV writer.
+ */
+ CHIP_ERROR EncodeForRead(TLV::TLVWriter & writer, TLV::Tag tag, FabricIndex fabric) const;
+
+ FabricIndex GetFabricIndex() const { return mEntry.fabricIndex; }
+
+ static constexpr bool kIsFabricScoped = true;
+
+ private:
+ /**
+ * Constructor-provided entry is staged into a staging entry.
+ */
+ CHIP_ERROR Stage() const;
+
+ Entry mEntry;
+ mutable StagingEntry mStagingEntry;
+ mutable StagingRestriction mStagingRestrictions[CHIP_CONFIG_ACCESS_RESTRICTION_MAX_RESTRICTIONS_PER_ENTRY];
+ };
+};
+
+} // namespace app
+} // namespace chip
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 321f7aa..295b553 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 <access/AccessControl.h>
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+#include "ArlEncoder.h"
+#include <access/AccessRestrictionProvider.h>
+#endif
+
#include <app-common/zap-generated/cluster-objects.h>
#include <app/AttributeAccessInterface.h>
@@ -25,6 +30,7 @@
#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 <app/util/attribute-storage.h>
@@ -41,6 +47,10 @@
using EntryListener = AccessControl::EntryListener;
using ExtensionEvent = Clusters::AccessControl::Events::AccessControlExtensionChanged::Type;
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+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;
@@ -48,7 +58,12 @@
namespace {
-class AccessControlAttribute : public AttributeAccessInterface, public EntryListener
+class AccessControlAttribute : public AttributeAccessInterface,
+ public AccessControl::EntryListener
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+ ,
+ public AccessRestrictionProvider::Listener
+#endif
{
public:
AccessControlAttribute() : AttributeAccessInterface(Optional<EndpointId>(0), AccessControlCluster::Id) {}
@@ -64,8 +79,17 @@
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 MarkCommissioningRestrictionListChanged() override;
+
+ void MarkRestrictionListChanged(FabricIndex fabricIndex) override;
+
+ void OnFabricRestrictionReviewUpdate(FabricIndex fabricIndex, uint64_t token, Optional<CharSpan> instruction,
+ Optional<CharSpan> redirectUrl) override;
+#endif
private:
/// Business logic implementation of write, returns generic CHIP_ERROR.
@@ -78,6 +102,10 @@
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,
@@ -114,8 +142,8 @@
TLV::TLVReader reader;
reader.Init(data);
- auto containerType = chip::TLV::kTLVType_List;
- err = reader.Next(containerType, chip::TLV::AnonymousTag());
+ 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);
@@ -123,7 +151,7 @@
while ((err = reader.Next()) == CHIP_NO_ERROR)
{
- VerifyOrReturnError(chip::TLV::IsProfileTag(reader.GetTag()), CHIP_IM_GLOBAL_STATUS(ConstraintError));
+ VerifyOrReturnError(TLV::IsProfileTag(reader.GetTag()), CHIP_IM_GLOBAL_STATUS(ConstraintError));
}
VerifyOrReturnError(err == CHIP_END_OF_TLV, CHIP_IM_GLOBAL_STATUS(ConstraintError));
@@ -159,6 +187,12 @@
ReturnErrorOnFailure(GetAccessControl().GetMaxEntriesPerFabric(value));
return aEncoder.Encode(static_cast<uint16_t>(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 +409,7 @@
}
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 +423,11 @@
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 +462,86 @@
ChipLogError(DataManagement, "AccessControlCluster: event failed %" CHIP_ERROR_FORMAT, err.Format());
}
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+CHIP_ERROR AccessControlAttribute::ReadCommissioningArl(AttributeValueEncoder & aEncoder)
+{
+ auto accessRestrictionProvider = GetAccessControl().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 AccessControlAttribute::ReadArl(AttributeValueEncoder & aEncoder)
+{
+ auto accessRestrictionProvider = GetAccessControl().GetAccessRestrictionProvider();
+
+ return aEncoder.EncodeList([&](const auto & encoder) -> CHIP_ERROR {
+ if (accessRestrictionProvider != nullptr)
+ {
+ for (const auto & info : Server::GetInstance().GetFabricTable())
+ {
+ 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;
+ });
+}
+void AccessControlAttribute::MarkCommissioningRestrictionListChanged()
+{
+ MatterReportingAttributeChangeCallback(kRootEndpointId, AccessControlCluster::Id,
+ AccessControlCluster::Attributes::CommissioningARL::Id);
+}
+
+void AccessControlAttribute::MarkRestrictionListChanged(FabricIndex fabricIndex)
+{
+ MatterReportingAttributeChangeCallback(kRootEndpointId, AccessControlCluster::Id, AccessControlCluster::Attributes::Arl::Id);
+}
+
+void AccessControlAttribute::OnFabricRestrictionReviewUpdate(FabricIndex fabricIndex, uint64_t token,
+ Optional<CharSpan> instruction, Optional<CharSpan> redirectUrl)
+{
+ CHIP_ERROR err;
+ ArlReviewEvent event{ .token = token, .fabricIndex = fabricIndex };
+
+ if (instruction.HasValue())
+ {
+ event.instruction.SetNonNull(instruction.Value());
+ }
+
+ if (redirectUrl.HasValue())
+ {
+ event.redirectURL.SetNonNull(redirectUrl.Value());
+ }
+
+ EventNumber eventNumber;
+ SuccessOrExit(err = LogEvent(event, kRootEndpointId, 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
@@ -481,4 +595,88 @@
AttributeAccessInterfaceRegistry::Instance().Register(&sAttribute);
GetAccessControl().AddEntryListener(sAttribute);
+
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+ auto accessRestrictionProvider = GetAccessControl().GetAccessRestrictionProvider();
+ if (accessRestrictionProvider != nullptr)
+ {
+ accessRestrictionProvider->AddListener(sAttribute);
+ }
+#endif
}
+
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+bool emberAfAccessControlClusterReviewFabricRestrictionsCallback(
+ CommandHandler * commandObj, const ConcreteCommandPath & commandPath,
+ const Clusters::AccessControl::Commands::ReviewFabricRestrictions::DecodableType & commandData)
+{
+ if (commandPath.mEndpointId != kRootEndpointId)
+ {
+ ChipLogError(DataManagement, "AccessControlCluster: invalid endpoint in ReviewFabricRestrictions request");
+ commandObj->AddStatus(commandPath, Protocols::InteractionModel::Status::InvalidCommand);
+ return true;
+ }
+
+ 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");
+ commandObj->AddStatus(commandPath, Protocols::InteractionModel::Status::InvalidCommand);
+ return true;
+ }
+
+ 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");
+ commandObj->AddStatus(commandPath, Protocols::InteractionModel::Status::InvalidCommand);
+ return true;
+ }
+
+ entries.push_back(entry);
+ }
+
+ if (entryIter.GetStatus() != CHIP_NO_ERROR)
+ {
+ ChipLogError(DataManagement, "AccessControlCluster: invalid ARL data");
+ commandObj->AddStatus(commandPath, Protocols::InteractionModel::Status::InvalidCommand);
+ return true;
+ }
+
+ CHIP_ERROR err = GetAccessControl().GetAccessRestrictionProvider()->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 review failed: %" CHIP_ERROR_FORMAT, err.Format());
+ commandObj->AddStatus(commandPath, Protocols::InteractionModel::ClusterStatusCode(err));
+ }
+
+ return true;
+}
+#endif
diff --git a/src/app/reporting/Engine.cpp b/src/app/reporting/Engine.cpp
index 6d79e9e..99d038a 100644
--- a/src/app/reporting/Engine.cpp
+++ b/src/app/reporting/Engine.cpp
@@ -341,7 +341,7 @@
Access::RequestPath requestPath{ .cluster = current->mValue.mClusterId,
.endpoint = current->mValue.mEndpointId,
- .requestType = RequestType::kEventReadOrSubscribeRequest,
+ .requestType = RequestType::kEventReadRequest,
.entityId = current->mValue.mEventId };
Access::Privilege requestPrivilege = RequiredPrivilege::ForReadEvent(path);
diff --git a/src/app/server/BUILD.gn b/src/app/server/BUILD.gn
index 401356d..58524c3 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")
diff --git a/src/app/server/Server.cpp b/src/app/server/Server.cpp
index 22cd274..426e3c6 100644
--- a/src/app/server/Server.cpp
+++ b/src/app/server/Server.cpp
@@ -177,6 +177,13 @@
SuccessOrExit(err = mAccessControl.Init(initParams.accessDelegate, sDeviceTypeResolver));
Access::SetAccessControl(mAccessControl);
+#if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS
+ if (initParams.accessRestrictionProvider != nullptr)
+ {
+ mAccessControl.SetAccessRestrictionProvider(initParams.accessRestrictionProvider);
+ }
+#endif
+
mAclStorage = initParams.aclStorage;
SuccessOrExit(err = mAclStorage->Init(*mDeviceStorage, mFabrics.begin(), mFabrics.end()));
diff --git a/src/app/server/Server.h b/src/app/server/Server.h
index 2f6126a..27c8505 100644
--- a/src/app/server/Server.h
+++ b/src/app/server/Server.h
@@ -163,6 +163,13 @@
// 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::AccessRestrictionProvider * accessRestrictionProvider = nullptr;
+#endif
+
// Network native params can be injected depending on the
// selected Endpoint implementation
void * endpointNativeParams = nullptr;
diff --git a/src/credentials/FabricTable.cpp b/src/credentials/FabricTable.cpp
index b847add..f999cf7 100644
--- a/src/credentials/FabricTable.cpp
+++ b/src/credentials/FabricTable.cpp
@@ -71,6 +71,44 @@
return TLV::EstimateStructOverhead(sizeof(FabricIndex), CHIP_CONFIG_MAX_FABRICS * (1 + sizeof(FabricIndex)) + 1);
}
+CHIP_ERROR AddNewFabricForTestInternal(FabricTable & fabricTable, bool leavePending, const ByteSpan & rootCert,
+ const ByteSpan & icacCert, const ByteSpan & nocCert, const ByteSpan & opKeySpan,
+ FabricIndex * outFabricIndex)
+{
+ VerifyOrReturnError(outFabricIndex != nullptr, CHIP_ERROR_INVALID_ARGUMENT);
+
+ CHIP_ERROR err = CHIP_ERROR_INTERNAL;
+
+ Crypto::P256Keypair injectedOpKey;
+ Crypto::P256SerializedKeypair injectedOpKeysSerialized;
+
+ Crypto::P256Keypair * opKey = nullptr;
+ if (!opKeySpan.empty())
+ {
+ VerifyOrReturnError(opKeySpan.size() == injectedOpKeysSerialized.Capacity(), CHIP_ERROR_INVALID_ARGUMENT);
+
+ memcpy(injectedOpKeysSerialized.Bytes(), opKeySpan.data(), opKeySpan.size());
+ SuccessOrExit(err = injectedOpKeysSerialized.SetLength(opKeySpan.size()));
+ SuccessOrExit(err = injectedOpKey.Deserialize(injectedOpKeysSerialized));
+ opKey = &injectedOpKey;
+ }
+
+ SuccessOrExit(err = fabricTable.AddNewPendingTrustedRootCert(rootCert));
+ SuccessOrExit(err =
+ fabricTable.AddNewPendingFabricWithProvidedOpKey(nocCert, icacCert, VendorId::TestVendor1, opKey,
+ /*isExistingOpKeyExternallyOwned =*/false, outFabricIndex));
+ if (!leavePending)
+ {
+ SuccessOrExit(err = fabricTable.CommitPendingFabricData());
+ }
+exit:
+ if (err != CHIP_NO_ERROR)
+ {
+ fabricTable.RevertPendingFabricData();
+ }
+ return err;
+}
+
} // anonymous namespace
CHIP_ERROR FabricInfo::Init(const FabricInfo::InitParams & initParams)
@@ -695,34 +733,14 @@
CHIP_ERROR FabricTable::AddNewFabricForTest(const ByteSpan & rootCert, const ByteSpan & icacCert, const ByteSpan & nocCert,
const ByteSpan & opKeySpan, FabricIndex * outFabricIndex)
{
- VerifyOrReturnError(outFabricIndex != nullptr, CHIP_ERROR_INVALID_ARGUMENT);
+ return AddNewFabricForTestInternal(*this, /*leavePending=*/false, rootCert, icacCert, nocCert, opKeySpan, outFabricIndex);
+}
- CHIP_ERROR err = CHIP_ERROR_INTERNAL;
-
- Crypto::P256Keypair injectedOpKey;
- Crypto::P256SerializedKeypair injectedOpKeysSerialized;
-
- Crypto::P256Keypair * opKey = nullptr;
- if (!opKeySpan.empty())
- {
- VerifyOrReturnError(opKeySpan.size() == injectedOpKeysSerialized.Capacity(), CHIP_ERROR_INVALID_ARGUMENT);
-
- memcpy(injectedOpKeysSerialized.Bytes(), opKeySpan.data(), opKeySpan.size());
- SuccessOrExit(err = injectedOpKeysSerialized.SetLength(opKeySpan.size()));
- SuccessOrExit(err = injectedOpKey.Deserialize(injectedOpKeysSerialized));
- opKey = &injectedOpKey;
- }
-
- SuccessOrExit(err = AddNewPendingTrustedRootCert(rootCert));
- SuccessOrExit(err = AddNewPendingFabricWithProvidedOpKey(nocCert, icacCert, VendorId::TestVendor1, opKey,
- /*isExistingOpKeyExternallyOwned =*/false, outFabricIndex));
- SuccessOrExit(err = CommitPendingFabricData());
-exit:
- if (err != CHIP_NO_ERROR)
- {
- RevertPendingFabricData();
- }
- return err;
+CHIP_ERROR FabricTable::AddNewUncommittedFabricForTest(const ByteSpan & rootCert, const ByteSpan & icacCert,
+ const ByteSpan & nocCert, const ByteSpan & opKeySpan,
+ FabricIndex * outFabricIndex)
+{
+ return AddNewFabricForTestInternal(*this, /*leavePending=*/true, rootCert, icacCert, nocCert, opKeySpan, outFabricIndex);
}
/*
@@ -1546,6 +1564,16 @@
return isLegal;
}
+FabricIndex FabricTable::GetPendingNewFabricIndex() const
+{
+ if (mStateFlags.Has(StateFlags::kIsAddPending))
+ {
+ return mFabricIndexWithPendingState;
+ }
+
+ return kUndefinedFabricIndex;
+}
+
CHIP_ERROR FabricTable::AllocatePendingOperationalKey(Optional<FabricIndex> fabricIndex, MutableByteSpan & outputCsr)
{
// We can only manage commissionable pending fail-safe state if we have a keystore
diff --git a/src/credentials/FabricTable.h b/src/credentials/FabricTable.h
index aef60f6..af90d78 100644
--- a/src/credentials/FabricTable.h
+++ b/src/credentials/FabricTable.h
@@ -737,6 +737,18 @@
bool HasOperationalKeyForFabric(FabricIndex fabricIndex) const;
/**
+ * @brief If a newly-added fabric is pending, this returns its index, or kUndefinedFabricIndex if none are pending.
+ *
+ * A newly-added fabric is pending if AddNOC has been previously called successfully but the
+ * fabric is not yet fully committed by CommissioningComplete.
+ *
+ * NOTE: that this never returns a value other than kUndefinedFabricIndex when UpdateNOC is pending.
+ *
+ * @return the fabric index of the pending fabric, or kUndefinedFabricIndex if no fabrics are pending.
+ */
+ FabricIndex GetPendingNewFabricIndex() const;
+
+ /**
* @brief Returns the operational keystore. This is used for
* CASE and the only way the keystore should be used.
*
@@ -968,6 +980,11 @@
CHIP_ERROR AddNewFabricForTest(const ByteSpan & rootCert, const ByteSpan & icacCert, const ByteSpan & nocCert,
const ByteSpan & opKeySpan, FabricIndex * outFabricIndex);
+ // Add a new fabric for testing. The Operational Key is a raw P256Keypair (public key and private key raw bits) that will
+ // get copied (directly) into the fabric table. The fabric will NOT be committed, and will remain pending.
+ CHIP_ERROR AddNewUncommittedFabricForTest(const ByteSpan & rootCert, const ByteSpan & icacCert, const ByteSpan & nocCert,
+ const ByteSpan & opKeySpan, FabricIndex * outFabricIndex);
+
// Same as AddNewFabricForTest, but ignore if we are colliding with same <Root Public Key, Fabric Id>, so
// that a single fabric table can have N nodes for same fabric. This usually works, but is bad form.
CHIP_ERROR AddNewFabricForTestIgnoringCollisions(const ByteSpan & rootCert, const ByteSpan & icacCert, const ByteSpan & nocCert,
diff --git a/src/credentials/tests/TestFabricTable.cpp b/src/credentials/tests/TestFabricTable.cpp
index c5b78fc..0d34f48 100644
--- a/src/credentials/tests/TestFabricTable.cpp
+++ b/src/credentials/tests/TestFabricTable.cpp
@@ -551,6 +551,7 @@
FabricTable & fabricTable = fabricTableHolder.GetFabricTable();
EXPECT_EQ(fabricTable.FabricCount(), 0);
+ EXPECT_EQ(fabricTable.GetPendingNewFabricIndex(), kUndefinedFabricIndex);
{
FabricIndex nextFabricIndex = kUndefinedFabricIndex;
@@ -604,6 +605,7 @@
EXPECT_EQ(fabricTable.FetchPendingNonFabricAssociatedRootCert(fetchedSpan), CHIP_NO_ERROR);
EXPECT_TRUE(fetchedSpan.data_equal(rcac));
}
+ EXPECT_EQ(fabricTable.GetPendingNewFabricIndex(), kUndefinedFabricIndex);
FabricIndex newFabricIndex = kUndefinedFabricIndex;
bool keyIsExternallyOwned = true;
@@ -614,6 +616,11 @@
CHIP_NO_ERROR);
EXPECT_EQ(newFabricIndex, 1);
EXPECT_EQ(fabricTable.FabricCount(), 1);
+
+ // After adding the pending new fabric (equivalent of AddNOC processing), the new
+ // fabric must be pending.
+ EXPECT_EQ(fabricTable.GetPendingNewFabricIndex(), 1);
+
{
// No more pending root cert; it's associated with a fabric now.
MutableByteSpan fetchedSpan{ rcacBuf };
@@ -661,6 +668,9 @@
EXPECT_EQ(nextFabricIndex, 2);
}
+ // Fabric can't be pending anymore.
+ EXPECT_EQ(fabricTable.GetPendingNewFabricIndex(), kUndefinedFabricIndex);
+
// Validate contents
const auto * fabricInfo = fabricTable.FindFabricWithIndex(1);
ASSERT_NE(fabricInfo, nullptr);
@@ -732,12 +742,16 @@
}
EXPECT_EQ(fabricTable.AddNewPendingTrustedRootCert(rcac), CHIP_NO_ERROR);
+ EXPECT_EQ(fabricTable.GetPendingNewFabricIndex(), kUndefinedFabricIndex);
+
FabricIndex newFabricIndex = kUndefinedFabricIndex;
EXPECT_EQ(fabricTable.FabricCount(), 1);
EXPECT_EQ(fabricTable.AddNewPendingFabricWithOperationalKeystore(noc, icac, kVendorId, &newFabricIndex), CHIP_NO_ERROR);
EXPECT_EQ(fabricTable.FabricCount(), 2);
EXPECT_EQ(newFabricIndex, 2);
+ EXPECT_EQ(fabricTable.GetPendingNewFabricIndex(), 2);
+
// No storage yet
EXPECT_EQ(storage.GetNumKeys(), numStorageAfterFirstAdd);
// Next fabric index has not been updated yet.
@@ -1897,6 +1911,8 @@
uint8_t csrBuf[chip::Crypto::kMIN_CSR_Buffer_Size];
MutableByteSpan csrSpan{ csrBuf };
+ EXPECT_EQ(fabricTable.GetPendingNewFabricIndex(), kUndefinedFabricIndex);
+
// Make sure to tag fabric index to pending opkey: otherwise the UpdateNOC fails
EXPECT_EQ(fabricTable.AllocatePendingOperationalKey(chip::MakeOptional(static_cast<FabricIndex>(1)), csrSpan),
CHIP_NO_ERROR);
@@ -1908,6 +1924,7 @@
EXPECT_EQ(fabricTable.FabricCount(), 1);
EXPECT_EQ(fabricTable.UpdatePendingFabricWithOperationalKeystore(1, noc, ByteSpan{}), CHIP_NO_ERROR);
+ EXPECT_EQ(fabricTable.GetPendingNewFabricIndex(), kUndefinedFabricIndex);
EXPECT_EQ(fabricTable.FabricCount(), 1);
// No storage yet
@@ -1936,6 +1953,7 @@
// Revert, should see Node ID 999 again
fabricTable.RevertPendingFabricData();
EXPECT_EQ(fabricTable.FabricCount(), 1);
+ EXPECT_EQ(fabricTable.GetPendingNewFabricIndex(), kUndefinedFabricIndex);
EXPECT_EQ(storage.GetNumKeys(), numStorageAfterAdd);
diff --git a/src/lib/core/CHIPConfig.h b/src/lib/core/CHIPConfig.h
index 7600b4e..8936cc1 100644
--- a/src/lib/core/CHIPConfig.h
+++ b/src/lib/core/CHIPConfig.h
@@ -1207,6 +1207,27 @@
#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
*
* @brief
diff --git a/src/messaging/tests/TestReliableMessageProtocol.cpp b/src/messaging/tests/TestReliableMessageProtocol.cpp
index 68b6ed2..6390e4e 100644
--- a/src/messaging/tests/TestReliableMessageProtocol.cpp
+++ b/src/messaging/tests/TestReliableMessageProtocol.cpp
@@ -92,6 +92,12 @@
System::PacketBufferHandle && buffer) override
{
IsOnMessageReceivedCalled = true;
+
+ if (ec->HasSessionHandle() && ec->GetSessionHolder()->IsSecureSession())
+ {
+ mLastSubjectDescriptor = ec->GetSessionHolder()->AsSecureSession()->GetSubjectDescriptor();
+ }
+
if (payloadHeader.IsAckMsg())
{
mReceivedPiggybackAck = true;
@@ -125,6 +131,7 @@
EXPECT_EQ(buffer->TotalLength(), sizeof(PAYLOAD));
EXPECT_EQ(memcmp(buffer->Start(), PAYLOAD, buffer->TotalLength()), 0);
+
return CHIP_NO_ERROR;
}
@@ -151,6 +158,8 @@
}
}
+ Access::SubjectDescriptor mLastSubjectDescriptor{};
+
bool IsOnMessageReceivedCalled = false;
bool mReceivedPiggybackAck = false;
bool mRetainExchange = false;
@@ -1830,9 +1839,12 @@
EXPECT_EQ(loopback.mSentMessageCount, kMaxMRPTransmits);
EXPECT_EQ(loopback.mDroppedMessageCount, kMaxMRPTransmits - 1);
EXPECT_EQ(rm->TestGetCountRetransTable(), 1); // We have no ack yet.
- EXPECT_TRUE(mockReceiver.IsOnMessageReceivedCalled); // Other side got the message.
+ ASSERT_TRUE(mockReceiver.IsOnMessageReceivedCalled); // Other side got the message.
EXPECT_FALSE(mockSender.IsOnMessageReceivedCalled); // We did not get a response.
+ // It was not a commissioning CASE session so that is lined-up properly.
+ EXPECT_FALSE(mockReceiver.mLastSubjectDescriptor.isCommissioning);
+
// Ensure there will be no more weirdness with acks and that our MRP timer is restarted properly.
mockReceiver.SetDropAckResponse(false);
diff --git a/src/transport/SecureSession.cpp b/src/transport/SecureSession.cpp
index c96f9cd..7694df2 100644
--- a/src/transport/SecureSession.cpp
+++ b/src/transport/SecureSession.cpp
@@ -160,10 +160,11 @@
Access::SubjectDescriptor subjectDescriptor;
if (IsOperationalNodeId(mPeerNodeId))
{
- subjectDescriptor.authMode = Access::AuthMode::kCase;
- subjectDescriptor.subject = mPeerNodeId;
- subjectDescriptor.cats = mPeerCATs;
- subjectDescriptor.fabricIndex = GetFabricIndex();
+ subjectDescriptor.authMode = Access::AuthMode::kCase;
+ subjectDescriptor.subject = mPeerNodeId;
+ subjectDescriptor.cats = mPeerCATs;
+ subjectDescriptor.fabricIndex = GetFabricIndex();
+ subjectDescriptor.isCommissioning = IsCommissioningSession();
}
else if (IsPAKEKeyId(mPeerNodeId))
{
@@ -171,9 +172,10 @@
// Initiator (aka commissioner) leaves subject descriptor unfilled.
if (GetCryptoContext().IsResponder())
{
- subjectDescriptor.authMode = Access::AuthMode::kPase;
- subjectDescriptor.subject = mPeerNodeId;
- subjectDescriptor.fabricIndex = GetFabricIndex();
+ subjectDescriptor.authMode = Access::AuthMode::kPase;
+ subjectDescriptor.subject = mPeerNodeId;
+ subjectDescriptor.fabricIndex = GetFabricIndex();
+ subjectDescriptor.isCommissioning = IsCommissioningSession();
}
}
else
@@ -183,6 +185,24 @@
return subjectDescriptor;
}
+bool SecureSession::IsCommissioningSession() const
+{
+ // PASE session is always a commissioning session.
+ if (IsPASESession())
+ {
+ return true;
+ }
+
+ // CASE session is a commissioning session if it was marked as such.
+ // The SessionManager is what keeps track.
+ if (IsCASESession() && mIsCaseCommissioningSession)
+ {
+ return true;
+ }
+
+ return false;
+}
+
void SecureSession::Retain()
{
#if CHIP_CONFIG_SECURE_SESSION_REFCOUNT_LOGGING
diff --git a/src/transport/SecureSession.h b/src/transport/SecureSession.h
index fe70d67..ba586a3 100644
--- a/src/transport/SecureSession.h
+++ b/src/transport/SecureSession.h
@@ -156,6 +156,8 @@
Access::SubjectDescriptor GetSubjectDescriptor() const override;
+ bool IsCommissioningSession() const override;
+
bool AllowsMRP() const override { return GetPeerAddress().GetTransportType() == Transport::Type::kUdp; }
bool AllowsLargePayload() const override { return GetPeerAddress().GetTransportType() == Transport::Type::kTcp; }
@@ -245,6 +247,12 @@
}
}
+ void SetCaseCommissioningSessionStatus(bool isCaseCommissioningSession)
+ {
+ VerifyOrDie(GetSecureSessionType() == Type::kCASE);
+ mIsCaseCommissioningSession = isCaseCommissioningSession;
+ }
+
bool IsPeerActive() const
{
return ((System::SystemClock().GetMonotonicTimestamp() - GetLastPeerActivityTime()) <
@@ -323,9 +331,10 @@
SecureSessionTable & mTable;
State mState;
const Type mSecureSessionType;
- NodeId mLocalNodeId = kUndefinedNodeId;
- NodeId mPeerNodeId = kUndefinedNodeId;
- CATValues mPeerCATs = CATValues{};
+ bool mIsCaseCommissioningSession = false;
+ NodeId mLocalNodeId = kUndefinedNodeId;
+ NodeId mPeerNodeId = kUndefinedNodeId;
+ CATValues mPeerCATs = CATValues{};
const uint16_t mLocalSessionId;
uint16_t mPeerSessionId = 0;
diff --git a/src/transport/Session.h b/src/transport/Session.h
index fc656c8..b49d461 100644
--- a/src/transport/Session.h
+++ b/src/transport/Session.h
@@ -244,6 +244,13 @@
virtual bool AllowsLargePayload() const = 0;
virtual const SessionParameters & GetRemoteSessionParameters() const = 0;
virtual System::Clock::Timestamp GetMRPBaseTimeout() const = 0;
+
+ // Returns true if `subjectDescriptor.IsCommissioning` (based on Core Specification
+ // pseudocode in ACL Architecture chapter) should be true when computing a
+ // subject descriptor for that session. This is only valid to call during
+ // synchronous processing of a message received on the session.
+ virtual bool IsCommissioningSession() const { return false; }
+
// GetAckTimeout is the estimate for how long it could take for the other
// side to receive our message (accounting for our MRP retransmits if it
// gets lost) and send a response.
diff --git a/src/transport/SessionManager.cpp b/src/transport/SessionManager.cpp
index d958247..8228733 100644
--- a/src/transport/SessionManager.cpp
+++ b/src/transport/SessionManager.cpp
@@ -1024,6 +1024,14 @@
MATTER_LOG_MESSAGE_RECEIVED(chip::Tracing::IncomingMessageType::kSecureUnicast, &payloadHeader, &packetHeader,
secureSession, &peerAddress, chip::ByteSpan(msg->Start(), msg->TotalLength()));
CHIP_TRACE_MESSAGE_RECEIVED(payloadHeader, packetHeader, secureSession, peerAddress, msg->Start(), msg->TotalLength());
+
+ // Always recompute whether a message is for a commissioning session based on the latest knowledge of
+ // the fabric table.
+ if (secureSession->IsCASESession())
+ {
+ secureSession->SetCaseCommissioningSessionStatus(secureSession->GetFabricIndex() ==
+ mFabricTable->GetPendingNewFabricIndex());
+ }
mCB->OnMessageReceived(packetHeader, payloadHeader, session.Value(), isDuplicate, std::move(msg));
}
else
diff --git a/src/transport/tests/TestSessionManager.cpp b/src/transport/tests/TestSessionManager.cpp
index 154071a..dfe3ca4 100644
--- a/src/transport/tests/TestSessionManager.cpp
+++ b/src/transport/tests/TestSessionManager.cpp
@@ -28,6 +28,7 @@
#define CHIP_ENABLE_TEST_ENCRYPTED_BUFFER_API // Up here in case some other header
// includes SessionManager.h indirectly
+#include <access/SubjectDescriptor.h>
#include <credentials/PersistentStorageOpCertStore.h>
#include <credentials/tests/CHIPCert_unit_test_vectors.h>
#include <crypto/DefaultSessionKeystore.h>
@@ -112,10 +113,12 @@
}
ReceiveHandlerCallCount++;
+ lastSubjectDescriptor = session->GetSubjectDescriptor();
}
int ReceiveHandlerCallCount = 0;
bool LargeMessageSent = false;
+ Access::SubjectDescriptor lastSubjectDescriptor{};
};
class TestSessionManager : public ::testing::Test
@@ -141,7 +144,7 @@
&fabricTableHolder.GetFabricTable(), sessionKeystore));
}
-TEST_F(TestSessionManager, CheckMessageTest)
+TEST_F(TestSessionManager, CheckMessageOverPaseTest)
{
uint16_t payload_len = sizeof(PAYLOAD);
@@ -213,7 +216,10 @@
EXPECT_EQ(err, CHIP_NO_ERROR);
mContext.DrainAndServiceIO();
- EXPECT_EQ(callback.ReceiveHandlerCallCount, 1);
+ ASSERT_EQ(callback.ReceiveHandlerCallCount, 1);
+
+ // This was a PASE session so we expect the subject descriptor to indicate it's for commissioning.
+ EXPECT_TRUE(callback.lastSubjectDescriptor.isCommissioning);
// Let's send the max sized message and make sure it is received
chip::System::PacketBufferHandle large_buffer = chip::MessagePacketBuffer::NewWithData(LARGE_PAYLOAD, kMaxAppMessageLen);