Line data Source code
1 : /*
2 : * Copyright (c) 2024 Project CHIP Authors
3 : * All rights reserved.
4 : *
5 : * Licensed under the Apache License, Version 2.0 (the "License");
6 : * you may not use this file except in compliance with the License.
7 : * You may obtain a copy of the License at
8 : *
9 : * http://www.apache.org/licenses/LICENSE-2.0
10 : *
11 : * Unless required by applicable law or agreed to in writing, software
12 : * distributed under the License is distributed on an "AS IS" BASIS,
13 : * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 : * See the License for the specific language governing permissions and
15 : * limitations under the License.
16 : */
17 : #include <data-model-providers/codegen/CodegenDataModelProvider.h>
18 :
19 : #include <access/AccessControl.h>
20 : #include <app-common/zap-generated/attribute-type.h>
21 : #include <app/AttributeAccessInterface.h>
22 : #include <app/AttributeAccessInterfaceRegistry.h>
23 : #include <app/RequiredPrivilege.h>
24 : #include <app/data-model/FabricScoped.h>
25 : #include <app/reporting/reporting.h>
26 : #include <app/util/af-types.h>
27 : #include <app/util/attribute-metadata.h>
28 : #include <app/util/attribute-storage-detail.h>
29 : #include <app/util/attribute-storage-null-handling.h>
30 : #include <app/util/attribute-storage.h>
31 : #include <app/util/attribute-table-detail.h>
32 : #include <app/util/attribute-table.h>
33 : #include <app/util/ember-io-storage.h>
34 : #include <app/util/ember-strings.h>
35 : #include <app/util/odd-sized-integers.h>
36 : #include <data-model-providers/codegen/EmberAttributeDataBuffer.h>
37 : #include <data-model-providers/codegen/EmberMetadata.h>
38 : #include <lib/core/CHIPError.h>
39 : #include <lib/support/CodeUtils.h>
40 :
41 : #include <zap-generated/endpoint_config.h>
42 :
43 : namespace chip {
44 : namespace app {
45 : namespace {
46 :
47 : using namespace chip::app::Compatibility::Internal;
48 : using Protocols::InteractionModel::Status;
49 :
50 : class ContextAttributesChangeListener : public AttributesChangedListener
51 : {
52 : public:
53 2579 : ContextAttributesChangeListener(const DataModel::InteractionModelContext & context) : mListener(context.dataModelChangeListener)
54 2579 : {}
55 2546 : void MarkDirty(const AttributePathParams & path) override { mListener->MarkDirty(path); }
56 :
57 : private:
58 : DataModel::ProviderChangeListener * mListener;
59 : };
60 :
61 : /// Attempts to write via an attribute access interface (AAI)
62 : ///
63 : /// If it returns a CHIP_ERROR, then this is a FINAL result (i.e. either failure or success)
64 : ///
65 : /// If it returns std::nullopt, then there is no AAI to handle the given path
66 : /// and processing should figure out the value otherwise (generally from other ember data)
67 2579 : std::optional<CHIP_ERROR> TryWriteViaAccessInterface(const ConcreteDataAttributePath & path, AttributeAccessInterface * aai,
68 : AttributeValueDecoder & decoder)
69 : {
70 : // Processing can happen only if an attribute access interface actually exists..
71 2579 : if (aai == nullptr)
72 : {
73 139 : return std::nullopt;
74 : }
75 :
76 2440 : CHIP_ERROR err = aai->Write(path, decoder);
77 :
78 2440 : if (err != CHIP_NO_ERROR)
79 : {
80 6 : return std::make_optional(err);
81 : }
82 :
83 : // If the decoder tried to decode, then a value should have been read for processing.
84 : // - if decoding was done, assume DONE (i.e. final CHIP_NO_ERROR)
85 : // - otherwise, if no decoding done, return that processing must continue via nullopt
86 4868 : return decoder.TriedDecode() ? std::make_optional(CHIP_NO_ERROR) : std::nullopt;
87 : }
88 :
89 : } // namespace
90 :
91 2587 : DataModel::ActionReturnStatus CodegenDataModelProvider::WriteAttribute(const DataModel::WriteAttributeRequest & request,
92 : AttributeValueDecoder & decoder)
93 : {
94 2587 : ChipLogDetail(DataManagement, "Writing attribute: Cluster=" ChipLogFormatMEI " Endpoint=0x%x AttributeId=" ChipLogFormatMEI,
95 : ChipLogValueMEI(request.path.mClusterId), request.path.mEndpointId, ChipLogValueMEI(request.path.mAttributeId));
96 :
97 : // TODO: ordering is to check writability/existence BEFORE ACL and this seems wrong, however
98 : // existing unit tests (TC_AcessChecker.py) validate that we get UnsupportedWrite instead of UnsupportedAccess
99 : //
100 : // This should likely be fixed in spec (probably already fixed by
101 : // https://github.com/CHIP-Specifications/connectedhomeip-spec/pull/9024)
102 : // and tests and implementation
103 : //
104 : // Open issue that needs fixing: https://github.com/project-chip/connectedhomeip/issues/33735
105 2587 : auto metadata = Ember::FindAttributeMetadata(request.path);
106 :
107 : // Explicit failure in finding a suitable metadata
108 2587 : if (const Status * status = std::get_if<Status>(&metadata))
109 : {
110 3 : VerifyOrDie((*status == Status::UnsupportedEndpoint) || //
111 : (*status == Status::UnsupportedCluster) || //
112 : (*status == Status::UnsupportedAttribute));
113 3 : return *status;
114 : }
115 :
116 2584 : const EmberAfAttributeMetadata ** attributeMetadata = std::get_if<const EmberAfAttributeMetadata *>(&metadata);
117 :
118 : // All the global attributes that we do not have metadata for are
119 : // read-only. Specifically only the following list-based attributes match the
120 : // "global attributes not in metadata" (see GlobalAttributes.h :: GlobalAttributesNotInMetadata):
121 : // - AttributeList
122 : // - EventList
123 : // - AcceptedCommands
124 : // - GeneratedCommands
125 : //
126 : // Given the above, UnsupportedWrite should be correct (attempt to write to a read-only list)
127 2584 : bool isReadOnly = (attributeMetadata == nullptr) || (*attributeMetadata)->IsReadOnly();
128 :
129 : // Internal is allowed to bypass timed writes and read-only.
130 2584 : if (!request.operationFlags.Has(DataModel::OperationFlags::kInternal))
131 : {
132 2583 : VerifyOrReturnError(!isReadOnly, Status::UnsupportedWrite);
133 : }
134 :
135 : // ACL check for non-internal requests
136 2582 : bool checkAcl = !request.operationFlags.Has(DataModel::OperationFlags::kInternal);
137 :
138 : // For chunking, ACL check is not re-done if the previous write was successful for the exact same
139 : // path. We apply this everywhere as a shortcut, although realistically this is only for AccessControl cluster
140 2582 : if (checkAcl && request.previousSuccessPath.has_value())
141 : {
142 : // NOTE: explicit cast/check only for attribute path and nothing else.
143 : //
144 : // In particular `request.path` is a DATA path (contains a list index)
145 : // and we do not want request.previousSuccessPath to be auto-cast to a
146 : // data path with a empty list and fail the compare.
147 : //
148 : // This could be `request.previousSuccessPath != request.path` (where order
149 : // is important) however that would seem more brittle (relying that a != b
150 : // behaves differently than b != a due to casts). Overall Data paths are not
151 : // the same as attribute paths.
152 : //
153 : // Also note that Concrete path have a mExpanded that is not used in compares.
154 720 : const ConcreteAttributePath & attributePathA = request.path;
155 720 : const ConcreteAttributePath & attributePathB = *request.previousSuccessPath;
156 :
157 720 : checkAcl = (attributePathA != attributePathB);
158 : }
159 :
160 2582 : if (checkAcl)
161 : {
162 1864 : VerifyOrReturnError(request.subjectDescriptor != nullptr, Status::UnsupportedAccess);
163 :
164 1863 : Access::RequestPath requestPath{ .cluster = request.path.mClusterId,
165 1863 : .endpoint = request.path.mEndpointId,
166 : .requestType = Access::RequestType::kAttributeWriteRequest,
167 1863 : .entityId = request.path.mAttributeId };
168 1863 : CHIP_ERROR err = Access::GetAccessControl().Check(*request.subjectDescriptor, requestPath,
169 : RequiredPrivilege::ForWriteAttribute(request.path));
170 :
171 1863 : if (err != CHIP_NO_ERROR)
172 : {
173 1 : VerifyOrReturnValue(err != CHIP_ERROR_ACCESS_DENIED, Status::UnsupportedAccess);
174 0 : VerifyOrReturnValue(err != CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL, Status::AccessRestricted);
175 :
176 0 : return err;
177 : }
178 : }
179 :
180 : // Internal is allowed to bypass timed writes and read-only.
181 2581 : if (!request.operationFlags.Has(DataModel::OperationFlags::kInternal))
182 : {
183 2580 : VerifyOrReturnError(!(*attributeMetadata)->MustUseTimedWrite() || request.writeFlags.Has(DataModel::WriteFlags::kTimed),
184 : Status::NeedsTimedInteraction);
185 : }
186 :
187 : // Extra check: internal requests can bypass the read only check, however global attributes
188 : // have no underlying storage, so write still cannot be done
189 2580 : VerifyOrReturnError(attributeMetadata != nullptr, Status::UnsupportedWrite);
190 :
191 2580 : if (request.path.mDataVersion.HasValue())
192 : {
193 2 : std::optional<DataModel::ClusterInfo> clusterInfo = GetServerClusterInfo(request.path);
194 2 : if (!clusterInfo.has_value())
195 : {
196 0 : ChipLogError(DataManagement, "Unable to get cluster info for Endpoint 0x%x, Cluster " ChipLogFormatMEI,
197 : request.path.mEndpointId, ChipLogValueMEI(request.path.mClusterId));
198 1 : return Status::DataVersionMismatch;
199 : }
200 :
201 2 : if (request.path.mDataVersion.Value() != clusterInfo->dataVersion)
202 : {
203 1 : ChipLogError(DataManagement, "Write Version mismatch for Endpoint 0x%x, Cluster " ChipLogFormatMEI,
204 : request.path.mEndpointId, ChipLogValueMEI(request.path.mClusterId));
205 1 : return Status::DataVersionMismatch;
206 : }
207 : }
208 :
209 2579 : ContextAttributesChangeListener change_listener(CurrentContext());
210 :
211 : AttributeAccessInterface * aai =
212 2579 : AttributeAccessInterfaceRegistry::Instance().Get(request.path.mEndpointId, request.path.mClusterId);
213 2579 : std::optional<CHIP_ERROR> aai_result = TryWriteViaAccessInterface(request.path, aai, decoder);
214 2579 : if (aai_result.has_value())
215 : {
216 2436 : if (*aai_result == CHIP_NO_ERROR)
217 : {
218 : // TODO: this is awkward since it provides AAI no control over this, specifically
219 : // AAI may not want to increase versions for some attributes that are Q
220 2430 : emberAfAttributeChanged(request.path.mEndpointId, request.path.mClusterId, request.path.mAttributeId, &change_listener);
221 : }
222 2436 : return *aai_result;
223 : }
224 :
225 143 : MutableByteSpan dataBuffer = gEmberAttributeIOBufferSpan;
226 : {
227 143 : Ember::EmberAttributeDataBuffer emberData(*attributeMetadata, dataBuffer);
228 143 : ReturnErrorOnFailure(decoder.Decode(emberData));
229 : }
230 :
231 : Protocols::InteractionModel::Status status;
232 :
233 119 : if (dataBuffer.size() > (*attributeMetadata)->size)
234 : {
235 1 : ChipLogDetail(Zcl, "Data to write exceeds the attribute size claimed.");
236 1 : return Status::InvalidValue;
237 : }
238 :
239 118 : EmberAfWriteDataInput dataInput(dataBuffer.data(), (*attributeMetadata)->attributeType);
240 :
241 118 : dataInput.SetChangeListener(&change_listener);
242 : // TODO: dataInput.SetMarkDirty() should be according to `ChangesOmmited`
243 :
244 118 : if (request.operationFlags.Has(DataModel::OperationFlags::kInternal))
245 : {
246 : // Internal requests use the non-External interface that has less enforcement
247 : // than the external version (e.g. does not check/enforce writable settings, does not
248 : // validate attribute types) - see attribute-table.h documentation for details.
249 1 : status = emberAfWriteAttribute(request.path, dataInput);
250 : }
251 : else
252 : {
253 117 : status = emAfWriteAttributeExternal(request.path, dataInput);
254 : }
255 :
256 118 : if (status != Protocols::InteractionModel::Status::Success)
257 : {
258 2 : return status;
259 : }
260 :
261 116 : return CHIP_NO_ERROR;
262 2579 : }
263 :
264 0 : void CodegenDataModelProvider::Temporary_ReportAttributeChanged(const AttributePathParams & path)
265 : {
266 0 : ContextAttributesChangeListener change_listener(CurrentContext());
267 0 : if (path.mClusterId != kInvalidClusterId)
268 : {
269 0 : emberAfAttributeChanged(path.mEndpointId, path.mClusterId, path.mAttributeId, &change_listener);
270 : }
271 : else
272 : {
273 : // When the path has wildcard cluster Id, call the emberAfEndpointChanged to mark attributes on the given endpoint
274 : // as having changing, but do NOT increase/alter any cluster data versions, as this happens when a bridged endpoint is
275 : // added or removed from a bridge and the cluster data is not changed during the process.
276 0 : emberAfEndpointChanged(path.mEndpointId, &change_listener);
277 : }
278 0 : }
279 :
280 : } // namespace app
281 : } // namespace chip
|