Перейти к концу метаданных
Переход к началу метаданных

Часто бывает полезно выявить неактивные пропуска, то есть пропуска, по которым давно не было никаких событий доступа. Для решения этой задачи в Платформе НЕЙРОСС реализован скрипт автоматизации, позволяет автоматически обнаружить неактивные пропуска и перенести их в отдельную папку, а также, при необходимости, — выполнить бессрочную приостановку пропуска. Приостановка позволяет изъять сведения о таких пропусках из контроллеров доступа.

Контролируются не все пропуска, а только пропуска выбранных владельцев.

Посредством настройки свойств скрипта автоматизации вы можете:

  • задать имя папки для перемещения пропусков;
  • определить, требуется ли автоматически выполнять изъятие пропусков, либо решение будет принимать оператор бюро пропусков;
  • задать период неактивности — временной интервал в днях, по истечении которого при отсутствии событий доступа пропуск считается неактивным;
  • указать, требуется ли учитывать факт изменения пропуска совместно с событием доступа — пропуск будет считаться неактивным, если за заданный период не было ни события доступа, ни факта изменения пропуска оператором бюро пропусков;
  • пометить, что пропуск приостановлен скриптом автоматизации.

Задание автоматизации может запускаться оператором вручную, по расписанию (например, раз в неделю по воскресеньям), либо по какому-либо событию/системному действию.

Пример: Найти пропуска, по которым не было событий доступа в течение 60 дней и переместить их в папку «Устаревшие пропуска»

Задание автоматизации запускается автоматически ежедневно в 23:00, производится поиск за интервал в 60 дней, пропуска не приостанавливаются, но помещаются в папку «Устаревшие пропуска», факт изменения пропуска не анализируется.

Настройки СКУД

Вам потребуется:

  1. Создать пользовательское свойство владельца пропуска для указания перечня лиц, для которых требуется проводить контроль активности пропуска.
  2. Добавить свойство на форму владельца пропуска и задать в значение true для владельцев, чьи пропуска требуется анализировать.
  3. Создать пользовательское свойство пропуска для пометок о том, что пропуск приостановлен скриптом.
  4. Добавить свойство на форму пропуска. 

Порядок действий:

ЗадачаКомментарий
Создание пользовательского свойства

В разделе Настройки СКУД АРМ НЕЙРОСС Доступ:

  1. На вкладке Свойства владельца пропуска добавьте новое свойство, задайте название и ключ свойства, укажите тип «Переключатель».
  2. На вкладке Свойства пропуска добавьте новое свойство, задайте название и ключ свойства, укажите тип «Переключатель».

Добавление пользовательских свойств

Изменение формы владельца пропуска

В разделе Персонал АРМ НЕЙРОСС Доступ выберите папку пропусков, для которой нужно настроить контроль активности, перейдите к вкладке Настройки папки:

  1. На вкладке Форма ввода: Владелец пропуска добавьте поле Контроль активности (название может быть любым) на форму владельца пропуска.
  2. На вкладке Форма ввода: Пропуск добавьте поле Приостановлен скриптом (название может быть любым) на форму пропуска.

Рекомендуем использовать Конструктор форм. При использовании нестандартных форм, обратитесь к специалистам компании ИТРИУМ за услугой по доработке формы.

Настройка форм ввода данных

При необходимости вывода информации в таблице пропусков, на вкладке Настройка таблицы добавьте вывод данных свойств.

Настройка таблицы пропусков

Включение необходимости контроля активности пропуска

Выберите владельцев, для которых требуется проводить контроль активности пропуска, и в режиме редактирования пропуска / группы пропусков в поле Контроль активности установите переключатель в положение Включено. Сохраните данные владельца.

Управление пропусками

Просмотр результатов работы скриптаДождитесь выполнения условия запуска задания (в нашем примере это 23:00 текущего дня).

Настройки автоматизации

Скопируйте приведённый ниже код скрипта и создайте файл с произвольным именем и расширением JSON, например: задание_автоматизации_Контроль_устаревших пропусков.json. Для этого удобно использовать простые текстовые редакторы типа Блокнот или Notepad++. Вы также можете обратиться к специалистам компании ИТРИУМ, мы вышлем подготовленный файл.

 Раскрыть | Код скрипта
    import play.api.libs.json.Format
    import beans.pacs.{PacsFolderBean, PassBean}
    import extensions.automation.scripts.AutomationActionScript
    import extensions.automation.signals.AutomationSignal
    import models.common.UserIdentity
    import models.neyross.{Pass, PassComponent}
    import models.pacs.{PacsFolder, PacsFolderComponent}
    import dto.pacs.PacsFolderMovementDto
    import services.logging.web.{LoggerWithWeb => Logger}
    import play.api.libs.json.{JsObject, Json}
    import proto.neyross.PassProto
    import services.common.{SystemLogService, SystemLogTag}
    import services.neyross.NeyrossEmbedApiService
    import slick.dbio.DBIOAction
    import utils.TableExtension
    import utils.bootstrap.CustomPostgresProfile
    import utils.common.ScalaUtils

    import java.time.OffsetDateTime
    import java.util.UUID
    import scala.concurrent.Future

    class ObsoletePassControlAutomationScript extends AutomationActionScript {

      val logger = Logger("ObsoletePassControlAutomationScript")

      private val postgresProfile = ctx.dbConfig.profile.asInstanceOf[CustomPostgresProfile]

      import postgresProfile.api._

      implicit val ec = ctx.executionContext
      private val neyrossEmbedApi = ctx.injector.instanceOf[NeyrossEmbedApiService]
      private val pacsFolderBean = ctx.injector.instanceOf[PacsFolderBean]
      private val systemLogService = ctx.injector.instanceOf[SystemLogService]
      private val passBean = ctx.injector.instanceOf[PassBean]

      private val folders = new PacsFolderComponent()(ctx.dbConfig)
      private val passes = new PassComponent()(ctx.dbConfig, neyrossEmbedApi, ec)

      // @parameter { "type": "string", "title": "Название папки для устаревших пропусков", "key": "workingFolder" }
      val workingFolder = "Устаревшие пропуска"

      // @parameter { "type": "string", "title": "Идентификатор свойства-признака контроля", "key": "shouldControlOutdateProperty", "description": "Контролироваться будут пропуска только тех владельцев, которые имеют данное свойство" }
      val shouldControlOutdateProperty = "inactivity"

      // @parameter { "type": "string", "title": "Значение свойства-признака контроля", "key": "shouldControlOutdatePropertyValue", "description": "Контролироваться будут пропуска только тех владельцев, которые имеют указанное значение данного свойства (true означает Да)" }
      val shouldControlOutdatePropertyValue = "true"

      // @parameter { "type": "select", "title": "Режим работы", "key": "mode", "options": [ { "value": "default", "label": "Перемещение устаревших пропусков в папку" }, { "value": "withStopping", "label": "Приостановка и перемещение устаревших пропусков в папку" } ] }
      val mode = "withStopping"

      // @parameter { "type": "number", "title": "Максимальный период неактивности, дней", "key": "inactivityPeriod" }
      val inactivityPeriod = 1

      // @parameter { "type": "boolean", "title": "Анализировать время изменения пропуска", "key": "isUpdateTimeAnalysisEnabled", "description": "Если флаг установлен, при расчёте активности пропуска будут учитываться также события изменения пропуска оператором бюро пропусков. В противном случае будет анализироваться только время последнего прохода по данному пропуску" }
      val isUpdateTimeAnalysisEnabled = false

      // @parameter { "type": "string", "title": "Идентификатор свойства-маркера контроля", "key": "scriptMarkPropertyKey", "description": "При автоматической приостановке пропуска к нему будет добавлено свойство с данным идентификатором и значением true (Да). Вы можете вынести данное свойство на форму пропуска или в таблицу пропусков, при необходимости" }
      val scriptMarkPropertyKey = "PausedByScrypt"

      val modeLocalization: Map[String, String] =
        Map(
          "default" -> "Перемещение устаревших пропусков в папку",
          "withStopping" -> "Приостановка и перемещение устаревших пропусков в папку"
        )

      logger.debug(s"mode: ${modeLocalization.getOrElse(mode, "")}")
      logger.debug(s"modification time analysing: $isUpdateTimeAnalysisEnabled")
      logger.debug(s"inactivity period: $inactivityPeriod")

      override def onSignal(signal: AutomationSignal): Future[Unit] = {
        workingFolder.trim match {
          case "" =>
            logger.warn("working folder name is empty. do nothing")
            Future.unit
          case nonEmptyFolderName =>
            runWithNonEmptyFolderName(nonEmptyFolderName)
        }
      }

      private def createFolderWithNameAndParent(name: String, parent: Option[String]) = {
        val uuid = UUID.randomUUID().toString
        val folder = PacsFolder(
          id = None,
          uuid = Some(uuid),
          name = Some(name),
          parent = parent,
          isDefault = false,
          templates = None,
          defaultValues = None,
          settings = None,
          created = OffsetDateTime.now(),
          deleted = false
        )
        folders.insert(folder).map(_ => Some(uuid))
      }

      private def getFolderUuidByName(folderName: String) = {
        folders.folders
          .filter(f => f.name === folderName && !f.deleted)
          .result
          .headOption
          .flatMap({
            case Some(folder) =>
              DBIOAction.successful(folder.uuid)
            case _ =>
              folders
                .findDefault()
                .flatMap({
                  case Some(defaultFolder) if defaultFolder.uuid.nonEmpty =>
                    createFolderWithNameAndParent(folderName, defaultFolder.uuid)
                  case _ =>
                    logger.warn("there is no valid default folder. wtf?")
                    DBIOAction.successful(None)
                })
          })
      }

      private def isOutdated(timestamp: OffsetDateTime): Boolean = {
        val threshold = OffsetDateTime.now().minusDays(inactivityPeriod)
        logger.trace(s"checking if chosen timestamp $timestamp is older than threshold $threshold")
        threshold.isAfter(timestamp)
      }

      private def isStoppedOrInOutdatedFolder(
                                               pass: Pass,
                                               outdatedFolderUuid: String,
                                               passFolders: List[String]
                                             ): Boolean = {
        pass.getState.contains(PassProto.Pass.State.sSTOPPED) || passFolders.contains(outdatedFolderUuid)
      }

      private def isOutdatedPassRecord(record: PassRecord): Boolean = {
        // from all pass times, choose latest one and check if it outdated
        val lastAccessGrantedAtOption = record._2
        val created = record._3
        val lastUpdatedAtOption = record._4
        logger.trace(s"last access granted: $lastAccessGrantedAtOption, created: $created, last updated: $lastUpdatedAtOption")
        // in any cases, pass has "created" time
        var latestTimeToCheck: OffsetDateTime = created
        if (
          lastAccessGrantedAtOption.isDefined &&
            lastAccessGrantedAtOption.get.isAfter(latestTimeToCheck)
        ) {
          // last access event was later than created time - using it
          latestTimeToCheck = lastAccessGrantedAtOption.get
        }
        if (isUpdateTimeAnalysisEnabled) {
          if (
            lastUpdatedAtOption.isDefined &&
              lastUpdatedAtOption.get.isAfter(latestTimeToCheck)
          ) {
            // last update event was later than created time and last access time - using it
            latestTimeToCheck = lastUpdatedAtOption.get
          }
        }
        isOutdated(latestTimeToCheck)
      }

      private def getPassesToMove(records: Seq[PassRecord], outdatedFolderUuid: String): Seq[Pass] = {
        records
          .filter(record => {
            logger.trace(s"pass ${record._5.uuid} handling")
            !isStoppedOrInOutdatedFolder(record._5, outdatedFolderUuid, record._1.getOrElse(Nil)) &&
              isOutdatedPassRecord(record)
          })
          .map(_._5)
      }

      type PassFolders = Option[List[String]]
      type LastAccessGrantedAt = Option[OffsetDateTime]
      type Created = OffsetDateTime
      type LastUpdatedAt = Option[OffsetDateTime]
      type PassRecord = (PassFolders, LastAccessGrantedAt, Created, LastUpdatedAt, Pass)

      type PersonUUID = String
      type PersonFolders = Option[Seq[String]]
      type PersonRecord = (PersonUUID, PersonFolders)

      private def personToControlHandler(
                                          outdatedFolderUuid: String,
                                          personRecord: PersonRecord
                                        ): Future[Seq[String]] = {
        val dbAction =
          passes
            .extendedPasses4(
              TableExtension.folders(ctx.dbConfig),
              TableExtension.lastAccessGrantedAt(ctx.dbConfig),
              TableExtension.created(ctx.dbConfig),
              TableExtension.lastUpdatedAt(ctx.dbConfig)
            )
            .filter(r => r.person === personRecord._1 && !r.deleted)
            .result

        ctx
          .dbRun(dbAction)
          .flatMap(result => {
            logger.trace(s"found passes of person ${personRecord._1}: ${result.map(_._5.uuid)}")
            val passesToMove = getPassesToMove(result, outdatedFolderUuid)
            logger.trace(s"found outdated passes of person ${personRecord._1}: ${passesToMove.map(_.uuid)}")
            if (passesToMove.nonEmpty) {
              val isPersonInOutdatedFolder = personRecord._2.exists(_.contains(outdatedFolderUuid))
              val personsToMove = Option.when(!isPersonInOutdatedFolder)(personRecord._1).toList
              val dto = PacsFolderMovementDto(personsToMove, passesToMove.map(_.uuid))
              pacsFolderBean.copyToFolder(outdatedFolderUuid, dto)(ui = null, None).map(_ => passesToMove.map(_.uuid))
            } else {
              Future.successful(Nil)
            }
          })
      }

      private def getPersonsToControl: Future[Vector[PersonRecord]] = {
        val personsToControl =
          sql"""
    SELECT DISTINCT uuid, folders
      FROM
          neyross_person,
          jsonb_to_recordset((person_json->>'properties'):: jsonb) AS props(key text, value text)
      WHERE
          key = '#${shouldControlOutdateProperty}' AND
          value = '#${shouldControlOutdatePropertyValue}' AND
          neyross_person.deleted = false
     """.as[PersonRecord]

        ctx.dbRun(personsToControl)
      }

      case class Property(key: String, value: String, index: Option[Long] = None)

      object Property {
        implicit val jsonFormat: Format[Property] = Json.format[Property]
      }

      private def stopPassesWithUuid(uuids: Seq[String]): Future[Unit] = {
        ctx
          .dbRun(passes.findByUuids(uuids))
          .flatMap(updatedPasses => {
            ScalaUtils
              .sequentialTraverse(updatedPasses)(updatedPass => {
                logger.trace(s"stopping pass ${updatedPass.uuid}; setting property $scriptMarkPropertyKey and disabled_from fields")
                val passJson = updatedPass.toFullJson.asOpt[JsObject].get
                val passProperties = (passJson \ "properties").asOpt[List[Property]].getOrElse(Nil)

                val newPassProperties = passProperties.filter(p => {
                  p.key != scriptMarkPropertyKey
                }) :+ Property(
                  scriptMarkPropertyKey,
                  "true"
                )
                val newPassJson = passJson ++ Json.obj(
                  "disabled_from" -> OffsetDateTime.now().toInstant.getEpochSecond.toInt,
                  "properties" -> Json.toJson(newPassProperties)
                )
                logger.trace(s"new pass json is: $newPassJson")
                passBean
                  .updateV2(updatedPass.uuid, newPassJson)(UserIdentity.getFakeUserIdentityFor("Автоматизация"), None)
              })
              .map(_ => ())
          })
      }

      private def runWithNonEmptyFolderName(folderName: String): Future[Unit] = {
        systemLogService
          .log(
            SystemLogTag.NORMAL,
            SystemLogTag.AUTOMATION
          )(s"Запущен процесс контроля устаревших пропусков")
          .map(_ => ())
        ctx
          .dbRun(getFolderUuidByName(folderName))
          .flatMap({
            case Some(folderUUID) =>
              logger.debug(s"working folder is $folderName ($folderUUID)")
              getPersonsToControl
                .flatMap(persons => {
                  logger.debug(s"found persons to control: $persons")
                  ScalaUtils.sequentialTraverse(persons)(personToControlHandler(folderUUID, _))
                })
                .flatMap(uuids => {
                  val flatUuidsList = uuids.flatten.distinct
                  if (flatUuidsList.nonEmpty) {
                    val stopping = if (mode == "withStopping") {
                      stopPassesWithUuid(flatUuidsList)
                    } else {
                      Future.unit
                    }
                    stopping.flatMap(_ => {
                      val yesOrNo = Option
                        .when(isUpdateTimeAnalysisEnabled)("да")
                        .getOrElse("нет")
                      logger.debug(s"were moved ${flatUuidsList.size} passes")
                      val message =
                        s"""В папку '$folderName' было перемещено ${flatUuidsList.size} пропусков.
                           |Режим работы: ${modeLocalization.getOrElse(mode, "")}.
                           |Анализировать время изменения пропуска: $yesOrNo
                           |Максимальный период неактивности: $inactivityPeriod дней""".stripMargin
                      systemLogService
                        .log(
                          SystemLogTag.NORMAL,
                          SystemLogTag.AUTOMATION
                        )(
                          message = message,
                          data = Some(Json.toJson(flatUuidsList))
                        )
                        .map(_ => ())
                    })
                  } else {
                    logger.debug("not found persons with obsolete persons or passes")
                    systemLogService
                      .log(
                        SystemLogTag.NORMAL,
                        SystemLogTag.AUTOMATION
                      )(s"Не найдено владельцев для контроля устаревших пропусков, либо все пропуска таких владельцев не являются устаревшими")
                      .map(_ => ())
                  }
                })
            case _ =>
              Future.unit
          })
        Future.unit
      }
    }

    new ObsoletePassControlAutomationScript

Добавьте задание автоматизации

  1. В разделе Автоматизация нажмите на кнопку  Добавить новое задание , нажмите на кнопку Импорт и укажите путь к подготовленному на предыдущем этапе файлу [Автоматизация].
  2. В блоке Параметры сигнала выберите По расписанию, настройте расписание (например, 0 23 * * * для ежедневного запуска, или 0 23 * * 1 для запуска раз в неделю по воскресеньям). 

  3. В блоке Параметры действия настройте параметры задания автоматизации согласно таблице ниже.

    ПараметрКомментарий
    Название папки устаревших пропусковНазвание папки. При запуске скрипта проверяется наличие папки с указанным именем в папке Все. При отсутствии, — она будет создана. В эту папку будут помещаться все найденные пропуска и их владельцы. Если название пустое, задание не выполняется. 
    Идентификатор свойства — признака контроляУникальный идентификатор пользовательского свойства владельца пропуска, созданный на этапе Настройки СКУД. В нашем примере: inactivity
    Значение свойства — признака контроля
    • true — если требуется проверять пропуска, для владельцев которого в поле Контроль пропусков переключатель установлен в положение  Включено.
    • false — если требуется проверять пропуска, для владельцев которого в поле Контроль пропусков переключатель установлен в положение  Выключено.

    В нашем примере: true

    Режим работы

    Выберите из раскрывающегося списка требуемые действия с пропусками:  требуется ли просто помещать пропуска в отдельную папку для принятия решения оператором о дальнейших действиях с пропуском, либо необходимо приостанавливать действие найденных пропусков и помещать в папку.

    • Перемещение устаревших пропусков в папку — просто помещать пропуска в отдельную папку для принятия решения оператором о дальнейших действиях с пропуском. Пропуск остаётся действительным. Доступ по нему возможен.
    • Приостановка и перемещение устаревших пропусков в папку — приостанавливать и помещать в отдельную папку. Пропуск не удаляется из контроллеров доступа, но доступ блокируется.
    Максимальный период неактивности, днейУкажите временной интервал в днях (количество дней), по истечению которого пропуск без событий доступа считается устаревшим.
    Анализировать время изменения пропускаУстановите флаг в поле, если, помимо событий доступа при расчёте активности пропуска требуется учитывать не только события доступа, но и события изменения пропуска оператором бюро пропусков. 
    Идентификатор свойства-маркера контроляУникальный идентификатор пользовательского свойства пропуска, значение которого будет устанавливаться в true для пропусков, которые приостановлены данным скриптом. В нашем примере: PausedByScrypt

    Алгоритм работы

    1. Поиск владельцев, для которых в поле с указанным идентификатором задано указанное значение.
    2. Поиск активных пропусков для найденных владельцев (если пропуск уже приостановлен или находится в папке устаревших пропусков операции с ним не выполняются).
    3. Поиск времени «активизации пропуска»:
      1. Если флаг в поле «Анализировать время изменения пропуска» установлен, то выбирается наиболее позднее из времени последнего редактирования и времени последнего прохода по пропуску
      2. Если флаг не установлен, выбирается время последнего прохода по пропуску.
    4. Если «время активизации» раньше чем текущее время минус «Максимальный период неактивности», пропуск помещается в папку устаревших пропусков.
    5. Если указан режим работы «Приостановка и перемещение устаревших пропусков в папку», все найденные пропуска бессрочно приостанавливаются. В поле «Приостановлен с» задаётся текущее время.
  4. Нажмите на кнопку Сохранить задание.

Запуск задания

Дождитесь выполнения условия запуска задания или запустите задание вручную. Для ручного запуска в поле Тип сигнала выберите значение По HTTP-запросу, сохраните задание и нажмите отправить запрос.

Вы можете отслеживать процедуру инициализации и выполнения задания в Журнале аудита [Журнал аудита].

Запуск и выполненные скриптом действия фиксируются в системном журнале [Системный журнал].

Если в поле Режим работы задана необходимость приостановки пропуска. В системном журнале отражается факт блокировки действия на контроллерах и задания срока приостановки с текущего времени:

Пропуска приостанавливаются с текущего времени, свойство Приостановлен скриптом у приостановленных пропусков устанавливается в значение Да (true).

  • Нет меток