/**
 * @license
 * Copyright 2022-2023 Project CHIP Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { Subject } from "#action/server/Subject.js";
import { Diagnostic, InternalError, Logger, MatterFlowError } from "@matter/general";
import { AccessLevel } from "@matter/model";
import {
  CaseAuthenticatedTag,
  FabricIndex,
  NodeId,
  StatusCode,
  StatusResponseError
} from "@matter/types";
import { AccessControl } from "@matter/types/clusters/access-control";
const logger = Logger.get("FabricAccessControl");
const ImplicitDefaultPaseAclEntry = {
  fabricIndex: FabricIndex.NO_FABRIC,
  // not fabric-specific
  privilege: AccessLevel.Administer,
  authMode: AccessControl.AccessControlEntryAuthMode.Pase,
  subjects: [],
  targets: []
  // entire node
};
var AuthModeNone = /* @__PURE__ */ ((AuthModeNone2) => {
  AuthModeNone2[AuthModeNone2["None"] = 0] = "None";
  return AuthModeNone2;
})(AuthModeNone || {});
class AccessDeniedError extends StatusResponseError {
  constructor(message) {
    super(message ?? "Unauthorized", StatusCode.UnsupportedAccess);
  }
}
class FabricAccessControl {
  #fabricIndex;
  #aclList = [];
  #extensionEntryAccessCheck = () => true;
  constructor(fabric) {
    if (fabric === void 0) {
      this.#fabricIndex = FabricIndex.NO_FABRIC;
      this.#aclList.push(ImplicitDefaultPaseAclEntry);
    } else {
      this.#fabricIndex = fabric.fabricIndex;
    }
  }
  /**
   * Public method used to update the Access Control List on changes.
   */
  set aclList(aclList) {
    if (aclList.some(({ fabricIndex }) => fabricIndex !== this.#fabricIndex)) {
      throw new InternalError("ACL entries must match the fabric index of the manager");
    }
    this.#aclList = [...aclList];
    logger.info(
      "ACL List updated",
      this.#aclList.map((e) => Diagnostic.dict(e))
    );
  }
  set extensionEntryAccessCheck(func) {
    this.#extensionEntryAccessCheck = func;
  }
  /**
   * 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 at ${location.path}, returning View access level`);
      return [AccessLevel.View];
    }
    if (endpoint === void 0) {
      logger.warn(`Access control check without endpoint at ${location.path}, returning View access level`);
      return [AccessLevel.View];
    }
    return this.#getGrantedPrivileges(context, endpoint, location.cluster);
  }
  /**
   * Subjects must match exactly, or both are CAT with matching CAT ID and acceptable CAT version
   */
  #subjectMatches(aclSubject, isdSubject) {
    if (BigInt(aclSubject) === BigInt(isdSubject)) {
      return true;
    }
    const aclNode = NodeId(aclSubject);
    const isdNode = NodeId(isdSubject);
    if (!NodeId.isCaseAuthenticatedTag(aclNode) || !NodeId.isCaseAuthenticatedTag(isdNode)) {
      return false;
    }
    const aclSubjectCat = NodeId.extractAsCaseAuthenticatedTag(aclNode);
    const isdSubjectCat = NodeId.extractAsCaseAuthenticatedTag(isdNode);
    return CaseAuthenticatedTag.getIdentifyValue(aclSubjectCat) === CaseAuthenticatedTag.getIdentifyValue(isdSubjectCat) && CaseAuthenticatedTag.getVersion(isdSubjectCat) >= CaseAuthenticatedTag.getVersion(aclSubjectCat);
  }
  /**
   * Add the new privilege to the granted privileges set and also add any privileges subsumed by the new privilege.
   */
  #addGrantedPrivilege(grantedPrivileges, privilege) {
    grantedPrivileges.add(privilege);
    switch (privilege) {
      /**
       * Formally removed in Spec in 1.4.2 but SDK and tests still implement it
       * Remove when https://github.com/project-chip/connectedhomeip/issues/41840 is solved
       */
      case AccessLevel.ProxyView:
        grantedPrivileges.add(AccessLevel.View);
        break;
      case AccessLevel.Operate:
        grantedPrivileges.add(AccessLevel.View);
        break;
      case AccessLevel.Manage:
        grantedPrivileges.add(AccessLevel.Operate);
        grantedPrivileges.add(AccessLevel.View);
        break;
      case AccessLevel.Administer:
        grantedPrivileges.add(AccessLevel.Manage);
        grantedPrivileges.add(AccessLevel.Operate);
        grantedPrivileges.add(AccessLevel.View);
        break;
    }
  }
  /**
   * Determines the granted privileges for the given session, endpoint, and cluster ID and returns them.
   */
  #getGrantedPrivileges(context, endpoint, clusterId) {
    const endpointId = endpoint.id;
    const subjectDesc = this.#getIsdFromMessage(context);
    const grantedPrivileges = /* @__PURE__ */ new Set();
    if (subjectDesc.authMode === AccessControl.AccessControlEntryAuthMode.Pase && subjectDesc.isCommissioning) {
      this.#addGrantedPrivilege(grantedPrivileges, AccessLevel.Administer);
    }
    for (const aclEntry of this.#aclList) {
      if (grantedPrivileges.has(AccessLevel.Administer)) {
        break;
      }
      if (aclEntry.fabricIndex === FabricIndex.NO_FABRIC || aclEntry.fabricIndex !== subjectDesc.fabricIndex) {
        continue;
      }
      if (aclEntry.authMode !== subjectDesc.authMode) {
        continue;
      }
      if (aclEntry.subjects === null || aclEntry.subjects.length === 0) {
        if (aclEntry.authMode !== AccessControl.AccessControlEntryAuthMode.Case && aclEntry.authMode !== AccessControl.AccessControlEntryAuthMode.Group) {
          throw new MatterFlowError("ACL error: only CASE and Group auth can have empty subjects");
        }
      } else {
        let matchedSubject = false;
        subjectLoop: for (const aclSubject of aclEntry.subjects) {
          for (const isdSubject of subjectDesc.subjects) {
            if (this.#subjectMatches(aclSubject, isdSubject)) {
              matchedSubject = true;
              break subjectLoop;
            }
          }
        }
        if (!matchedSubject) {
          continue;
        }
      }
      if (aclEntry.targets === null || aclEntry.targets.length === 0) {
      } else {
        let matchedTarget = false;
        for (const {
          cluster: targetClusterId,
          endpoint: targetEndpointId,
          deviceType: targetDeviceType
        } of aclEntry.targets) {
          if (targetClusterId === null && targetEndpointId === null && targetDeviceType === null) {
            throw new MatterFlowError("ACL error: target cannot be empty");
          }
          if (targetEndpointId !== null && targetDeviceType !== null) {
            throw new MatterFlowError("ACL error: target cannot specify both endpoint and device type");
          }
          if (targetClusterId !== null && targetClusterId !== clusterId) {
            continue;
          }
          if (targetEndpointId !== null && targetEndpointId !== endpointId) {
            continue;
          }
          if (targetDeviceType !== null && !endpoint.deviceTypes.includes(targetDeviceType)) {
            continue;
          }
          matchedTarget = true;
          break;
        }
        if (!matchedTarget) {
          continue;
        }
      }
      if (!this.#extensionEntryAccessCheck(this.#aclList, aclEntry, subjectDesc, endpoint, clusterId)) {
        continue;
      }
      this.#addGrantedPrivilege(grantedPrivileges, aclEntry.privilege);
    }
    if (subjectDesc.authMode === AccessControl.AccessControlEntryAuthMode.Group && grantedPrivileges.has(AccessLevel.Administer)) {
      throw new MatterFlowError("ACL error: should never grant Administer privilege to a Group");
    }
    return [...grantedPrivileges];
  }
  /**
   * Determines the Incoming Subject Descriptor (ISD) from the given session.
   */
  #getIsdFromMessage(session) {
    const fabricIndex = session.fabric;
    const isd = {
      isCommissioning: false,
      authMode: 0 /* None */,
      subjects: new Array(),
      fabricIndex: FabricIndex.NO_FABRIC
    };
    const { subject } = session;
    if (subject === void 0) {
      throw new MatterFlowError("ACL error: ACL checks require an authorized subject");
    }
    if (subject.id === NodeId.UNSPECIFIED_NODE_ID) {
      isd.authMode = AccessControl.AccessControlEntryAuthMode.Pase;
      isd.isCommissioning = true;
      isd.subjects.push(NodeId.UNSPECIFIED_NODE_ID);
      if (fabricIndex !== void 0 && fabricIndex !== FabricIndex.NO_FABRIC) {
        isd.fabricIndex = fabricIndex;
      }
    } else if (Subject.isGroup(subject)) {
      if (fabricIndex === void 0 || fabricIndex === FabricIndex.NO_FABRIC) {
        throw new MatterFlowError("ACL error: fabric needs to be associated for group sessions");
      }
      if (subject.hasValidMapping) {
        isd.authMode = AccessControl.AccessControlEntryAuthMode.Group;
        isd.subjects.push(subject.id);
        isd.fabricIndex = fabricIndex;
      }
    } else if (Subject.isNode(subject)) {
      if (fabricIndex === void 0 || fabricIndex === FabricIndex.NO_FABRIC) {
        throw new MatterFlowError("ACL error: Case session must be associated with a fabric");
      }
      isd.authMode = AccessControl.AccessControlEntryAuthMode.Case;
      isd.subjects.push(subject.id);
      if (subject.catSubjects !== void 0) {
        isd.subjects.push(...subject.catSubjects);
      }
      isd.fabricIndex = fabricIndex;
    }
    return isd;
  }
}
export {
  AccessDeniedError,
  FabricAccessControl
};
//# sourceMappingURL=FabricAccessControl.js.map
