"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var AccessControlServer_exports = {};
__export(AccessControlServer_exports, {
  AccessControlServer: () => AccessControlServer
});
module.exports = __toCommonJS(AccessControlServer_exports);
var import_access_control = require("#clusters/access-control");
var import_general = require("#general");
var import_model = require("#model");
var import_protocol = require("#protocol");
var import_types = require("#types");
var import_AccessControlBehavior = require("./AccessControlBehavior.js");
/**
 * @license
 * Copyright 2022-2024 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
const logger = import_general.Logger.get("AccessControlServer");
class AccessControlServer extends import_AccessControlBehavior.AccessControlBehavior {
  initialize() {
    this.reactTo(this.events.acl$Changing, this.#validateAccessControlListChanges);
    this.reactTo(this.events.acl$Changed, this.#handleAccessControlListChange);
    if (this.state.extension !== void 0 && this.events.extension$Changing !== void 0 && this.events.extension$Changed !== void 0) {
      this.reactTo(this.events.extension$Changing, this.#validateAccessControlExtensionChanges);
      this.reactTo(this.events.extension$Changed, this.#handleAccessControlExtensionChange);
    }
    const lifecycle = this.endpoint.lifecycle;
    this.reactTo(lifecycle.online, this.#online);
  }
  #online() {
    const fabrics = this.env.get(import_protocol.FabricManager);
    const acl = (0, import_general.deepCopy)(this.state.acl);
    const originalAclLength = acl.length;
    for (const fabric of fabrics) {
      if (!acl.some((entry) => entry.fabricIndex === fabric.fabricIndex)) {
        acl.push({
          fabricIndex: fabric.fabricIndex,
          privilege: import_access_control.AccessControl.AccessControlEntryPrivilege.Administer,
          authMode: import_access_control.AccessControl.AccessControlEntryAuthMode.Case,
          subjects: [fabric.rootNodeId],
          targets: null
          // entire node
        });
        logger.warn(
          "Added missing ACL entry for fabric",
          fabric.fabricIndex,
          "for Node ID",
          fabric.rootNodeId,
          ". This should only happen once after upgrading to matter.js 0.9.1"
        );
      }
    }
    if (acl.length > originalAclLength) {
      this.state.acl = acl;
    }
    logger.info("initializing ACL manager with ACL", acl);
    this.internal.aclManager = new import_protocol.AccessControlManager(
      acl,
      (aclList, aclEntry, subjectDesc, endpoint, clusterId) => this.extensionEntryAccessCheck(
        aclList,
        aclEntry,
        subjectDesc,
        endpoint,
        clusterId
      )
    );
    this.reactTo(this.events.acl$Changed, this.#updateAccessControlList);
  }
  #validateAccessControlListChanges(value) {
    const relevantFabricIndex = this.context.session?.associatedFabric.fabricIndex;
    if (relevantFabricIndex === void 0) {
      return;
    }
    const fabricAcls = value.filter((entry) => entry.fabricIndex === relevantFabricIndex);
    if (fabricAcls.length > this.state.accessControlEntriesPerFabric) {
      throw new import_types.StatusResponseError("AccessControlEntriesPerFabric exceeded", import_types.StatusCode.ResourceExhausted);
    }
    for (const entry of fabricAcls) {
      const { privilege, subjects, targets, authMode } = entry;
      if (subjects !== null && subjects.length > this.state.subjectsPerAccessControlEntry) {
        throw new import_types.StatusResponseError("SubjectsPerAccessControlEntry exceeded", import_types.StatusCode.ResourceExhausted);
      }
      if (targets !== null && targets.length > this.state.targetsPerAccessControlEntry) {
        throw new import_types.StatusResponseError("TargetsPerAccessControlEntry exceeded", import_types.StatusCode.ResourceExhausted);
      }
      if (authMode === import_access_control.AccessControl.AccessControlEntryAuthMode.Pase) {
        throw new import_types.StatusResponseError("AuthMode for ACL must not be PASE", import_types.StatusCode.ConstraintError);
      } else if (authMode === import_access_control.AccessControl.AccessControlEntryAuthMode.Case) {
        if (subjects !== null) {
          for (const subject of subjects) {
            if (import_types.NodeId.isCaseAuthenticatedTag(subject)) {
              const cat = import_types.NodeId.extractAsCaseAuthenticatedTag(subject);
              if (import_types.CaseAuthenticatedTag.getVersion(cat) === 0) {
                throw new import_types.StatusResponseError(
                  "CaseAuthenticatedTag version 0 is not allowed",
                  import_types.StatusCode.ConstraintError
                );
              }
            } else if (!import_types.NodeId.isOperationalNodeId(subject)) {
              throw new import_types.StatusResponseError(
                "Subject must be a valid OperationalNodeId or CaseAuthenticatedTag",
                import_types.StatusCode.ConstraintError
              );
            }
          }
        }
      } else if (authMode === import_access_control.AccessControl.AccessControlEntryAuthMode.Group) {
        if (privilege === import_access_control.AccessControl.AccessControlEntryPrivilege.Administer) {
          throw new import_types.StatusResponseError(
            "Group ACLs must not have Administer privilege",
            import_types.StatusCode.ConstraintError
          );
        }
        if (subjects !== null) {
          for (const subject of subjects) {
            if ((0, import_types.GroupId)(subject) === import_types.GroupId.UNSPECIFIED_GROUP_ID) {
              throw new import_types.StatusResponseError(
                "Subject must be a valid GroupId for Group ACLs",
                import_types.StatusCode.ConstraintError
              );
            }
          }
        }
      }
      if (targets !== null) {
        for (const target of targets) {
          const { cluster, endpoint, deviceType } = target;
          if (deviceType !== null && endpoint !== null) {
            throw new import_types.StatusResponseError(
              "DeviceType and Endpoint are mutually exclusive",
              import_types.StatusCode.ConstraintError
            );
          }
          if (cluster === null && endpoint === null && deviceType === null) {
            throw new import_types.StatusResponseError("At least one field must be present", import_types.StatusCode.ConstraintError);
          }
          if (cluster !== null && !import_types.ClusterId.isValid(cluster)) {
            throw new import_types.StatusResponseError("Cluster must be a valid ClusterId", import_types.StatusCode.ConstraintError);
          }
          if (endpoint !== null && !import_types.EndpointNumber.isValid(endpoint)) {
            throw new import_types.StatusResponseError(
              "Endpoint must be a valid OperationalNodeId",
              import_types.StatusCode.ConstraintError
            );
          }
          if (deviceType !== null && !import_types.DeviceTypeId.isValid(deviceType)) {
            throw new import_types.StatusResponseError(
              "DeviceType must be a valid DeviceType",
              import_types.StatusCode.ConstraintError
            );
          }
        }
      }
    }
  }
  #handleAccessControlListChange(value, oldValue) {
    if (this.internal.aclManager === void 0) {
      return;
    }
    const { session } = this.context;
    const relevantFabricIndex = session?.associatedFabric.fabricIndex;
    if (relevantFabricIndex === void 0 || this.events.accessControlEntryChanged === void 0) {
      return;
    }
    const adminPasscodeId = session === void 0 || session?.isPase ? 0 : null;
    const adminNodeId = adminPasscodeId === null ? session?.associatedFabric.rootNodeId : null;
    if (adminNodeId === void 0) {
      return;
    }
    const fabricAcls = value.filter((entry) => entry.fabricIndex === relevantFabricIndex);
    const oldFabricAcls = oldValue.filter((entry) => entry.fabricIndex === relevantFabricIndex);
    let i = 0;
    for (; i < fabricAcls.length; i++) {
      if (!(0, import_general.isDeepEqual)(fabricAcls[i], oldFabricAcls[i])) {
        const changeType = oldFabricAcls[i] === void 0 ? import_access_control.AccessControl.ChangeType.Added : fabricAcls[i] === void 0 ? import_access_control.AccessControl.ChangeType.Removed : import_access_control.AccessControl.ChangeType.Changed;
        this.events.accessControlEntryChanged.emit(
          {
            changeType,
            adminNodeId,
            adminPasscodeId,
            latestValue: (changeType === import_access_control.AccessControl.ChangeType.Removed ? oldFabricAcls[i] : fabricAcls[i]) ?? null,
            fabricIndex: relevantFabricIndex
          },
          this.context
        );
      }
    }
    if (oldFabricAcls.length > i) {
      for (let j = oldFabricAcls.length - 1; j >= i; j--) {
        this.events.accessControlEntryChanged.emit(
          {
            changeType: import_access_control.AccessControl.ChangeType.Removed,
            adminNodeId,
            adminPasscodeId,
            latestValue: oldValue[j],
            fabricIndex: relevantFabricIndex
          },
          this.context
        );
      }
    }
  }
  #validateAccessControlExtensionChanges(value) {
    const relevantFabricIndex = this.context.session?.associatedFabric.fabricIndex;
    if (relevantFabricIndex === void 0) {
      return;
    }
    const fabricExtensions = value.filter((entry) => entry.fabricIndex === relevantFabricIndex);
    if (fabricExtensions.length === 0) {
      return;
    }
    if (fabricExtensions.length > 1) {
      throw new import_types.StatusResponseError("Extension list must contain a single entry", import_types.StatusCode.ConstraintError);
    }
    this.extensionEntryValidator(fabricExtensions[0]);
  }
  #handleAccessControlExtensionChange(value, oldValue) {
    if (this.internal.aclManager === void 0) {
      return;
    }
    const { session } = this.context;
    const relevantFabricIndex = session?.associatedFabric.fabricIndex;
    if (relevantFabricIndex === void 0 || this.events.accessControlExtensionChanged === void 0) {
      return;
    }
    const adminPasscodeId = session === void 0 || session?.isPase ? 0 : null;
    const adminNodeId = adminPasscodeId === null ? session?.associatedFabric.rootNodeId : null;
    if (adminNodeId === void 0) {
      return;
    }
    const fabricExtensions = value.filter((entry) => entry.fabricIndex === relevantFabricIndex);
    const oldFabricExtensions = oldValue.filter((entry) => entry.fabricIndex === relevantFabricIndex);
    const changeType = fabricExtensions.length > oldFabricExtensions.length ? import_access_control.AccessControl.ChangeType.Added : fabricExtensions.length < oldFabricExtensions.length ? import_access_control.AccessControl.ChangeType.Removed : import_access_control.AccessControl.ChangeType.Changed;
    this.events.accessControlExtensionChanged.emit(
      {
        changeType,
        adminNodeId,
        adminPasscodeId,
        latestValue: (changeType === import_access_control.AccessControl.ChangeType.Removed ? oldFabricExtensions[0] : fabricExtensions[0]) ?? null,
        fabricIndex: relevantFabricIndex
      },
      this.context
    );
  }
  /**
   * Implements the access control check for the given context, location and endpoint and is called by the
   * InteractionServer. The method returns the list of granted Access privileges for the given context, location and
   * endpoint.
   */
  accessLevelsFor(context, location, endpoint) {
    if (location.cluster === void 0) {
      logger.warn("Access control check without cluster, returning View access level");
      return [import_model.AccessLevel.View];
    }
    if (context.session === void 0) {
      logger.warn("Access control check without session, returning View access level");
      return [import_model.AccessLevel.View];
    }
    if (endpoint === void 0) {
      logger.warn("Access control check without endpoint, returning View access level");
      return [import_model.AccessLevel.View];
    }
    return this.aclManager.getGrantedPrivileges(context.session, endpoint, location.cluster);
  }
  /**
   * This method allows to implement the validation of manufacturer specific ACL extensions when an extension entry is
   * added or changed. The default implementation checks whether the extension is a valid TLV and possible to decode.
   *
   * In case of an Error throws StatusResponseError.
   *
   * Override this method in your own behavior to implement custom validation.
   */
  extensionEntryValidator(extension) {
    const { data } = extension;
    if (data.length < 2 || data[0] !== import_types.TlvType.List || data[data.length - 1] !== import_types.TlvType.EndOfContainer) {
      throw new import_types.StatusResponseError("Extension must be a valid TLV", import_types.StatusCode.ConstraintError);
    }
    try {
      (0, import_types.TlvTaggedList)({}, true).decode(data);
    } catch (error) {
      logger.debug(`Extension TLV decoding failed:`, error);
      throw new import_types.StatusResponseError("Extension must be a valid TLV", import_types.StatusCode.ConstraintError);
    }
  }
  /**
   * This method allows to implement the validation of manufacturer specific ACL extensions when access control is
   * checked to decide if access is allowed or not.
   * The default implementation always returns true. Override this method in your own behavior to implement custom
   * validation.
   */
  extensionEntryAccessCheck(_aclList, _aclEntry, _subjectDesc, _endpoint, _clusterId) {
    return true;
  }
  /**
   * The AccessControlManager instance that is used to manage the ACL for this behavior.
   */
  get aclManager() {
    if (this.internal.aclManager === void 0) {
      throw new import_general.InternalError("ACL manager not initialized yet");
    }
    return this.internal.aclManager;
  }
  #updateAccessControlList(acl) {
    if (!this.aclUpdateDelayed) {
      logger.info("ACL updated, updating ACL manager", acl);
      this.aclManager.updateAccessControlList((0, import_general.deepCopy)(acl));
    } else {
      logger.info("ACL updated, but ACL manager update is delayed", acl);
      this.internal.delayedAclData = acl;
    }
  }
  /**
   * If set to true, the ACL will not be updated immediately when it changes, but only when the `aclUpdateDelayed`
   * property is set to false again.
   * This is a hack to prevent the ACL from updating while we are in the middle of a write transaction and will be
   * removed again once we somehow handle relevant sub transactions.
   */
  get aclUpdateDelayed() {
    return this.internal.aclUpdateDelayed;
  }
  /**
   * If set to true, the ACL will not be updated immediately when it changes, but only when the `aclUpdateDelayed`
   * property is set to false again.
   * This is a hack to prevent the ACL from updating while we are in the middle of a write transaction and will be
   * removed again once we somehow handle relevant sub transactions.
   */
  set aclUpdateDelayed(value) {
    logger.info("Setting ACL update delayed to", value);
    if (!value) {
      this.#updateDelayedAccessControlList();
    }
    this.internal.aclUpdateDelayed = value;
  }
  #updateDelayedAccessControlList() {
    if (this.internal.delayedAclData === void 0) {
      return;
    }
    const delayedData = (0, import_general.deepCopy)(this.internal.delayedAclData);
    this.internal.delayedAclData = void 0;
    logger.info("Updating ACL manager with ACL", delayedData);
    this.aclManager.updateAccessControlList(delayedData);
  }
}
((AccessControlServer2) => {
  class Internal {
    /** AccessControlManager instance that is used to manage the ACL checks for this device. */
    aclManager;
    /** If set to true ACL updates are delayed while in a write transaction. More details see getter/setter above. */
    aclUpdateDelayed = false;
    /** Latest delayed data of acl */
    delayedAclData;
  }
  AccessControlServer2.Internal = Internal;
})(AccessControlServer || (AccessControlServer = {}));
//# sourceMappingURL=AccessControlServer.js.map
