@matter/node 0.14.1-alpha.0-20250606-a9bcd03f9 → 0.15.0-alpha.0-20250612-ddd428561

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/cjs/behavior/Events.d.ts +8 -3
  2. package/dist/cjs/behavior/Events.d.ts.map +1 -1
  3. package/dist/cjs/behavior/Events.js +5 -1
  4. package/dist/cjs/behavior/Events.js.map +1 -1
  5. package/dist/cjs/behavior/cluster/ClusterBehaviorUtil.js +3 -3
  6. package/dist/cjs/behavior/cluster/ClusterBehaviorUtil.js.map +1 -1
  7. package/dist/cjs/behavior/context/server/OnlineContext.d.ts +2 -1
  8. package/dist/cjs/behavior/context/server/OnlineContext.d.ts.map +1 -1
  9. package/dist/cjs/behavior/context/server/OnlineContext.js +22 -7
  10. package/dist/cjs/behavior/context/server/OnlineContext.js.map +1 -1
  11. package/dist/cjs/behavior/state/managed/Datasource.d.ts +6 -5
  12. package/dist/cjs/behavior/state/managed/Datasource.d.ts.map +1 -1
  13. package/dist/cjs/behavior/state/managed/Datasource.js +25 -14
  14. package/dist/cjs/behavior/state/managed/Datasource.js.map +1 -1
  15. package/dist/cjs/behavior/supervision/ValueSupervisor.d.ts +7 -3
  16. package/dist/cjs/behavior/supervision/ValueSupervisor.d.ts.map +1 -1
  17. package/dist/cjs/behaviors/access-control/AccessControlServer.d.ts +20 -36
  18. package/dist/cjs/behaviors/access-control/AccessControlServer.d.ts.map +1 -1
  19. package/dist/cjs/behaviors/access-control/AccessControlServer.js +153 -87
  20. package/dist/cjs/behaviors/access-control/AccessControlServer.js.map +1 -1
  21. package/dist/cjs/behaviors/operational-credentials/OperationalCredentialsServer.d.ts.map +1 -1
  22. package/dist/cjs/behaviors/operational-credentials/OperationalCredentialsServer.js +8 -19
  23. package/dist/cjs/behaviors/operational-credentials/OperationalCredentialsServer.js.map +2 -2
  24. package/dist/cjs/node/server/InteractionServer.d.ts.map +1 -1
  25. package/dist/cjs/node/server/InteractionServer.js +10 -44
  26. package/dist/cjs/node/server/InteractionServer.js.map +2 -2
  27. package/dist/cjs/node/server/ProtocolService.js +1 -1
  28. package/dist/cjs/node/server/ProtocolService.js.map +1 -1
  29. package/dist/esm/behavior/Events.d.ts +8 -3
  30. package/dist/esm/behavior/Events.d.ts.map +1 -1
  31. package/dist/esm/behavior/Events.js +5 -2
  32. package/dist/esm/behavior/Events.js.map +1 -1
  33. package/dist/esm/behavior/cluster/ClusterBehaviorUtil.js +4 -4
  34. package/dist/esm/behavior/cluster/ClusterBehaviorUtil.js.map +1 -1
  35. package/dist/esm/behavior/context/server/OnlineContext.d.ts +2 -1
  36. package/dist/esm/behavior/context/server/OnlineContext.d.ts.map +1 -1
  37. package/dist/esm/behavior/context/server/OnlineContext.js +29 -9
  38. package/dist/esm/behavior/context/server/OnlineContext.js.map +1 -1
  39. package/dist/esm/behavior/state/managed/Datasource.d.ts +6 -5
  40. package/dist/esm/behavior/state/managed/Datasource.d.ts.map +1 -1
  41. package/dist/esm/behavior/state/managed/Datasource.js +25 -14
  42. package/dist/esm/behavior/state/managed/Datasource.js.map +1 -1
  43. package/dist/esm/behavior/supervision/ValueSupervisor.d.ts +7 -3
  44. package/dist/esm/behavior/supervision/ValueSupervisor.d.ts.map +1 -1
  45. package/dist/esm/behaviors/access-control/AccessControlServer.d.ts +20 -36
  46. package/dist/esm/behaviors/access-control/AccessControlServer.d.ts.map +1 -1
  47. package/dist/esm/behaviors/access-control/AccessControlServer.js +153 -88
  48. package/dist/esm/behaviors/access-control/AccessControlServer.js.map +1 -1
  49. package/dist/esm/behaviors/operational-credentials/OperationalCredentialsServer.d.ts.map +1 -1
  50. package/dist/esm/behaviors/operational-credentials/OperationalCredentialsServer.js +8 -19
  51. package/dist/esm/behaviors/operational-credentials/OperationalCredentialsServer.js.map +1 -1
  52. package/dist/esm/node/server/InteractionServer.d.ts.map +1 -1
  53. package/dist/esm/node/server/InteractionServer.js +10 -44
  54. package/dist/esm/node/server/InteractionServer.js.map +1 -1
  55. package/dist/esm/node/server/ProtocolService.js +1 -1
  56. package/dist/esm/node/server/ProtocolService.js.map +1 -1
  57. package/package.json +7 -7
  58. package/src/behavior/Events.ts +8 -3
  59. package/src/behavior/cluster/ClusterBehaviorUtil.ts +4 -4
  60. package/src/behavior/context/server/OnlineContext.ts +39 -9
  61. package/src/behavior/state/managed/Datasource.ts +37 -20
  62. package/src/behavior/supervision/ValueSupervisor.ts +8 -3
  63. package/src/behaviors/access-control/AccessControlServer.ts +210 -102
  64. package/src/behaviors/operational-credentials/OperationalCredentialsServer.ts +10 -18
  65. package/src/node/server/InteractionServer.ts +10 -63
  66. package/src/node/server/ProtocolService.ts +1 -1
@@ -4,8 +4,8 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import type { Transaction } from "#general";
8
- import { AsyncObservable } from "#general";
7
+ import { ActionContext } from "#behavior/context/ActionContext.js";
8
+ import type { AsyncObservable, Transaction } from "#general";
9
9
  import { DataModelPath, Schema } from "#model";
10
10
  import type { AccessControl, Val } from "#protocol";
11
11
  import type { ValidationLocation } from "../state/validation/location.js";
@@ -84,7 +84,12 @@ export namespace ValueSupervisor {
84
84
  /**
85
85
  * If present the session is associated with an online interaction. Emits when the interaction ends.
86
86
  */
87
- interactionComplete?: AsyncObservable<[]>;
87
+ interactionComplete?: AsyncObservable<[session?: ActionContext]>;
88
+
89
+ /**
90
+ * Set to true when the interaction has started and the interactionBegin event was emitted for this session
91
+ */
92
+ interactionStarted?: boolean;
88
93
 
89
94
  /**
90
95
  * If true, structs initialize without named properties which are more expensive to install. This is useful
@@ -7,14 +7,16 @@
7
7
  import { ActionContext } from "#behavior/context/ActionContext.js";
8
8
  import { AccessControl as AccessControlTypes } from "#clusters/access-control";
9
9
  import { deepCopy, InternalError, Logger, MaybePromise } from "#general";
10
- import { AccessLevel } from "#model";
11
10
  import { NodeLifecycle } from "#node/NodeLifecycle.js";
12
11
  import {
13
12
  AccessControl,
14
- AccessControlManager,
15
13
  AclEndpointContext,
14
+ AclEntry,
15
+ AclList,
16
+ Fabric,
16
17
  FabricManager,
17
18
  IncomingSubjectDescriptor,
19
+ MessageExchange,
18
20
  NodeSession,
19
21
  SecureSession,
20
22
  } from "#protocol";
@@ -23,10 +25,12 @@ import {
23
25
  ClusterId,
24
26
  DeviceTypeId,
25
27
  EndpointNumber,
28
+ FabricIndex,
26
29
  GroupId,
27
30
  NodeId,
28
31
  StatusCode,
29
32
  StatusResponseError,
33
+ SubjectId,
30
34
  TlvTaggedList,
31
35
  TlvType,
32
36
  } from "#types";
@@ -36,6 +40,9 @@ const logger = Logger.get("AccessControlServer");
36
40
 
37
41
  /**
38
42
  * This is the default server implementation of AccessControlBehavior.
43
+ *
44
+ * When custom extensions are used, the `extensionEntryValidator` and `extensionEntryAccessCheck` methods can be
45
+ * overridden to implement custom validation and access checks for the extension entries.
39
46
  */
40
47
  export class AccessControlServer extends AccessControlBehavior.with("Extension") {
41
48
  declare internal: AccessControlServer.Internal;
@@ -57,20 +64,25 @@ export class AccessControlServer extends AccessControlBehavior.with("Extension")
57
64
  }
58
65
 
59
66
  #online() {
60
- // Handle Backward compatibility to Matter.js before 0.9.1 and add the missing ACL entry if no entry was set
61
- // so far by the controller
67
+ const aclsForFabric = this.#mapFabricAcls();
68
+
69
+ // Initialize the ACL managers for each fabric
62
70
  const fabrics = this.env.get(FabricManager);
63
- const acl = deepCopy(this.state.acl);
64
- const originalAclLength = acl.length;
65
71
  for (const fabric of fabrics) {
66
- if (!acl.some(entry => entry.fabricIndex === fabric.fabricIndex)) {
67
- acl.push({
72
+ const fabricAcls = aclsForFabric.get(fabric.fabricIndex) ?? [];
73
+
74
+ if (!fabricAcls.length) {
75
+ // Handle Backward compatibility to Matter.js before 0.9.1 and add the missing ACL entry if no entry was set
76
+ // so far by the controller
77
+ const fallbackAcl: AccessControlTypes.AccessControlEntry = {
68
78
  fabricIndex: fabric.fabricIndex,
69
79
  privilege: AccessControlTypes.AccessControlEntryPrivilege.Administer,
70
80
  authMode: AccessControlTypes.AccessControlEntryAuthMode.Case,
71
81
  subjects: [fabric.rootNodeId],
72
82
  targets: null, // entire node
73
- });
83
+ };
84
+ this.state.acl.push(fallbackAcl);
85
+ fabricAcls.push(fallbackAcl);
74
86
  logger.warn(
75
87
  "Added missing ACL entry for fabric",
76
88
  fabric.fabricIndex,
@@ -79,28 +91,51 @@ export class AccessControlServer extends AccessControlBehavior.with("Extension")
79
91
  ". This should only happen once after upgrading to matter.js 0.9.1",
80
92
  );
81
93
  }
82
- }
83
- if (acl.length > originalAclLength) {
84
- this.state.acl = acl;
94
+ fabric.acl.aclList = fabricAcls;
95
+ fabric.acl.extensionEntryAccessCheck = this.extensionEntryAccessCheck.bind(this);
85
96
  }
86
97
 
87
- logger.info("initializing ACL manager with ACL", acl);
88
- this.internal.aclManager = new AccessControlManager(
89
- acl,
90
- (aclList, aclEntry, subjectDesc, endpoint, clusterId) =>
91
- this.extensionEntryAccessCheck(
92
- aclList as unknown as AccessControlTypes.AccessControlEntry[],
93
- aclEntry as unknown as AccessControlTypes.AccessControlEntry,
94
- subjectDesc,
95
- endpoint,
96
- clusterId,
97
- ),
98
- );
98
+ // TODO handle delete fabric more generically later to remove fabric scoped data
99
+ this.reactTo(fabrics.events.updated, this.#updateFabricAcls);
100
+ this.reactTo(fabrics.events.added, this.#updateFabricAcls);
101
+
102
+ this.reactTo(this.events.interactionBegin, this.#handleInteractionBegin);
103
+ this.reactTo(this.events.interactionEnd, this.#handleInteractionEnd);
99
104
 
100
105
  this.reactTo(this.events.acl$Changed, this.#updateAccessControlList);
106
+
107
+ this.internal.initialized = true;
101
108
  }
102
109
 
103
- #validateAccessControlListChanges(value: AccessControlTypes.AccessControlEntry[]) {
110
+ addDefaultCaseAcl(fabric: Fabric, subjects: SubjectId[]) {
111
+ const entry = {
112
+ fabricIndex: fabric.fabricIndex,
113
+ privilege: AccessControlTypes.AccessControlEntryPrivilege.Administer,
114
+ authMode: AccessControlTypes.AccessControlEntryAuthMode.Case,
115
+ subjects: subjects as NodeId[],
116
+ targets: null, // entire node
117
+ };
118
+ this.state.acl.push(entry);
119
+ this.#updateFabricAcls(fabric);
120
+ // The update of the ACL is not validated because it has no assigned sess/fabric (and even then would use
121
+ // the wrong one), so we trigger the event ourself.
122
+ this.events.accessControlEntryChanged?.emit(
123
+ {
124
+ changeType: AccessControlTypes.ChangeType.Added,
125
+ adminNodeId: null, // When we add it, it is always from a PASE session
126
+ adminPasscodeId: 0, // When we add it, it is always from a PASE session
127
+ latestValue: entry,
128
+ fabricIndex: fabric.fabricIndex,
129
+ },
130
+ this.context,
131
+ );
132
+ }
133
+
134
+ #validateAccessControlListChanges(
135
+ value: AccessControlTypes.AccessControlEntry[],
136
+ _oldValue: AccessControlTypes.AccessControlEntry[],
137
+ context?: ActionContext,
138
+ ) {
104
139
  // TODO: This might be not really correct for local ACL changes because there the session fabric could be
105
140
  // different which would lead to missing validation of the relevant entries
106
141
  const relevantFabricIndex = this.context.session?.associatedFabric.fabricIndex;
@@ -108,6 +143,21 @@ export class AccessControlServer extends AccessControlBehavior.with("Extension")
108
143
  if (relevantFabricIndex === undefined) {
109
144
  return;
110
145
  }
146
+ if (context !== undefined && context.exchange !== undefined) {
147
+ const delayedChangeExchange = this.internal.aclUpdateDelayed.get(relevantFabricIndex);
148
+ if (delayedChangeExchange !== undefined && delayedChangeExchange !== context.exchange) {
149
+ // We are in a delayed ACL update with another exchange, so we do not process this one with Busy error
150
+ // This is formally not in specification, but chip also does this that way
151
+ logger.warn(
152
+ "Decline parallel ACL changes from multiple exchanges",
153
+ context.exchange.id,
154
+ "vs.",
155
+ delayedChangeExchange.id,
156
+ );
157
+ throw new StatusResponseError("Parallel ACL change from multiple exchanges", StatusCode.Busy);
158
+ }
159
+ }
160
+
111
161
  const fabricAcls = value.filter(entry => entry.fabricIndex === relevantFabricIndex);
112
162
  if (fabricAcls.length > this.state.accessControlEntriesPerFabric) {
113
163
  throw new StatusResponseError("AccessControlEntriesPerFabric exceeded", StatusCode.ResourceExhausted);
@@ -223,7 +273,7 @@ export class AccessControlServer extends AccessControlBehavior.with("Extension")
223
273
  value: AccessControlTypes.AccessControlEntry[],
224
274
  oldValue: AccessControlTypes.AccessControlEntry[],
225
275
  ) {
226
- if (this.internal.aclManager === undefined) {
276
+ if (!this.internal.initialized) {
227
277
  return; // Too early to send events
228
278
  }
229
279
  const { session } = this.context;
@@ -302,7 +352,7 @@ export class AccessControlServer extends AccessControlBehavior.with("Extension")
302
352
  value: AccessControlTypes.AccessControlExtension[],
303
353
  oldValue: AccessControlTypes.AccessControlExtension[],
304
354
  ) {
305
- if (this.internal.aclManager === undefined) {
355
+ if (!this.internal.initialized) {
306
356
  return; // Too early to send events
307
357
  }
308
358
  const { session } = this.context;
@@ -341,35 +391,6 @@ export class AccessControlServer extends AccessControlBehavior.with("Extension")
341
391
  );
342
392
  }
343
393
 
344
- /**
345
- * Implements the access control check for the given context, location and endpoint and is called by the
346
- * InteractionServer. The method returns the list of granted Access privileges for the given context, location and
347
- * endpoint.
348
- */
349
- accessLevelsFor(
350
- context: ActionContext,
351
- location: AccessControl.Location,
352
- endpoint?: AclEndpointContext,
353
- ): AccessLevel[] {
354
- if (location.cluster === undefined) {
355
- // Without a cluster, internal behaviors are only accessible internally so this is an irrelevant placeholder
356
- logger.warn("Access control check without cluster, returning View access level");
357
- return [AccessLevel.View];
358
- }
359
- if (context.session === undefined) {
360
- // Without a session, we can't determine access levels
361
- logger.warn("Access control check without session, returning View access level");
362
- return [AccessLevel.View];
363
- }
364
- if (endpoint === undefined) {
365
- // Without an endpoint, we can't determine access levels, ???
366
- logger.warn("Access control check without endpoint, returning View access level");
367
- return [AccessLevel.View];
368
- }
369
-
370
- return this.aclManager.getGrantedPrivileges(context, endpoint, location.cluster);
371
- }
372
-
373
394
  /**
374
395
  * This method allows to implement the validation of manufacturer specific ACL extensions when an extension entry is
375
396
  * added or changed. The default implementation checks whether the extension is a valid TLV and possible to decode.
@@ -399,8 +420,8 @@ export class AccessControlServer extends AccessControlBehavior.with("Extension")
399
420
  * validation.
400
421
  */
401
422
  protected extensionEntryAccessCheck(
402
- _aclList: AccessControlTypes.AccessControlEntry[],
403
- _aclEntry: AccessControlTypes.AccessControlEntry,
423
+ _aclList: AclList,
424
+ _aclEntry: AclEntry,
404
425
  _subjectDesc: IncomingSubjectDescriptor,
405
426
  _endpoint: AclEndpointContext,
406
427
  _clusterId: ClusterId,
@@ -408,77 +429,164 @@ export class AccessControlServer extends AccessControlBehavior.with("Extension")
408
429
  return true;
409
430
  }
410
431
 
432
+ /** A fabric was added or updated, so we need to initialize the ACL for this fabric */
433
+ #updateFabricAcls(fabric: Fabric) {
434
+ const fabricIndex = fabric.fabricIndex;
435
+ fabric.acl.aclList = deepCopy(this.state.acl).filter(entry => entry.fabricIndex === fabricIndex);
436
+ }
437
+
411
438
  /**
412
- * The AccessControlManager instance that is used to manage the ACL for this behavior.
439
+ * When beginning an interaction for an online session, we register the potential ACL change for the associated
440
+ * fabric index. If ACL data are really changed later, the exchange gets added then.
413
441
  */
414
- get aclManager() {
415
- if (this.internal.aclManager === undefined) {
416
- throw new InternalError("ACL manager not initialized yet");
442
+ #handleInteractionBegin(session?: AccessControl.Session) {
443
+ if (session !== undefined && !session.offline && session.fabric !== undefined) {
444
+ this.#prepareAclUpdateFor(session.fabric);
417
445
  }
418
- return this.internal.aclManager;
419
446
  }
420
447
 
421
- #updateAccessControlList(acl: AccessControlTypes.AccessControlEntry[]) {
422
- if (!this.aclUpdateDelayed) {
423
- logger.info("ACL updated, updating ACL manager", acl);
424
- this.aclManager.updateAccessControlList(deepCopy(acl));
448
+ /**
449
+ * When an interaction is finished, we check if there was a delayed ACL update for the associated fabric and apply
450
+ * it to the manager. For this we check if we have an exchange stored because otherwise the interaction was in fact
451
+ * not changing the ACL.
452
+ */
453
+ #handleInteractionEnd(session?: AccessControl.Session) {
454
+ if (session !== undefined && !session.offline && session.fabric !== undefined) {
455
+ if (this.internal.aclUpdateDelayed.get(session.fabric) !== undefined) {
456
+ this.#applyDelayedAclUpdateFor(session.fabric);
457
+ }
458
+ }
459
+ }
460
+
461
+ /** The ACL list was changed, so we need to determine if and when to apply the update to the ACL manager */
462
+ #updateAccessControlList(
463
+ acl: AccessControlTypes.AccessControlEntry[],
464
+ _oldAcl: AccessControlTypes.AccessControlEntry[],
465
+ context?: ActionContext,
466
+ ) {
467
+ if (context === undefined || context.offline) {
468
+ // local or offline ACL change, so we update all fabrics because we do not know better
469
+ this.#updateAllFabricsAcls();
470
+ } else {
471
+ const fabric = context.session?.associatedFabric;
472
+ if (fabric === undefined || fabric.fabricIndex === undefined || context.exchange === undefined) {
473
+ throw new InternalError("We require a fabric bound online session to write ACL changes");
474
+ }
475
+ this.#handleFabricAclUpdate(fabric, acl, context.exchange);
476
+ }
477
+ }
478
+
479
+ /**
480
+ * Handles the ACL update for a specific fabric. If an exchange is present, we delay the update until the
481
+ * interaction is finished.
482
+ */
483
+ #handleFabricAclUpdate(fabric: Fabric, acl: AccessControlTypes.AccessControlEntry[], exchange: MessageExchange) {
484
+ const fabricIndex = fabric.fabricIndex;
485
+ if (this.internal.aclUpdateDelayed.has(fabricIndex)) {
486
+ // We have registered an interaction for this fabric, so we delay the update
487
+ logger.debug(
488
+ "ACL attribute updated, but interaction still in progress, delaying update of ACL manager for FabricIndex",
489
+ fabricIndex,
490
+ );
491
+ this.#delayAclUpdateFor(fabricIndex, exchange, acl);
425
492
  } else {
426
- logger.info("ACL updated, but ACL manager update is delayed", acl);
427
- this.internal.delayedAclData = acl;
493
+ // No interaction registered, so we apply directly because local/offline change
494
+ logger.debug("ACL attribute updated, applying update to ACL manager", fabricIndex);
495
+
496
+ fabric.acl.aclList = deepCopy(acl).filter(entry => entry.fabricIndex === fabricIndex);
428
497
  }
429
498
  }
430
499
 
431
- resetDelayedAccessControlList() {
432
- this.internal.delayedAclData = undefined;
433
- this.aclUpdateDelayed = false;
500
+ #mapFabricAcls() {
501
+ const acl = deepCopy(this.state.acl);
502
+ const aclsForFabric = new Map<FabricIndex, AccessControlTypes.AccessControlEntry[]>();
503
+ // Collect ACLs for each fabric
504
+ for (const entry of acl) {
505
+ const { fabricIndex } = entry;
506
+ const acls = aclsForFabric.get(fabricIndex) ?? [];
507
+ acls.push(entry);
508
+ aclsForFabric.set(fabricIndex, acls);
509
+ }
510
+ return aclsForFabric;
511
+ }
512
+
513
+ /** Update all fabrics with the current ACL list */
514
+ #updateAllFabricsAcls() {
515
+ const aclsForFabric = this.#mapFabricAcls();
516
+
517
+ const fabrics = this.env.get(FabricManager);
518
+ for (const fabric of fabrics) {
519
+ // Update all Fabrics and set the ACL list for each fabric, empty ACLs when none are present
520
+ fabric.acl.aclList = aclsForFabric.get(fabric.fabricIndex) ?? [];
521
+ }
434
522
  }
435
523
 
436
524
  /**
437
- * If set to true, the ACL will not be updated immediately when it changes, but only when the `aclUpdateDelayed`
438
- * property is set to false again.
439
- * This is a hack to prevent the ACL from updating while we are in the middle of a write transaction and will be
440
- * removed again once we somehow handle relevant sub transactions.
525
+ * Register a potential change of ACL for a specific fabric index. if changes happened is checked when interaction
526
+ * ends.
441
527
  */
442
- get aclUpdateDelayed() {
443
- return this.internal.aclUpdateDelayed;
528
+ #prepareAclUpdateFor(fabricIndex: FabricIndex) {
529
+ if (!this.internal.aclUpdateDelayed.has(fabricIndex)) {
530
+ logger.info("Register ACL update to be delayed for fabricIndex", fabricIndex);
531
+ this.internal.aclUpdateDelayed.set(fabricIndex, undefined);
532
+ }
444
533
  }
445
534
 
446
535
  /**
447
- * If set to true, the ACL will not be updated immediately when it changes, but only when the `aclUpdateDelayed`
448
- * property is set to false again.
449
- * This is a hack to prevent the ACL from updating while we are in the middle of a write transaction and will be
450
- * removed again once we somehow handle relevant sub transactions.
536
+ * Register a concrete change of ACL for a specific fabric index. The exchange allows to also limit ACL changes to
537
+ * that exchange until interaction is finished.
451
538
  */
452
- set aclUpdateDelayed(value: boolean) {
453
- if (!value) {
454
- logger.info("Committing delayed ACL update");
455
- this.#updateDelayedAccessControlList();
456
- } else if (!this.internal.aclUpdateDelayed) {
457
- logger.info("Register ACL update to be delayed");
539
+ #delayAclUpdateFor(
540
+ fabricIndex: FabricIndex,
541
+ exchange: MessageExchange,
542
+ acl: AccessControlTypes.AccessControlEntry[],
543
+ ) {
544
+ if (!this.internal.aclUpdateDelayed.has(fabricIndex)) {
545
+ logger.info("Register ACL update to be delayed for fabricIndex", fabricIndex);
458
546
  }
459
- this.internal.aclUpdateDelayed = value;
547
+ this.internal.aclUpdateDelayed.set(fabricIndex, exchange);
548
+ this.internal.delayedAclData.set(
549
+ fabricIndex,
550
+ deepCopy(acl).filter(entry => entry.fabricIndex === fabricIndex),
551
+ );
460
552
  }
461
553
 
462
- #updateDelayedAccessControlList() {
463
- if (this.internal.delayedAclData === undefined) {
464
- return;
554
+ /** Applies the delayed ACL update for a specific fabric index, if existing */
555
+ #applyDelayedAclUpdateFor(fabricIndex: FabricIndex) {
556
+ const updateDelayed = !!this.internal.aclUpdateDelayed.get(fabricIndex);
557
+ const delayedData = this.internal.delayedAclData.get(fabricIndex);
558
+
559
+ this.internal.delayedAclData.delete(fabricIndex);
560
+ this.internal.aclUpdateDelayed.delete(fabricIndex);
561
+ if (updateDelayed && delayedData !== undefined) {
562
+ this.env.get(FabricManager).for(fabricIndex).acl.aclList = delayedData;
465
563
  }
466
- const delayedData = deepCopy(this.internal.delayedAclData);
467
- this.internal.delayedAclData = undefined;
468
- logger.info("Updating ACL manager with ACL", delayedData);
469
- this.aclManager.updateAccessControlList(delayedData);
470
564
  }
471
565
  }
472
566
 
473
567
  export namespace AccessControlServer {
474
568
  export class Internal {
475
- /** AccessControlManager instance that is used to manage the ACL checks for this device. */
476
- aclManager?: AccessControlManager;
569
+ /** Is the cluster logic initialized? Used to block events before full initialization. */
570
+ initialized = false;
477
571
 
478
- /** If set to true ACL updates are delayed while in a write transaction. More details see getter/setter above. */
479
- aclUpdateDelayed = false;
572
+ /**
573
+ * When an online and potentially chunked ACL writing happens, we will delay the update and store the exchange
574
+ * used for the writing. With this we also verify that concurrent writes are blocked and will not mix the data.
575
+ */
576
+ aclUpdateDelayed = new Map<FabricIndex, MessageExchange | undefined>();
480
577
 
481
- /** Latest delayed data of acl */
482
- delayedAclData?: AccessControlTypes.AccessControlEntry[];
578
+ /** Latest delayed data of acl attribute */
579
+ delayedAclData = new Map<FabricIndex, AccessControlTypes.AccessControlEntry[]>();
483
580
  }
581
+
582
+ export declare const ExtensionInterface: {
583
+ extensionEntryValidator: (extension: AccessControlTypes.AccessControlExtension) => void;
584
+ extensionEntryAccessCheck: (
585
+ aclList: AclList,
586
+ aclEntry: AclEntry,
587
+ subjectDesc: IncomingSubjectDescriptor,
588
+ endpoint: AclEndpointContext,
589
+ clusterId: ClusterId,
590
+ ) => boolean;
591
+ };
484
592
  }
@@ -8,7 +8,6 @@ import { ValueSupervisor } from "#behavior/supervision/ValueSupervisor.js";
8
8
  import { CommissioningServer } from "#behavior/system/commissioning/CommissioningServer.js";
9
9
  import { ProductDescriptionServer } from "#behavior/system/product-description/ProductDescriptionServer.js";
10
10
  import { AccessControlServer } from "#behaviors/access-control";
11
- import { AccessControl } from "#clusters/access-control";
12
11
  import { OperationalCredentials } from "#clusters/operational-credentials";
13
12
  import { Endpoint } from "#endpoint/Endpoint.js";
14
13
  import { CryptoVerifyError, Logger, MatterFlowError, MaybePromise, UnexpectedDataError } from "#general";
@@ -104,15 +103,15 @@ export class OperationalCredentialsServer extends OperationalCredentialsBehavior
104
103
  const session = this.session;
105
104
  NodeSession.assert(session);
106
105
 
107
- const elements = TlvAttestation.encode({
106
+ const attestationElements = TlvAttestation.encode({
108
107
  declaration: certification.declaration,
109
108
  attestationNonce: attestationNonce,
110
109
  timestamp: 0,
111
110
  });
112
- return {
113
- attestationElements: elements,
114
- attestationSignature: await certification.sign(session, elements),
115
- };
111
+
112
+ const attestationSignature = await certification.sign(session, attestationElements);
113
+
114
+ return { attestationElements, attestationSignature };
116
115
  }
117
116
 
118
117
  override async csrRequest({ csrNonce, isForUpdateNoc }: OperationalCredentials.CsrRequest) {
@@ -260,14 +259,7 @@ export class OperationalCredentialsServer extends OperationalCredentialsBehavior
260
259
 
261
260
  // The receiver SHALL create and add a new Access Control Entry using the CaseAdminSubject field to grant
262
261
  // subsequent Administer access to an Administrator member of the new Fabric.
263
- const aclCluster = this.agent.get(AccessControlServer);
264
- aclCluster.state.acl.push({
265
- fabricIndex: fabric.fabricIndex,
266
- privilege: AccessControl.AccessControlEntryPrivilege.Administer,
267
- authMode: AccessControl.AccessControlEntryAuthMode.Case,
268
- subjects: [caseAdminSubject],
269
- targets: null, // entire node
270
- });
262
+ await this.endpoint.act(agent => agent.get(AccessControlServer).addDefaultCaseAcl(fabric, [caseAdminSubject]));
271
263
 
272
264
  const session = this.session;
273
265
  NodeSession.assert(session);
@@ -355,14 +347,14 @@ export class OperationalCredentialsServer extends OperationalCredentialsBehavior
355
347
 
356
348
  // Build a new Fabric with the updated NOC and ICAC
357
349
  try {
358
- const updateFabric = await timedOp.buildUpdatedFabric(nocValue, icacValue);
350
+ const updatedFabric = await timedOp.buildUpdatedFabric(nocValue, icacValue);
359
351
 
360
352
  // update FabricManager and Resumption records but leave current session intact
361
- await timedOp.updateFabric(updateFabric);
353
+ await timedOp.updateFabric(updatedFabric);
362
354
 
363
355
  return {
364
356
  statusCode: OperationalCredentials.NodeOperationalCertStatus.Ok,
365
- fabricIndex: updateFabric.fabricIndex,
357
+ fabricIndex: updatedFabric.fabricIndex,
366
358
  };
367
359
  } catch (error) {
368
360
  logger.info("Building fabric for updateNoc failed", error);
@@ -432,7 +424,7 @@ export class OperationalCredentialsServer extends OperationalCredentialsBehavior
432
424
  try {
433
425
  await failsafeContext.setRootCert(rootCaCertificate);
434
426
  } catch (error) {
435
- logger.info("setting root certificate failed", error);
427
+ logger.info("Error installing root certificate:", error);
436
428
  if (
437
429
  error instanceof CryptoVerifyError ||
438
430
  error instanceof CertificateError ||
@@ -7,7 +7,6 @@
7
7
  import { NodeActivity } from "#behavior/context/NodeActivity.js";
8
8
  import { OnlineContext } from "#behavior/context/server/OnlineContext.js";
9
9
  import { AccessControlServer } from "#behaviors/access-control";
10
- import { AccessControl as AccessControlClusterType } from "#clusters/access-control";
11
10
  import {
12
11
  Crypto,
13
12
  Diagnostic,
@@ -39,7 +38,6 @@ import {
39
38
  TimedRequest,
40
39
  WriteRequest,
41
40
  WriteResponse,
42
- WriteResult,
43
41
  } from "#protocol";
44
42
  import {
45
43
  DEFAULT_MAX_PATHS_PER_INVOKE,
@@ -68,10 +66,6 @@ export interface WithActivity {
68
66
  [activityKey]?: NodeActivity.Activity;
69
67
  }
70
68
 
71
- const AclClusterId = AccessControlClusterType.Complete.id;
72
- const AclAttributeId = AccessControlClusterType.Complete.attributes.acl.id;
73
- const AclExtensionAttributeId = AccessControlClusterType.Complete.attributes.extension.id;
74
-
75
69
  export interface PeerSubscription {
76
70
  subscriptionId: number;
77
71
  peerAddress: PeerAddress;
@@ -139,7 +133,6 @@ export class InteractionServer implements ProtocolHandler, InteractionRecipient
139
133
  #activity: NodeActivity;
140
134
  #newActivityBlocked = false;
141
135
  #aclServer?: AccessControlServer;
142
- #aclUpdateIsDelayedInExchange = new Set<MessageExchange>();
143
136
  #serverInteraction: OnlineServerInteraction;
144
137
 
145
138
  constructor(node: ServerNode, sessions: SessionManager) {
@@ -372,63 +365,17 @@ export class InteractionServer implements ProtocolHandler, InteractionRecipient
372
365
  );
373
366
  }
374
367
 
375
- // This is a hack to prevent the ACL from updating while we are in the middle of a write transaction
376
- // and is needed because Acl should not become effective during writing of the ACL itself.
377
- // We check if any ACL path is written and assume that acl write will happen, so we delay the update.
378
- // If nothing changed in the end this does not matter
379
- for (const {
380
- path: { endpointId, clusterId, attributeId },
381
- } of writeRequest.writeRequests) {
382
- if (
383
- (endpointId === undefined || endpointId === 0) &&
384
- clusterId === AclClusterId &&
385
- (attributeId === AclAttributeId || attributeId === AclExtensionAttributeId)
386
- ) {
387
- this.aclServer.aclUpdateDelayed = true;
388
- this.#aclUpdateIsDelayedInExchange.add(exchange);
389
- }
390
- }
391
-
392
- let result: Awaited<WriteResult<any>>;
393
- try {
394
- // TODO: We still need to add multi message writes!
395
-
396
- result = await this.#serverInteraction.write(
397
- writeRequest,
398
- this.#prepareOnlineContext(
399
- exchange,
400
- message,
401
- true, // always fabric filtered
402
- receivedWithinTimedInteraction,
403
- ),
404
- );
405
- } catch (error) {
406
- if (this.#aclUpdateIsDelayedInExchange.has(exchange)) {
407
- // Unlikely to get there at all, but make sure we handle it best we can for now
408
- this.#aclUpdateIsDelayedInExchange.delete(exchange);
409
-
410
- if (this.#aclUpdateIsDelayedInExchange.size === 0) {
411
- // only that one ACl change in flight, so we can reset the delayed ACL
412
- this.aclServer.resetDelayedAccessControlList();
413
- } else {
414
- // TODO: we should restore the delayed data just for this errored fabric?
415
- logger.error("One of multiple concurrent ACL writes failed, unhandled case for now.");
416
- }
417
- }
418
- throw error;
419
- }
420
-
421
- // We delayed the ACL update during this write transaction, so we need to update it now that anything is written
422
- if (this.#aclUpdateIsDelayedInExchange.has(exchange)) {
423
- this.#aclUpdateIsDelayedInExchange.delete(exchange);
368
+ // TODO: We still need to add multi message writes!
424
369
 
425
- if (this.#aclUpdateIsDelayedInExchange.size === 0) {
426
- // Committing the ACL changes in case of an unhandled exception might be dangerous, but we do it anyway
427
- this.aclServer.aclUpdateDelayed = false;
428
- } else {
429
- logger.info("Multiple concurrent ACL writes, waiting for all to finish.");
430
- }
431
- }
370
+ const result = await this.#serverInteraction.write(
371
+ writeRequest,
372
+ this.#prepareOnlineContext(
373
+ exchange,
374
+ message,
375
+ true, // always fabric filtered
376
+ receivedWithinTimedInteraction,
377
+ ),
378
+ );
432
379
 
433
380
  return {
434
381
  writeResponses: result?.map(({ path, status, clusterStatus }) => ({
@@ -316,7 +316,7 @@ class ClusterState implements DisposableClusterProtocol {
316
316
  }
317
317
 
318
318
  // Emit all attributes as changed that are not omitted or quieter
319
- this.#quieterObservers.on(this.#datasource.stateChanged, (changes: string[], version: number) => {
319
+ this.#quieterObservers.on(this.#datasource.changed, (changes: string[], version: number) => {
320
320
  const data = changes
321
321
  .map(name => attributeNameToIdMap.get(name))
322
322
  .filter(