Move `::WriteAttribute` validation inside Interaction Model Write Handling (#37322)

* Move write validity inside the write handler rather than requesting the datamodel provider to perform the checks

* Restyle

* A bit of code cleanup and reuse, to minimize flash impact

* Restyle

* Drop the mPreviousSuccessPath, making objects smaller and saving yet another small amount of flash

* Cleanup some includes

* Restyle

* Remove obsolete tests from codgen, as we do not do more validation now

* Test write interaction passes

* Test write passes

* Slight clarity of code update

* Fix comment

* Manual check for last written saves 16 bytes of flash on QPG.

* Status success is 2 bytes smaller in code generation than CHIP_NO_ERROR

* Not using endpoint finder saves flash

* Clean up code. We now save flash

* More flash savings by reusing the server cluster finder

* Update src/app/data-model-provider/MetadataLookup.cpp

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/app/data-model-provider/MetadataLookup.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/app/data-model-provider/Provider.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Update src/app/data-model-provider/MetadataLookup.h

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Updated comment

* Hoist "writing" log up in processing, to preserve similar functionality

* Code clarity updates

* Fix includes and update the code AGAIN because it does seem we use more flash

* Fix unsupported write on global attributes

* Fix includes

* Update src/app/WriteHandler.cpp

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Post merge cleanup

* Fix merge error

---------

Co-authored-by: Andrei Litvin <andreilitvin@google.com>
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
diff --git a/src/app/InteractionModelEngine.cpp b/src/app/InteractionModelEngine.cpp
index da21906..6a96afc 100644
--- a/src/app/InteractionModelEngine.cpp
+++ b/src/app/InteractionModelEngine.cpp
@@ -1801,28 +1801,8 @@
         }
     }
 
-    {
-        DataModel::ServerClusterFinder finder(provider);
-        if (finder.Find(aCommandPath).has_value())
-        {
-            // cluster exists, so command is invalid
-            return Protocols::InteractionModel::Status::UnsupportedCommand;
-        }
-    }
-
-    // At this point either cluster or endpoint does not exist. If we find the endpoint, then the cluster
-    // is invalid
-    {
-        DataModel::EndpointFinder finder(provider);
-        if (finder.Find(aCommandPath.mEndpointId))
-        {
-            // endpoint exists, so cluster is invalid
-            return Protocols::InteractionModel::Status::UnsupportedCluster;
-        }
-    }
-
-    // endpoint not found
-    return Protocols::InteractionModel::Status::UnsupportedEndpoint;
+    // invalid command, return the right failure status
+    return DataModel::ValidateClusterPath(provider, aCommandPath, Protocols::InteractionModel::Status::UnsupportedCommand);
 }
 
 DataModel::Provider * InteractionModelEngine::SetDataModelProvider(DataModel::Provider * model)
diff --git a/src/app/WriteHandler.cpp b/src/app/WriteHandler.cpp
index 9847641..09a9ec0 100644
--- a/src/app/WriteHandler.cpp
+++ b/src/app/WriteHandler.cpp
@@ -19,11 +19,15 @@
 #include <app/AppConfig.h>
 #include <app/AttributeAccessInterfaceRegistry.h>
 #include <app/AttributeValueDecoder.h>
+#include <app/ConcreteAttributePath.h>
+#include <app/GlobalAttributes.h>
 #include <app/InteractionModelEngine.h>
 #include <app/MessageDef/EventPathIB.h>
 #include <app/MessageDef/StatusIB.h>
+#include <app/RequiredPrivilege.h>
 #include <app/StatusResponse.h>
 #include <app/WriteHandler.h>
+#include <app/data-model-provider/ActionReturnStatus.h>
 #include <app/data-model-provider/MetadataLookup.h>
 #include <app/data-model-provider/MetadataTypes.h>
 #include <app/data-model-provider/OperationTypes.h>
@@ -44,6 +48,8 @@
 
 namespace {
 
+using Protocols::InteractionModel::Status;
+
 /// Wraps a EndpointIterator and ensures that `::Release()` is called
 /// for the iterator (assuming it is non-null)
 class AutoReleaseGroupEndpointIterator
@@ -754,6 +760,75 @@
     ChipLogDetail(DataManagement, "IM WH moving to [%s]", GetStateStr());
 }
 
+DataModel::ActionReturnStatus WriteHandler::CheckWriteAllowed(const Access::SubjectDescriptor & aSubject,
+                                                              const ConcreteAttributePath & aPath)
+{
+    // TODO: ordering is to check writability/existence BEFORE ACL and this seems wrong, however
+    //       existing unit tests (TC_AcessChecker.py) validate that we get UnsupportedWrite instead of UnsupportedAccess
+    //
+    //       This should likely be fixed in spec (probably already fixed by
+    //       https://github.com/CHIP-Specifications/connectedhomeip-spec/pull/9024)
+    //       and tests and implementation
+    //
+    //       Open issue that needs fixing: https://github.com/project-chip/connectedhomeip/issues/33735
+
+    std::optional<DataModel::AttributeEntry> attributeEntry;
+    DataModel::AttributeFinder finder(mDataModelProvider);
+
+    attributeEntry = finder.Find(aPath);
+
+    // if path is not valid, return a spec-compliant return code.
+    if (!attributeEntry.has_value())
+    {
+        // Global lists are not in metadata and not writable. Return the correct error code according to the spec
+        Status attributeErrorStatus =
+            IsSupportedGlobalAttributeNotInMetadata(aPath.mAttributeId) ? Status::UnsupportedWrite : Status::UnsupportedAttribute;
+
+        return DataModel::ValidateClusterPath(mDataModelProvider, aPath, attributeErrorStatus);
+    }
+
+    // Allow writes on writable attributes only
+    VerifyOrReturnValue(attributeEntry->writePrivilege.has_value(), Status::UnsupportedWrite);
+
+    bool checkAcl = true;
+    if (mLastSuccessfullyWrittenPath.has_value())
+    {
+        // only validate ACL if path has changed
+        //
+        // Note that this is NOT operator==: we could do `checkAcl == (aPath != *mLastSuccessfullyWrittenPath)`
+        // however that seems to use more flash.
+        if ((aPath.mEndpointId == mLastSuccessfullyWrittenPath->mEndpointId) &&
+            (aPath.mClusterId == mLastSuccessfullyWrittenPath->mClusterId) &&
+            (aPath.mAttributeId == mLastSuccessfullyWrittenPath->mAttributeId))
+        {
+            checkAcl = false;
+        }
+    }
+
+    if (checkAcl)
+    {
+        Access::RequestPath requestPath{ .cluster     = aPath.mClusterId,
+                                         .endpoint    = aPath.mEndpointId,
+                                         .requestType = Access::RequestType::kAttributeWriteRequest,
+                                         .entityId    = aPath.mAttributeId };
+        CHIP_ERROR err = Access::GetAccessControl().Check(aSubject, requestPath, RequiredPrivilege::ForWriteAttribute(aPath));
+
+        if (err != CHIP_NO_ERROR)
+        {
+            VerifyOrReturnValue(err != CHIP_ERROR_ACCESS_DENIED, Status::UnsupportedAccess);
+            VerifyOrReturnValue(err != CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL, Status::AccessRestricted);
+
+            return err;
+        }
+    }
+
+    // validate that timed write is enforced
+    VerifyOrReturnValue(IsTimedWrite() || !attributeEntry->flags.Has(DataModel::AttributeQualityFlags::kTimed),
+                        Status::NeedsTimedInteraction);
+
+    return Status::Success;
+}
+
 CHIP_ERROR WriteHandler::WriteClusterData(const Access::SubjectDescriptor & aSubject, const ConcreteDataAttributePath & aPath,
                                           TLV::TLVReader & aData)
 {
@@ -761,16 +836,21 @@
     // the write is done via the DataModel interface
     VerifyOrReturnError(mDataModelProvider != nullptr, CHIP_ERROR_INCORRECT_STATE);
 
-    DataModel::WriteAttributeRequest request;
+    ChipLogDetail(DataManagement, "Writing attribute: Cluster=" ChipLogFormatMEI " Endpoint=0x%x AttributeId=" ChipLogFormatMEI,
+                  ChipLogValueMEI(aPath.mClusterId), aPath.mEndpointId, ChipLogValueMEI(aPath.mAttributeId));
 
-    request.path                = aPath;
-    request.subjectDescriptor   = &aSubject;
-    request.previousSuccessPath = mLastSuccessfullyWrittenPath;
-    request.writeFlags.Set(DataModel::WriteFlags::kTimed, IsTimedWrite());
+    DataModel::ActionReturnStatus status = CheckWriteAllowed(aSubject, aPath);
+    if (status.IsSuccess())
+    {
+        DataModel::WriteAttributeRequest request;
 
-    AttributeValueDecoder decoder(aData, aSubject);
+        request.path              = aPath;
+        request.subjectDescriptor = &aSubject;
+        request.writeFlags.Set(DataModel::WriteFlags::kTimed, IsTimedWrite());
 
-    DataModel::ActionReturnStatus status = mDataModelProvider->WriteAttribute(request, decoder);
+        AttributeValueDecoder decoder(aData, aSubject);
+        status = mDataModelProvider->WriteAttribute(request, decoder);
+    }
 
     mLastSuccessfullyWrittenPath = status.IsSuccess() ? std::make_optional(aPath) : std::nullopt;
 
diff --git a/src/app/WriteHandler.h b/src/app/WriteHandler.h
index fa1d324..afb29b0 100644
--- a/src/app/WriteHandler.h
+++ b/src/app/WriteHandler.h
@@ -24,6 +24,7 @@
 #include <app/ConcreteAttributePath.h>
 #include <app/InteractionModelDelegatePointers.h>
 #include <app/MessageDef/WriteResponseMessage.h>
+#include <app/data-model-provider/ActionReturnStatus.h>
 #include <app/data-model-provider/Provider.h>
 #include <lib/core/CHIPCore.h>
 #include <lib/core/TLVDebug.h>
@@ -185,6 +186,14 @@
                                  System::PacketBufferHandle && aPayload) override;
     void OnResponseTimeout(Messaging::ExchangeContext * apExchangeContext) override;
 
+    /// Validate that a write is acceptable on the given path.
+    ///
+    /// Validates that ACL, writability and Timed interaction settings are ok.
+    ///
+    /// Returns a success status if all is ok, failure otherwise.
+    DataModel::ActionReturnStatus CheckWriteAllowed(const Access::SubjectDescriptor & aSubject,
+                                                    const ConcreteAttributePath & aPath);
+
     // Write the given data to the given path
     CHIP_ERROR WriteClusterData(const Access::SubjectDescriptor & aSubject, const ConcreteDataAttributePath & aPath,
                                 TLV::TLVReader & aData);
diff --git a/src/app/data-model-provider/MetadataLookup.cpp b/src/app/data-model-provider/MetadataLookup.cpp
index a249b7e..cfbdb91 100644
--- a/src/app/data-model-provider/MetadataLookup.cpp
+++ b/src/app/data-model-provider/MetadataLookup.cpp
@@ -21,6 +21,8 @@
 namespace app {
 namespace DataModel {
 
+using Protocols::InteractionModel::Status;
+
 std::optional<ServerClusterEntry> ServerClusterFinder::Find(const ConcreteClusterPath & path)
 {
     VerifyOrReturnValue(mProvider != nullptr, std::nullopt);
@@ -63,25 +65,6 @@
     return std::nullopt;
 }
 
-EndpointFinder::EndpointFinder(ProviderMetadataTree * provider) : mProvider(provider)
-{
-    VerifyOrReturn(mProvider != nullptr);
-    mEndpoints = mProvider->EndpointsIgnoreError();
-}
-
-std::optional<EndpointEntry> EndpointFinder::Find(EndpointId endpointId)
-{
-    for (auto & endpointEntry : mEndpoints)
-    {
-        if (endpointEntry.id == endpointId)
-        {
-            return endpointEntry;
-        }
-    }
-
-    return std::nullopt;
-}
-
 Protocols::InteractionModel::Status ValidateClusterPath(ProviderMetadataTree * provider, const ConcreteClusterPath & path,
                                                         Protocols::InteractionModel::Status successStatus)
 {
@@ -90,6 +73,7 @@
         return successStatus;
     }
 
+    // If we get here, the cluster identified by the path does not exist.
     auto endpoints = provider->EndpointsIgnoreError();
     for (auto & endpointEntry : endpoints)
     {
diff --git a/src/app/data-model-provider/MetadataLookup.h b/src/app/data-model-provider/MetadataLookup.h
index 7526bd3..29ded48 100644
--- a/src/app/data-model-provider/MetadataLookup.h
+++ b/src/app/data-model-provider/MetadataLookup.h
@@ -24,6 +24,7 @@
 #include <app/data-model-provider/ProviderMetadataTree.h>
 #include <lib/core/DataModelTypes.h>
 #include <lib/support/CodeUtils.h>
+#include <protocols/interaction_model/StatusCode.h>
 
 #include <optional>
 
@@ -65,20 +66,14 @@
     ReadOnlyBuffer<AttributeEntry> mAttributes;
 };
 
-/// Helps search for a specific server endpoint in the given
-/// metadata provider.
+/// Validates that the cluster identified by `path` exists within the given provider.
 ///
-/// Facilitates the operation of "find an endpoint with a given ID".
-class EndpointFinder
-{
-public:
-    EndpointFinder(ProviderMetadataTree * provider);
-    std::optional<EndpointEntry> Find(EndpointId endpointId);
-
-private:
-    ProviderMetadataTree * mProvider;
-    ReadOnlyBuffer<EndpointEntry> mEndpoints;
-};
+/// If the endpoint identified by the path does not exist, will return Status::UnsupportedEndpoint.
+/// If the endpoint exists but does not have the cluster identified by the path, will return Status::UnsupportedCluster.
+///
+/// otherwise, it will return successStatus.
+Protocols::InteractionModel::Status ValidateClusterPath(ProviderMetadataTree * provider, const ConcreteClusterPath & path,
+                                                        Protocols::InteractionModel::Status successStatus);
 
 /// Validates that the cluster identified by `path` exists within the given provider.
 /// If the endpoint does not exist, will return Status::UnsupportedEndpoint.
diff --git a/src/app/data-model-provider/OperationTypes.h b/src/app/data-model-provider/OperationTypes.h
index 294bf26..35d1654 100644
--- a/src/app/data-model-provider/OperationTypes.h
+++ b/src/app/data-model-provider/OperationTypes.h
@@ -88,17 +88,6 @@
 {
     ConcreteDataAttributePath path; // NOTE: this also contains LIST operation options (i.e. "data" path type)
     BitFlags<WriteFlags> writeFlags;
-
-    // The path of the previous successful write in the same write transaction, if any.
-    //
-    // In particular this means that a write to this path has succeeded before (i.e. it passed required ACL checks).
-    // The intent for this is to allow short-cutting ACL checks when ACL is in progress of being updated:
-    //   - During write chunking, list writes can be of the form "reset list" followed by "append item by item"
-    //   - When ACL is updating, a reset to empty would result in the entire ACL being deny and the "append"
-    //     would fail.
-    // callers are expected to keep track of a `previousSuccessPath` whenever a write succeeds (otherwise ACL
-    // checks may fail)
-    std::optional<ConcreteAttributePath> previousSuccessPath;
 };
 
 enum class InvokeFlags : uint32_t
diff --git a/src/app/data-model-provider/Provider.h b/src/app/data-model-provider/Provider.h
index 36a13d0..fc5a204 100644
--- a/src/app/data-model-provider/Provider.h
+++ b/src/app/data-model-provider/Provider.h
@@ -76,12 +76,7 @@
     ///
     /// When this is invoked, caller is expected to have already done some validations:
     ///    - cluster `data version` has been checked for the incoming request if applicable
-    ///
-    /// TEMPORARY/TRANSITIONAL requirement for transitioning from ember-specific code
-    ///   WriteAttribute is REQUIRED to perform:
-    ///     - ACL validation (see notes on OperationFlags::kInternal)
-    ///     - Validation of readability/writability (also controlled by OperationFlags::kInternal)
-    ///     - Validation of timed interaction required (also controlled by OperationFlags::kInternal)
+    ///    - validation of ACL/timed interaction flags/writability, if those checks are desired.
     virtual ActionReturnStatus WriteAttribute(const WriteAttributeRequest & request, AttributeValueDecoder & decoder) = 0;
 
     /// `handler` is used to send back the reply.
diff --git a/src/app/data-model-provider/tests/WriteTesting.h b/src/app/data-model-provider/tests/WriteTesting.h
index 7651ffe..2ab3988 100644
--- a/src/app/data-model-provider/tests/WriteTesting.h
+++ b/src/app/data-model-provider/tests/WriteTesting.h
@@ -60,12 +60,6 @@
         return *this;
     }
 
-    WriteOperation & SetPreviousSuccessPath(std::optional<ConcreteAttributePath> path)
-    {
-        mRequest.previousSuccessPath = path;
-        return *this;
-    }
-
     WriteOperation & SetDataVersion(Optional<DataVersion> version)
     {
         mRequest.path.mDataVersion = version;
diff --git a/src/app/reporting/Engine.cpp b/src/app/reporting/Engine.cpp
index fbcd9d3..7b8654d 100644
--- a/src/app/reporting/Engine.cpp
+++ b/src/app/reporting/Engine.cpp
@@ -52,20 +52,6 @@
 
 using Protocols::InteractionModel::Status;
 
-Status EventPathValid(DataModel::Provider * model, const ConcreteEventPath & eventPath)
-{
-    {
-        DataModel::ServerClusterFinder serverClusterFinder(model);
-        if (serverClusterFinder.Find(eventPath).has_value())
-        {
-            return Status::Success;
-        }
-    }
-
-    DataModel::EndpointFinder endpointFinder(model);
-    return endpointFinder.Find(eventPath.mEndpointId).has_value() ? Status::UnsupportedCluster : Status::UnsupportedEndpoint;
-}
-
 /// Returns the status of ACL validation.
 ///   If the return value has a status set, that means the ACL check failed,
 ///   the read must not be performed, and the returned status (which may
@@ -530,7 +516,9 @@
         }
 
         ConcreteEventPath path(current->mValue.mEndpointId, current->mValue.mClusterId, current->mValue.mEventId);
-        Status status = EventPathValid(mpImEngine->GetDataModelProvider(), path);
+
+        // A event path is valid only if the cluster is valid
+        Status status = DataModel::ValidateClusterPath(mpImEngine->GetDataModelProvider(), path, Status::Success);
 
         if (status != Status::Success)
         {
diff --git a/src/app/tests/TestWriteInteraction.cpp b/src/app/tests/TestWriteInteraction.cpp
index e0560dd..dfe01ee 100644
--- a/src/app/tests/TestWriteInteraction.cpp
+++ b/src/app/tests/TestWriteInteraction.cpp
@@ -23,6 +23,7 @@
 #include <app/reporting/tests/MockReportScheduler.h>
 #include <app/tests/AppTestContext.h>
 #include <app/tests/test-interaction-model-api.h>
+#include <app/util/mock/MockNodeConfig.h>
 #include <credentials/GroupDataProviderImpl.h>
 #include <crypto/CHIPCryptoPAL.h>
 #include <crypto/DefaultSessionKeystore.h>
@@ -53,6 +54,74 @@
 
 namespace chip {
 namespace app {
+
+using namespace chip::Test;
+
+const MockNodeConfig & TestMockNodeConfig()
+{
+    using namespace Clusters::Globals::Attributes;
+
+    // clang-format off
+    static const MockNodeConfig config({
+        MockEndpointConfig(kRootEndpointId, {
+            MockClusterConfig(Clusters::IcdManagement::Id, {
+                ClusterRevision::Id, FeatureMap::Id,
+                Clusters::IcdManagement::Attributes::OperatingMode::Id,
+            }),
+        }),
+        MockEndpointConfig(kTestEndpointId, {
+            MockClusterConfig(Clusters::UnitTesting::Id, {
+                ClusterRevision::Id, FeatureMap::Id,
+                Clusters::UnitTesting::Attributes::Boolean::Id,
+                Clusters::UnitTesting::Attributes::Int16u::Id,
+                Clusters::UnitTesting::Attributes::ListFabricScoped::Id,
+                Clusters::UnitTesting::Attributes::ListStructOctetString::Id,
+            }),
+        }),
+        MockEndpointConfig(kMockEndpoint1, {
+            MockClusterConfig(MockClusterId(1), {
+                ClusterRevision::Id, FeatureMap::Id,
+            }, {
+                MockEventId(1), MockEventId(2),
+            }),
+            MockClusterConfig(MockClusterId(2), {
+                ClusterRevision::Id, FeatureMap::Id, MockAttributeId(1),
+            }),
+        }),
+        MockEndpointConfig(kMockEndpoint2, {
+            MockClusterConfig(MockClusterId(1), {
+                ClusterRevision::Id, FeatureMap::Id,
+            }),
+            MockClusterConfig(MockClusterId(2), {
+                ClusterRevision::Id, FeatureMap::Id, MockAttributeId(1), MockAttributeId(2),
+            }),
+            MockClusterConfig(MockClusterId(3), {
+                ClusterRevision::Id, FeatureMap::Id, MockAttributeId(1), MockAttributeId(2), MockAttributeId(3),
+            }),
+        }),
+        MockEndpointConfig(kMockEndpoint3, {
+            MockClusterConfig(MockClusterId(1), {
+                ClusterRevision::Id, FeatureMap::Id, MockAttributeId(1),
+            }),
+            MockClusterConfig(MockClusterId(2), {
+                ClusterRevision::Id, FeatureMap::Id, MockAttributeId(1), MockAttributeId(2), MockAttributeId(3), MockAttributeId(4),
+            }),
+            MockClusterConfig(MockClusterId(3), {
+                ClusterRevision::Id, FeatureMap::Id,
+            }),
+            MockClusterConfig(MockClusterId(4), {
+                ClusterRevision::Id, FeatureMap::Id,
+            }),
+        }),
+        /// For `TestWriteRoundtripWithClusterObjects` where path 2/3/4 is used
+        MockEndpointConfig(2, {
+            MockClusterConfig(3, {4}),
+        })
+    });
+    // clang-format on
+    return config;
+}
+
 class TestWriteInteraction : public chip::Test::AppContext
 {
 public:
@@ -72,9 +141,11 @@
         ASSERT_EQ(chip::GroupTesting::InitData(&gGroupsProvider, GetBobFabricIndex(), span), CHIP_NO_ERROR);
 
         mOldProvider = InteractionModelEngine::GetInstance()->SetDataModelProvider(&TestImCustomDataModel::Instance());
+        chip::Test::SetMockNodeConfig(TestMockNodeConfig());
     }
     void TearDown() override
     {
+        chip::Test::ResetMockNodeConfig();
         InteractionModelEngine::GetInstance()->SetDataModelProvider(mOldProvider);
         chip::Credentials::GroupDataProvider * provider = chip::Credentials::GetGroupDataProvider();
         if (provider != nullptr)
diff --git a/src/controller/tests/data_model/TestWrite.cpp b/src/controller/tests/data_model/TestWrite.cpp
index 2188327..c50e7aa 100644
--- a/src/controller/tests/data_model/TestWrite.cpp
+++ b/src/controller/tests/data_model/TestWrite.cpp
@@ -21,8 +21,8 @@
 
 #include "DataModelFixtures.h"
 
-#include "app-common/zap-generated/ids/Clusters.h"
 #include <app-common/zap-generated/cluster-objects.h>
+#include <app-common/zap-generated/ids/Clusters.h>
 #include <app/AttributeValueDecoder.h>
 #include <app/InteractionModelEngine.h>
 #include <app/WriteClient.h>
@@ -39,9 +39,72 @@
 using namespace chip::app::Clusters::UnitTesting;
 using namespace chip::app::DataModelTests;
 using namespace chip::Protocols;
+using namespace chip::Test;
 
 namespace {
 
+const MockNodeConfig & TestMockNodeConfig()
+{
+    using namespace Clusters::Globals::Attributes;
+
+    // clang-format off
+    static const MockNodeConfig config({
+        MockEndpointConfig(kRootEndpointId, {
+            MockClusterConfig(Clusters::IcdManagement::Id, {
+                ClusterRevision::Id, FeatureMap::Id,
+                Clusters::IcdManagement::Attributes::OperatingMode::Id,
+            }),
+        }),
+        MockEndpointConfig(kTestEndpointId, {
+            MockClusterConfig(Clusters::UnitTesting::Id, {
+                ClusterRevision::Id, FeatureMap::Id,
+                Clusters::UnitTesting::Attributes::Boolean::Id,
+                Clusters::UnitTesting::Attributes::Int16u::Id,
+                Clusters::UnitTesting::Attributes::Int8u::Id,
+                Clusters::UnitTesting::Attributes::ListFabricScoped::Id,
+                Clusters::UnitTesting::Attributes::ListStructOctetString::Id,
+            }),
+        }),
+        MockEndpointConfig(kMockEndpoint1, {
+            MockClusterConfig(MockClusterId(1), {
+                ClusterRevision::Id, FeatureMap::Id,
+            }, {
+                MockEventId(1), MockEventId(2),
+            }),
+            MockClusterConfig(MockClusterId(2), {
+                ClusterRevision::Id, FeatureMap::Id, MockAttributeId(1),
+            }),
+        }),
+        MockEndpointConfig(kMockEndpoint2, {
+            MockClusterConfig(MockClusterId(1), {
+                ClusterRevision::Id, FeatureMap::Id,
+            }),
+            MockClusterConfig(MockClusterId(2), {
+                ClusterRevision::Id, FeatureMap::Id, MockAttributeId(1), MockAttributeId(2),
+            }),
+            MockClusterConfig(MockClusterId(3), {
+                ClusterRevision::Id, FeatureMap::Id, MockAttributeId(1), MockAttributeId(2), MockAttributeId(3),
+            }),
+        }),
+        MockEndpointConfig(kMockEndpoint3, {
+            MockClusterConfig(MockClusterId(1), {
+                ClusterRevision::Id, FeatureMap::Id, MockAttributeId(1),
+            }),
+            MockClusterConfig(MockClusterId(2), {
+                ClusterRevision::Id, FeatureMap::Id, MockAttributeId(1), MockAttributeId(2), MockAttributeId(3), MockAttributeId(4),
+            }),
+            MockClusterConfig(MockClusterId(3), {
+                ClusterRevision::Id, FeatureMap::Id,
+            }),
+            MockClusterConfig(MockClusterId(4), {
+                ClusterRevision::Id, FeatureMap::Id,
+            }),
+        }),
+    });
+    // clang-format on
+    return config;
+}
+
 class SingleWriteCallback : public WriteClient::Callback
 {
 public:
@@ -89,11 +152,13 @@
     {
         chip::Test::AppContext::SetUp();
         mOldProvider = InteractionModelEngine::GetInstance()->SetDataModelProvider(&CustomDataModel::Instance());
+        chip::Test::SetMockNodeConfig(TestMockNodeConfig());
     }
 
     // Performs teardown for each individual test in the test suite
     void TearDown() override
     {
+        chip::Test::ResetMockNodeConfig();
         InteractionModelEngine::GetInstance()->SetDataModelProvider(mOldProvider);
         chip::Test::AppContext::TearDown();
     }
diff --git a/src/data-model-providers/codegen/CodegenDataModelProvider_Write.cpp b/src/data-model-providers/codegen/CodegenDataModelProvider_Write.cpp
index e0d9c15..3160bbd 100644
--- a/src/data-model-providers/codegen/CodegenDataModelProvider_Write.cpp
+++ b/src/data-model-providers/codegen/CodegenDataModelProvider_Write.cpp
@@ -92,18 +92,6 @@
 DataModel::ActionReturnStatus CodegenDataModelProvider::WriteAttribute(const DataModel::WriteAttributeRequest & request,
                                                                        AttributeValueDecoder & decoder)
 {
-    ChipLogDetail(DataManagement, "Writing attribute: Cluster=" ChipLogFormatMEI " Endpoint=0x%x AttributeId=" ChipLogFormatMEI,
-                  ChipLogValueMEI(request.path.mClusterId), request.path.mEndpointId, ChipLogValueMEI(request.path.mAttributeId));
-
-    // TODO: ordering is to check writability/existence BEFORE ACL and this seems wrong, however
-    //       existing unit tests (TC_AcessChecker.py) validate that we get UnsupportedWrite instead of UnsupportedAccess
-    //
-    //       This should likely be fixed in spec (probably already fixed by
-    //       https://github.com/CHIP-Specifications/connectedhomeip-spec/pull/9024)
-    //       and tests and implementation
-    //
-    //       Open issue that needs fixing: https://github.com/project-chip/connectedhomeip/issues/33735
-
     auto metadata = Ember::FindAttributeMetadata(request.path);
 
     // Explicit failure in finding a suitable metadata
@@ -130,74 +118,11 @@
     const EmberAfAttributeMetadata ** attributeMetadata = std::get_if<const EmberAfAttributeMetadata *>(&metadata);
     VerifyOrDie(*attributeMetadata != nullptr);
 
-    // All the global attributes that we do not have metadata for are
-    // read-only. Specifically only the following list-based attributes match the
-    // "global attributes not in metadata" (see GlobalAttributes.h :: GlobalAttributesNotInMetadata):
-    //   - AttributeList
-    //   - EventList
-    //   - AcceptedCommands
-    //   - GeneratedCommands
+    // Extra check: internal requests can bypass the read only check, however global attributes
+    // have no underlying storage, so write still cannot be done
     //
-    // Given the above, UnsupportedWrite should be correct (attempt to write to a read-only list)
-    bool isReadOnly = (*attributeMetadata)->IsReadOnly();
-
-    // Internal is allowed to bypass timed writes and read-only.
-    if (!request.operationFlags.Has(DataModel::OperationFlags::kInternal))
-    {
-        VerifyOrReturnError(!isReadOnly, Status::UnsupportedWrite);
-    }
-
-    // ACL check for non-internal requests
-    bool checkAcl = !request.operationFlags.Has(DataModel::OperationFlags::kInternal);
-
-    // For chunking, ACL check is not re-done if the previous write was successful for the exact same
-    // path. We apply this everywhere as a shortcut, although realistically this is only for AccessControl cluster
-    if (checkAcl && request.previousSuccessPath.has_value())
-    {
-        // NOTE: explicit cast/check only for attribute path and nothing else.
-        //
-        //       In particular `request.path` is a DATA path (contains a list index)
-        //       and we do not want request.previousSuccessPath to be auto-cast to a
-        //       data path with a empty list and fail the compare.
-        //
-        //       This could be `request.previousSuccessPath != request.path` (where order
-        //       is important) however that would seem more brittle (relying that a != b
-        //       behaves differently than b != a due to casts). Overall Data paths are not
-        //       the same as attribute paths.
-        //
-        //       Also note that Concrete path have a mExpanded that is not used in compares.
-        const ConcreteAttributePath & attributePathA = request.path;
-        const ConcreteAttributePath & attributePathB = *request.previousSuccessPath;
-
-        checkAcl = (attributePathA != attributePathB);
-    }
-
-    if (checkAcl)
-    {
-        VerifyOrReturnError(request.subjectDescriptor != nullptr, Status::UnsupportedAccess);
-
-        Access::RequestPath requestPath{ .cluster     = request.path.mClusterId,
-                                         .endpoint    = request.path.mEndpointId,
-                                         .requestType = Access::RequestType::kAttributeWriteRequest,
-                                         .entityId    = request.path.mAttributeId };
-        CHIP_ERROR err = Access::GetAccessControl().Check(*request.subjectDescriptor, requestPath,
-                                                          RequiredPrivilege::ForWriteAttribute(request.path));
-
-        if (err != CHIP_NO_ERROR)
-        {
-            VerifyOrReturnValue(err != CHIP_ERROR_ACCESS_DENIED, Status::UnsupportedAccess);
-            VerifyOrReturnValue(err != CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL, Status::AccessRestricted);
-
-            return err;
-        }
-    }
-
-    // Internal is allowed to bypass timed writes and read-only.
-    if (!request.operationFlags.Has(DataModel::OperationFlags::kInternal))
-    {
-        VerifyOrReturnError(!(*attributeMetadata)->MustUseTimedWrite() || request.writeFlags.Has(DataModel::WriteFlags::kTimed),
-                            Status::NeedsTimedInteraction);
-    }
+    // I.e. if we get a `EmberAfCluster*` value from finding metadata, we fail here.
+    VerifyOrReturnError(attributeMetadata != nullptr, Status::UnsupportedWrite);
 
     if (request.path.mDataVersion.HasValue())
     {
diff --git a/src/data-model-providers/codegen/tests/TestCodegenModelViaMocks.cpp b/src/data-model-providers/codegen/tests/TestCodegenModelViaMocks.cpp
index 4d54a19..2505fdd 100644
--- a/src/data-model-providers/codegen/tests/TestCodegenModelViaMocks.cpp
+++ b/src/data-model-providers/codegen/tests/TestCodegenModelViaMocks.cpp
@@ -1905,29 +1905,6 @@
     }
 }
 
-TEST_F(TestCodegenModelViaMocks, EmberAttributeWriteAclDeny)
-{
-    UseMockNodeConfig config(gTestNodeConfig);
-    CodegenDataModelProviderWithContext model;
-    ScopedMockAccessControl accessControl;
-
-    /* Using this path is also failing existence checks, so this cannot be enabled
-     * until we fix ordering of ACL to be done before existence checks
-
-      WriteOperation test(kMockEndpoint1, MockClusterId(1), MockAttributeId(10));
-      AttributeValueDecoder decoder = test.DecoderFor<uint32_t>(1234);
-
-      ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::UnsupportedAccess);
-      ASSERT_TRUE(model.ChangeListener().DirtyList().empty());
-    */
-
-    WriteOperation test(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_INT32U_ATTRIBUTE_TYPE));
-    AttributeValueDecoder decoder = test.DecoderFor<uint32_t>(1234);
-
-    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::UnsupportedAccess);
-    ASSERT_TRUE(model.ChangeListener().DirtyList().empty());
-}
-
 TEST_F(TestCodegenModelViaMocks, EmberAttributeWriteBasicTypes)
 {
     TestEmberScalarTypeWrite<uint8_t, ZCL_INT8U_ATTRIBUTE_TYPE>(0x12);
@@ -2208,42 +2185,6 @@
     EXPECT_EQ(writtenData[4], 13u);
 }
 
-TEST_F(TestCodegenModelViaMocks, EmberAttributeWriteTimedWrite)
-{
-    UseMockNodeConfig config(gTestNodeConfig);
-    CodegenDataModelProviderWithContext model;
-    ScopedMockAccessControl accessControl;
-
-    WriteOperation test(kMockEndpoint3, MockClusterId(4), kAttributeIdTimedWrite);
-    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
-
-    AttributeValueDecoder decoder = test.DecoderFor<int32_t>(1234);
-
-    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::NeedsTimedInteraction);
-
-    // writing as timed should be fine
-    test.SetWriteFlags(WriteFlags::kTimed);
-    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
-}
-
-TEST_F(TestCodegenModelViaMocks, EmberAttributeWriteReadOnlyAttribute)
-{
-    UseMockNodeConfig config(gTestNodeConfig);
-    CodegenDataModelProviderWithContext model;
-    ScopedMockAccessControl accessControl;
-
-    WriteOperation test(kMockEndpoint3, MockClusterId(4), kAttributeIdReadOnly);
-    test.SetSubjectDescriptor(kAdminSubjectDescriptor);
-
-    AttributeValueDecoder decoder = test.DecoderFor<int32_t>(1234);
-
-    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), Status::UnsupportedWrite);
-
-    // Internal writes bypass the read only requirement
-    test.SetOperationFlags(OperationFlags::kInternal);
-    ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_NO_ERROR);
-}
-
 TEST_F(TestCodegenModelViaMocks, EmberAttributeWriteDataVersion)
 {
     UseMockNodeConfig config(gTestNodeConfig);