Разграничение доступа к GraphQL-операциям и данным#

Компонент DataSpace Core предоставляет функциональность разграничения доступа к GraphQL-операциям и данным.

Примечание

На протоколах, отличных от GraphQL (JSON-RPC, gRPC, REST), функциональность разграничения доступа не предоставляется.

Разграничение доступа к данным осуществляется при помощи следующих возможностей:

  1. Ограничение выполняемых GraphQL-операций заданным списком (далее список разрешенных к выполнению операций). Каждый элемент списка фиксирует имя и структуру (тело) одной разрешенной к выполнению операции GraphQL. Структура (тело) операции ограничивает типы и атрибуты типов, с которыми осуществляется взаимодействие (изменение и выборка в рамках mutation, выборка в рамках операции query). Как следствие, безымянные GraphQL-операции не разрешены к выполнению.

  2. Проверка доступа путем выполнения проверочных запросов (далее — CheckSelects) перед выполнением GraphQL-операции. Операция будет выполнена, только если все указанные для нее проверочные запросы вернут непустой результат. Проверочный запрос можно использовать, например, для проверки прав и значений переменных перед операциями mutation.

  3. Ограничение доступа к определенным данным путем наложения дополнительных условий фильтрации через переменную (далее — ParamAdditions). Дополнительное условие фильтрации связывается с переменной GraphQL-операции и применяется в месте использования указанной переменной в качестве значения атрибута cond. Дополнительные условия фильтрации через переменную позволяют ограничить выбираемые данные вне зависимости от пользовательского (переданного в переменной) условия фильтрации.

  4. Ограничение доступа к определенным данным путем наложения дополнительных условий фильтрации по пути к полю (далее — PathConditions). Дополнительное условие фильтрации связывается с полем, к которому применима фильтрация (имеет аргумент cond), и накладывает дополнительные ограничения в выборке. Дополнительные условия фильтрации через путь к полю позволяют ограничить выбираемые данные вне зависимости от пользовательского (переданного в переменной или заданного явно) условия фильтрации при его наличии.

  5. Проверка доступа путем выполнения проверочных запросов непосредственно перед фиксацией внесенных операцией изменений в БД (before commit transaction).

Для того чтобы воспользоваться функциональностью разграничения доступа, необходимо:

  1. Объявить порт, на котором будет применяться функциональность разграничения доступа при помощи настройки dataspace.security.jwt-only.port. Значение данной настройки может как совпадать, так и не совпадать со значением настройки server.port. Если значения не совпадают, то автоматически поднимается дополнительный порт приложения. Данный порт позволяет принимать и использовать JWT при разграничении доступа к GraphQL-операциям.

  2. Сформировать список разрешенных к исполнению GraphQL-операций с CheckSelects, ParamAdditions и PathConditions (далее — конфигурация разграничения доступа). Структура элементов списка описана ниже.

  3. Передать подготовленную на предыдущем шаге конфигурацию разграничения доступа в DataSpace Core (на этапе разработки — через специализированный API, на этапе сборки дистрибутива — через файл).

Способ передачи конфигурации определяется настройкой dataspace.security.permissions.source, принимающей одно из следующих значений:

  • db— в dataspace-core поднимается endpoint /security/permissions для управления конфигурацией (API приведено ниже).

  • file — конфигурация разграничения доступа загружается из файла конфигурации.

Структура конфигурации разграничения доступа не зависит от значения настройки dataspace.security.permissions.source. Перечень разрешенных GraphQL-операций описывается в формате JSON.

Примечание

Режим dataspace.security.permissions.source=db предназначен для разработки и не рекомендован к использованию в промышленной эксплуатации.

Конфигурация разграничения доступа#

Конфигурация разграничения доступа состоит из описания разрешенных к выполнению GraphQL-операций с CheckSelects, ParamAdditions и PathConditions.

Пример описания разрешенной GraphQL-операции с проверкой CheckSelects:

[
  {
    "name": "createProduct",
    "body": "mutation createProduct($someCode: String, $level: Int) {  packet {    createProduct(input: {id:\"prod1\", code: $someCode, level: $level}) {      id    }  }}",
    "allowEmptyChecks": false,
    "disableJwtVerification": false,
    "checkSelects": [
      {
        "typeName": "SubjectPrivileges",
        "conditionValue": "${Integer:level} < 10 && it.personnelNumber==${jwt:employeeID} && it.privilege=='createProduct'",
        "description": "Only a user with the appropriate permission can make this request.",
        "orderValue": "0",
        "beforeOperationDisable": "false",
        "beforeCommitEnable": "false"
      }
    ],
    "paramAdditions": []
  }
]

Пример описания разрешенной GraphQL-операции с ограничением ParamAdditions:

  [
    {
      "name": "searchProduct",
      "body": "query searchProduct($sort: [_SortCriterionSpecification!], $limit: Int, $offset: Int, $searchCond: String) {   searchProduct(cond: $searchCond, limit: $limit, offset: $offset, sort: $sort) {    count    elems {      code    }  }}",
      "allowEmptyChecks": true,
      "disableJwtVerification": false,
      "checkSelects": [],
      "paramAdditions": [
        {
          "paramName": "searchCond",
          "paramAddition": "it.level < 10 && entities{type=SubjectPrivileges, cond=it.personnelnumber==${jwt:employeeID} && it.privilege=='searchProduct'}.$exists"
        }
      ]
    }
  ]

Пример описания разрешенной GraphQL-операции с ограничением PathConditions:

  [
    {
      "name": "searchProduct",
      "body": "query searchProduct($sort: [_SortCriterionSpecification!], $limit: Int, $offset: Int, $searchCond: String) {   searchProduct(cond: $searchCond, limit: $limit, offset: $offset, sort: $sort) {    count    elems {      code    }  }}",
      "allowEmptyChecks": true,
      "disableJwtVerification": false,
      "checkSelects": [],
      "pathConditions": [
        {
          "path": "searchProduct",
          "cond": "it.level < 10 && entities{type=SubjectPrivileges, cond=it.personnelnumber==${jwt:employeeID} && it.privilege=='searchProduct'}.$exists"
        }
      ]
    }
  ]

Элементы описания разрешенной GraphQL-операций:

  • name — имя операции GraphQL (название, которое идет после query или mutation, обязательный атрибут);

  • body — текст GraphQL-операции (используется для сверки без учета пробельных символов и переносов строк, обязательный атрибут);

  • allowEmptyChecks — тип boolean — указывает явно, что для данной операции ограничения (CheckSelects) могут быть не заданы, в противном случае не позволяет выполнить операцию, пока не будут указаны проверочные условия CheckSelects (значение по умолчанию — «false», необязательный атрибут). Если коллекция CheckSelect пустая, то данный атрибут должен быть выставлен в «true»;

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

  • checkSelects — коллекция проверок, которые выполняются перед выполнением операции (необязательный атрибут);

    • typeName — базовый тип, на котором строится запрос к БД. Если поле отсутствует, то в качестве базового типа используется предопределенный системный тип SysRootSecurity (необязательный атрибут);

    • conditionValue — ограничивающее выборку условие (обязательный атрибут);

    • description — описание ограничения (необязательный атрибут);

    • orderValue — вспомогательное поле, позволяющее упорядочить и избежать перемешивания CheckSelects при выводе на интерфейсе (необязательный атрибут);

    • beforeOperationDisable — «true» отключает выполнение проверки до запроса, когда необходимо выполнить проверку только перед commit в БД (значение по умолчанию — false, необязательный атрибут);

    • beforeCommitEnable — «true» включает выполнение проверки непосредственно перед фиксацией внесенных операцией изменений в БД (before commit transaction) (значение по умолчанию — false, необязательный атрибут);

  • paramAdditions — дополнительно накладываемые условия фильтрации при использовании соответствующих переменных (данный подход устарел с появлением pathConditions) в качестве значений cond условий (необязательный атрибут):

    • paramName — имя параметра (обязательный атрибут);

    • paramAddition — условие, добавляемое в месте использования переменной в качестве значения cond (обязательный атрибут);

  • pathConditions — дополнительно накладываемые условия фильтрации по указанным путям в запросе (необязательный атрибут):

    • path — путь в GraphQL-запросе (обязательный атрибут);

    • cond — условие, накладываемое по указанному пути (обязательный атрибут).

Примечание

По умолчанию запросы интроспекции схемы не подлежат обработке безопасностью, т.е. разрешены. Безопасность на запросах интроспекции можно включить с помощью настройки dataspace.security.graphql.introspection.asoperation=true, после чего следует объявить конфигурацию безопасности для таких запросов (аналогично любым другим запросам).

Также имеется возможность указать единственную проверку, работающую аналогично CheckSelect и выполняемую перед запросами интроспекции. Для этого необходимо указать в качестве значения настройки dataspace.security.graphql.introspection.query.check-condition строковое выражение желаемой проверки. Данная проверка будет выполняться только на запросах интроспекции, т.е. в запросах, содержащих исключительно поля получения сведений о структуре схемы GraphQL (__schema, __type).

Примечание

ParamAdditions являются устаревшим подходом к наложению дополнительных ограничений выборки данных. Им на замену были введены PathConditions, с помощью которых можно указать дополнительное условие для конкретного поля из GraphQL запроса.

Примечание

Дополнительные условия ParamAdditions, PathConditions, а также CheckSelects применимы как к query-операциям, так и к mutation-операциям. Но выборку данных в mutation-операциях необходимо использовать с осторожностью. Ограничение выбираемых данных в mutation-операциях рекомендуется осуществлять только за счет фиксирования структуры операции.

Каждая проверка CheckSelects приводит к дополнительному обращению к БД. Для query-операций вместо проверок CheckSelects рекомендуется рассмотреть использование дополнительных условий PathConditions или ParamAdditions, применяемых при выполнении переданной операции, если это отвечает требованиям безопасности. В большинстве случаев CheckSelects может быть преобразован к PathConditions и ParamAdditions.

Провал проверки CheckSelects приводит к возврату сообщения об ошибке. Дополнительное ограничение PathConditions или ParamAdditions не приводит к возвращению ошибки, но влияет на объем выбираемых данных (вплоть до пустого результата).

Путь в PathConditions является полным путем от корня запроса (не включая имя запроса) до требуемого поля, также используются псевдонимы для поля при наличии. Если в запросе используется запрос со слиянием через merge-поле, то в качестве узла в пути используется имя соответствующего интерфейса, указанного во встроенном фрагменте. Пример запроса и путей PathConditions:

query pathConditionsExampleQuery {
  searchProduct {
    elems {
      id
      servicesAlias: services(cond: "it.code == 'goodCode'") {
        elems {
          id
          name
        }
      }
    }
  }
  merge {
    elems {
      ... on Product @mergeReqSpec(cond: "it.code $like 'product%'") {
        code
        services {
          elems {
            id
            name
          }
        }
      }
      ... on Document {
        name
      }
    }
  }
}

В примере выше пути:

  • searchProduct — путь к выбираемым продуктам;

  • searchProduct.elems.servicesAlias — путь к выбираемым услугам у выбранных продуктов (используется псевдоним для поля services);

  • merge.elems.Product — путь к выбираемым продуктам в запросе слияния merge;

  • merge.elems.Product.services — путь к выбираемым услугам у выбранных продуктов в запросе слияния merge;

  • merge.elems.Document — путь к выбираемым документам в запросе слияния merge.

Получение конфигурации разграничения доступа из файла#

Получение конфигурации разграничения доступа из файла включается настройкой dataspace.security.permissions.source=file.

Конфигурация разграничения доступа загружается из файла по пути, указанном в настройке dataspace.security.permissions.path относительно classPath DataSpace Core. Значение по умолчанию — securityConfig\graphql-operations.json. Для того чтобы файл попал в class path DataSpace Core, необходимо разместить файл security-operations.json в папке securityConfig рядом с файлом model.xml в проекте сборки DataSpace, тогда плагин генерации модели при сборке поместит файл в состав артефакта DataSpace Core.

img

Пример содержимого файла:

[
  {
    "name": "searchProduct",
    "body": "query searchProduct($sort: [_SortCriterionSpecification!], $limit: Int, $offset: Int, $searchCond: String) {   searchProduct(cond: $searchCond, limit: $limit, offset: $offset, sort: $sort) {    count    elems {      code    services { elems {    code    }}   }  }}",
    "paramAdditions": [
        {
          "paramName": "searchCond",
          "paramAddition": "it.level < 10 && entities{type=SubjectPrivileges, cond=it.personnelNumber==${jwt:employeeID} && it.privilege=='searchProduct'}.$exists"
        }
    ],
    "pathConditions": [
      {
        "path": "searchProduct.elems.services",
        "cond": "it.code $like '%target'"
      }
    ],
    "checkSelects": [],
    "allowEmptyChecks": true
  },
  {
    "name": "createProduct",
    "body": "mutation createProduct($someCode: String, $level: Int) {  packet {    createProduct(input: {id:\"prod1\", code: $someCode, level: $level}) {      id    }  }}",
    "checkSelects": [
      {
        "typeName": "SubjectPrivileges",
        "conditionValue": "${Integer:level} < 10 && it.personnelNumber==${jwt:employeeID} && it.privilege=='createProduct'",
        "description": "Only a user with the appropriate permission can make this request.",
        "orderValue": "0"
      }
    ]
  }
]

Получение конфигурации разграничения доступа из БД и ее загрузка через REST#

Включение загрузки разрешенных GraphQL-операций в БД через REST осуществляется следующими настройками:

  • dataspace.security.permissions.source=db — поднимает необходимые REST-контроллеры и устанавливает источником конфигурации БД.

  • server.port.security-api-access=true — разрешает на порту, заданном настройкой server.port, принятие запросов управления конфигурацией.

Внимание!

Порт dataspace.security.jwt-only.port считается публично доступным. В случае равенства значений настроек dataspace.security.jwt-only.port и server.port вызов REST API управления конфигурацией запрещается вне зависимости от любых других настроек. Для использования REST API управления конфигурацией необходимо, чтобы значения указанных настроек отличались.

Управление конфигурацией разграничения доступа в БД осуществляется через следующий REST API:

HTTP-метод

Путь

Параметры

Описание

POST

/security/permissions/operations-bulk/replaceAll

В теле запроса передается JSON-конфигурация разграничения доступа

Удаление всех текущих разрешенных GraphQL-операций с последующим созданием новых операций

POST

/security/permissions/operations-bulk/replace

В теле запроса передается JSON-конфигурация разграничения доступа

Добавление новых разрешенных GraphQL-операций с заменой при пересечении по имени

POST

/security/permissions/operations-bulk/deleteAll

Удаление из БД всех разрешенных GraphQL-операций

POST

/security/permissions/operations-bulk/merge

В теле запроса JSON [{name : String, body : String}]

Создание в БД тел новых разрешенных GraphQL-операций по имени или обновление тел существующих операций

POST

/security/permissions/operations-bulk/create

В теле запроса передается JSON-разграничения доступа

Добавление в БД коллекции новых разрешенных GraphQL-операций. В случае конфликта по имени выбрасывается исключение

POST

/security/permissions/operations

В теле запроса передается JSON одной операции из конфигурации разграничения доступа

Добавление в БД одной новой разрешенной GraphQL-операции. В случае конфликта по имени выбрасывается исключение

GET

/security/permissions/operations

name : String — шаблон имен операций (используется поиск по like), page: Integer, pageSize: Integer

Получение списка разрешенных GraphQL-операций

PUT

/security/permissions/operations/{operationName}

operationName : String — имя замещаемой операции, тело — конфигурация замещаемой операции (без имени)

Замещение разрешенной GraphQL-операции по имени

DELETE

/security/permissions/operations/{operationName}

operationName : String — имя удаляемой операции

Удаление разрешенной GraphQL-операции по имени

Все полученные для сохранения конфигурации разграничения доступа проходят следующую валидацию:

  • Корректность тела GraphQL-запроса.

  • CheckSelects — корректность условия.

  • ParamAdditions — корректность условия.

  • PathConditions — корректность условия.

  • Отсутствие PathConditions с одинаковым путем.

  • Отсутствие ParamAdditions с одинаковым путем.

  • Отсутствие PathConditions с некорректным путем.

  • Отсутствие ParamAdditions на необъявленные переменные.

  • Отсутствие неиспользуемых PathConditions.

  • Отсутствие PathConditions на поля, к которым неприменима фильтрация, т.е. не имеющие аргумента cond (для merge-запросов фильтрация всегда применима за счет наличия директивы @mergeReqSpec).

  • Отсутствие ParamAdditions на любые переменные, используемые в качестве аргумента, отличного от cond (при этом разрешается использование ParamAddition на таких переменных, если есть хотя бы одно использование в качестве cond).

Ограничения на длины полей конфигурации при размещении в БД#

Когда конфигурация разграничения доступа размещается в БД (dataspace.security.permissions.source=db) действуют следующие ограничения на длины полей:

  • name — 254 символа;

  • body — не ограничен;

  • checkSelects.typeName — 254 символа;

  • checkSelects.conditionValue — не ограничен;

  • checkSelects.description — 254 символа;

  • paramAdditions.paramName — 254 символа;

  • paramAdditions.paramAddition — 4000 символов;

  • pathConditions.path — не ограничен;

  • pathConditions.cond — не ограничен.

Передача и верификация JWT при обращении на порт с разграничением доступа#

При вызове GraphQL-операции через порт dataspace.security.jwt-only.port передача JWT (JSON Web Token) осуществляется в HTTP-заголовке Authorization (тип Bearer).

Данные, передаваемые в JWT, могут быть использованы в CheckSelects (поле conditionValue), ParamAdditions (поле paramAddition) и PathConditions (поле cond).

Передача JWT при обращении на порт dataspace.security.jwt-only.port не является обязательной. Необходимость наличия JWT для конкретной GraphQL-операции определяется значением атрибута disableJwtVerification конфигурации разграничения доступа:

  • Если значение — disableJwtVerification=true, то GraphQL-операция может быть выполнена без наличия JWT в HTTP-запросе. При этом ограничивающие и проверочные условия либо должны отсутствовать, либо в них не должно быть подстановок значений из JWT. В противном случае возникнет ошибка в runtime.

  • Если значение — disableJwtVerification=false, то GraphQL операция не может быть выполнена без наличия JWT в HTTP-запросе вне зависимости от наличия или отсутствия ограничивающих и проверочных условий.

Для валидации JWT необходимо указать способ получения JWKS (JSON Web Key). Для этого необходимо задать значение настройки dataspace.security.jwks.source. Допустимые значения: url, file, property, db. В случае значения, отличного от db, необходимо предоставить дополнительную информацию в настройке dataspace.security.jwks.value, соответствующую значению dataspace.security.jwks.source.

Значение dataspace.security.jwks.source

Значение dataspace.security.jwks.value

Пример значения dataspace.security.jwks.value

url

URL, по которому будет запрошен JWKS

http://localhost:8080/realms/some-realm/protocol/openid-connect/certs

file

Путь к файлу, в котором расположен JWKS, в classPath DataSpace Core. По умолчанию используется приведенное в примере значение. Для того чтобы файл попал в class path DataSpace Core, необходимо разместить файл jwks.json в папке securityConfig рядом с файлом model.xml в проекте сборки DataSpace, тогда плагин генерации модели при сборке поместит файл в состав артефакта DataSpace Core

securityConfig\jwks.json

property

Строковое представление JWKS

{ «keys»: […]}

db

JWKS заносится в БД через endpoint /security/jwks (сигнатура вызова описана ниже, см. примечание)

Значение отсутствует

Примечание

Endpoint /security/jwks недоступен при значениях dataspace.security.jwks.source отличных от db. Данный endpoint доступен на порту server.port. Данный endpoint недоступен на порту dataspace.security.jwt-only.port, как следствие он не будет доступен при равенстве значений настроек dataspace.security.jwt-only.port и server.port.

По умолчанию DataSpace Core не запустится, если указан dataspace.security.jwt-only.port, но не указан dataspace.security.jwks.source. Для явного отключения необходимости верификации JWT (например, при проведении локальных тестов) необходимо установить значение настройки dataspace.security.jwt.validation.disable=true. При данном значении настройки отключается любая верификация JWT по JWKS (в том числе, когда JWKS задан). Все JWT признаются валидными.

Внимание!

На промышленных и тестовых стендах не рекомендуется выставлять значение dataspace.security.jwt.validation.disable=true.

При верификации JWT проверяются поля exp (время истечения срока действия) и nbf (время, до которого JWT не должен обрабатываться).

В случае рассинхронизации времени между разными модулями, использующими один и тот же JWT, можно воспользоваться следующими настройками:

  • dataspace.security.jwt.expDelta — задает количество секунд, в течение которых JWT еще считается действующим после истечения времени exp.

  • dataspace.security.jwt.nbfDelta — задает количество секунд, в течение которых JWT уже считается действующим до наступления времени nbf.

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

При верификации JWT дополнительно могут быть проверены следующие поля (если заданы соответствующие настройки):

  • aud (идентификатор получателя, для которого предназначен JWT) (настройка dataspace.security.jwt.aud);

  • iss (идентификатор выдавшей стороны) (настройка dataspace.security.jwt.iss).

Примечание

Если dataspace.security.jwks.source=db, то значения для настроек dataspace.security.jwt.expDelta, dataspace.security.jwt.nbfDelta, dataspace.security.jwt.aud, dataspace.security.jwt.iss берутся так же из БД. Процесс сохранения значений настроек в БД описан ниже.

Сигнатура API установки JWKS в БД#

В случае использования БД в качестве источника JWKS (настройка dataspace.security.jwks.source=db) управление JWKS осуществляется через endpoint /security/jwks.

Для работы указанного endpoint необходимо наличие настройки server.port.security-api-access=true, также значение настройки server.port не должно совпадать со значением настройки dataspace.security.jwt-only.port.

Данный endpoint предоставляет следующий API:

HTTP-метод

Путь

Параметры

Описание

POST

/security/jwks

В теле запроса JWKS

Загрузка в БД JSON Web Key Sets

GET

/security/jwks

Получение сохраненного в БД JSON Web Key Sets

DELETE

/security/jwks

Удаление из БД сохраненного JSON Web Key Sets

Сигнатура API изменения настроек валидации JWT в БД#

В случае использования БД в качестве источника JWKS (при настройках dataspace.security.jwks.source=db, server.port.security-api-access=true, а также, если значение настройки server.port не совпадает со значением настройки dataspace.security.jwt-only.port) становятся доступны следующие endpoint:

HTTP-метод

Путь

Параметры

Описание

POST

/security/jwt/check/config

{«iss»:»», «aud»: «», «jwks»: «», «expDelta»: 0, «nbfDelta»: 0}

Сохранение параметров в БД. При вызове может быть передан любой набор из приведенных параметров. Если в качестве значения параметра указывается пустая строка, то значение параметра в БД обнуляется (выставляется «null»)

Примечание

Данный API предназначен для использования только при разработке и не должен использоваться в промышленной эксплуатации. При вызове API изменяются настройки только того экземпляра dataspace-core, который обработал вызов. В случае наличия нескольких реплик dataspace-core, работающих с одной схемой БД, для применения измененных параметров остальные экземпляры dataspace-core необходимо перезагрузить вручную.

Использование подстановки переменных в ограничивающих строковых условиях#

Подстановка значений из JWT#

Проверки и ограничения разрешенных GraphQL-операций включают строковые выражения DataSpace (поле conditionValue в CheckSelects, поле paramAddition в ParamAdditions и поле cond в PathCondition). В указанных строковых выражениях могут использоваться подстановки значений claim из JWT, переданного в заголовке Authorization запроса на порт dataspace.security.jwt-only.port. Также выполняется подстановка переменных GraphQL.

Примечание

Подстановки значений claim из JWT работают только в строковых выражения CheckSelects (поле conditionValue), ParamAdditions (поле paramAddition) и PathConditions (поле cond). В строковых выражениях внутри GraphQL-операции подстановка значений из JWT не производится.

Подставлять можно только примитивные значения и массивы примитивных значений.

Подстановка значения из JWT в общем случае имеет следующий вид: ${String[]:jwt:userdata.roles}, где:

  • String[] — тип значения из JWT, [] указывает на то, что значение является массивом примитивов, а не одиночным примитивом;

  • jwt — указывает на получение значения переменной из JWT;

  • userdata.roles — путь относительно полезной нагрузки JWT. Результатом пути может быть только примитивный элемент или коллекция примитивных элементов. В пути не допускаются промежуточные коллекционные объекты.

Пример строкового выражения с подстановкой значения из JWT: "'adminRole' $in ${String[]:jwt:userdata.roles}". Если значение переменной String, то тип может быть опущен: "'adminRole'==${jwt:userdata.mainRole}". Это аналогично "'adminRole'==${String:jwt:userdata.mainRole}". Тип String[] может быть сокращен до [].

Подстановка значения из переменных GraphQL-запроса#

Пример строкового выражения с подстановкой значения из GraphQL-переменной: "${String:productCode} $like '%somePostfix'". Тип String может быть также опущен, как и при подстановке из JWT.

В некоторых операциях, например, createMany, есть возможность передать в GraphQL-переменной массив объектов. Этот массив тоже можно использовать в строковых условиях. В этом случае строковое выражение с подстановкой массива из N объектов превращается в N условий с подстановкой примитивов, результаты проверки которых объединяются через логическую операцию И. При подстановке переменной-массива следует учесть:

  • в одном строковом выражении можно использовать не более одного массива объектов;

  • при этом один и тот же массив объектов можно использовать несколько раз;

  • результатом вычисления пути должен быть примитив.

Например, если для нижеприведенного запроса задать строковое выражение ${input.code}==${input.name}, это приведет к тому, что для каждого элемента в массиве input будет проверяться условие на равенство полей code и name:

mutation manyOnTopLevelSecurity(${'$'}input:[_CreateProductPartyInput!]!) {
    packet {
        cp: createManyProductParty(input: ${'$'}input)
    }
}

Алгоритм разграничения доступа к данным#

Алгоритм разграничения доступа к данным осуществляется следующим образом:

  1. Проверяется наличие заголовка Authorization (тип Bearer) с токеном доступа (JWT) во входящем запросе.

    • Если dataspace.security.jwt.validation.disable=true, то JWT не проверяется.

    • Если заголовок с JWT обнаружен и dataspace.security.jwt.validation.disable=false, то выполняется валидация JWT при помощи JWKS. Проверяется подпись JWT и время действия токена.

  2. Определяется имя GraphQL-операции.

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

  4. Проверяется совпадение hash переданной операции с hash эталонной операции (из найденной записи конфигурации). Если значения hash не совпадают, то возвращается соответствующая ошибка.

  5. Выполняются CheckSelect. Если все CheckSelect вернули непустой результат, то выполнение операции продолжается. В противном случае возвращается ошибка с информацией о том, что операция не прошла проверку безопасности.

  6. Проверяется наличие условий ParamAddition. При их наличии определяется место их применения по месту использованию соответствующих переменных в качестве значений cond параметров. При выполнении операции дополнительно накладываются условия ParamAddition, PathCondition к переданным через переменную условиям фильтрации данных. Условия добавляются с использованием оператора && следующим образом: (userCond) && (additional cond).

  7. Проверяется наличие условий PathCondition. При их наличии определяется место их применения по явно указанному пути path. При выполнении операции дополнительно накладываются условия PathCondition вне зависимости от того, чем является cond по данному пути: явно заданным условием, переменной или вообще отсутствует. Условия добавляются с использованием оператора && следующим образом: (userCond) && (additional cond).

  8. При наличии ParamAddition и PathCondition одновременно, условия добавляются с использованием оператора && следующим образом: (userCond) && (additional paramAddition cond) && (additional pathCondition cond).

Блок-схема алгоритма:

img

Взаимодействие backend-to-backend#

Взаимодействие backend-to-backend, когда оба backend находятся в защищенном контуре и являются доверенными, может быть осуществлено без разграничения доступа к операциям и данным, но без доступа к управлению конфигурацией разграничения доступа, когда конфигурация берется из БД.

Для осуществления такого взаимодействия поднимается дополнительный порт, путем задания значения настройки dataspace.security.unsecure.port. Значение указанной настройки может как совпадать, так и не совпадать со значением настройки server.port.

Примечание

При совпадении значений настроек dataspace.security.unsecure.port и server.port возможность управления конфигурацией разграничения доступа и JWKS отключается.

Если значения настроек dataspace.security.unsecure.port и server.port совпадают, то в приложении поднимается указанный порт.

Внимание!

На порту dataspace.security.unsecure.port не применяются правила разграничения доступа к GraphQL-операциям и данным. Разграничение же доступа в рамках поддержки мультитенантности применяется.

Настройки функциональности разграничения доступа к GraphQL-операциям и данным#

Настройка

Допустимые значения

Описание

dataspace.security.permissions.source

file, db

Указывает источник, из которого берется конфигурация разграничения доступа

dataspace.security.permissions.path

Имя файла или путь до файла относительно classPath dataspace-core

Содержит путь к файлу с конфигурацией разграничения доступа относительно classPath dataspace-core

dataspace.security.jwks.source

url, file, property, db

Указывает источник JWKS

dataspace.security.jwks.value

Зависит от настройки dataspace.security.jwks.source

В зависимости от настройки dataspace.security.jwks.source содержит URL, по которому достается JWKS, путь к файлу с JWKS или само значение JWKS. Для source==db данная настройка на задается

dataspace.security.jwt-only.port

Номер свободного порта, например, 7575

Задает номер порта, на котором действует разграничение доступа. Значение может как совпадать, так и не совпадать со значением настройки server.port. При совпадении с server.port REST по загрузке конфигурации разрешенных GraphQL и JWT будут недоступны (что неактуально при загрузке соответствующих данных из других источников)

server.port.security-api-access

true или false (по умолчанию — false)

Доступны ли REST API по загрузке конфигурации разграничения доступа и JWKS, если server.port != dataspace.security.jwt-only.port

dataspace.security.jwt.validation.disable

true или false (по умолчанию — false)

Определяет отключение валидации JWT. При значении «true» никакой проверки JWT не осуществляется, JWT автоматически признается валидным

dataspace.security.unsecure.port

Номер свободного порта, например, 7070

Задает номер порта, на котором не действует разграничение доступа к GraphQL операциям и данным, предназначенный для backend-to-backend взаимодействия. Значение может как совпадать, так и не совпадать со значением настройки server.port. При совпадении с server.port REST по загрузке конфигурации разрешенных GraphQL и JWT будут недоступны (что неактуально при загрузке соответствующих данных из других источников)

Субъекты доступа#

Определены три категории потребителей:

  1. Администратор системы (Admin) — пользователь, определяющий политики и правила безопасности в рамках системы разграничения прав. Основные задачи:

    • Управление внешней по отношению к DataSpace системой аутентификации и управления доступом, далее — IAM.

    • Конфигурирование системы разграничения доступа на стороне DataSpace (в режиме разработки/тестирования/отладки приложения):

      • Загрузка JWKS для проверки корректности JWT.

      • Формирование правил проверки GraphQL-операций на основании содержимого JWT и бизнес-правил конкретной реализации системы DataSpace.

  2. Пользователь системы (User) — потребитель основных функций DataSpace посредством GraphQL операций. Основные задачи:

    • Аутентификация в IAM для получения JWT и дальнейшего использования в обращении к DataSpace.

    • Передача GraphQL операций в систему DataSpace с использованием JWT, при успешной аутентификации в IAM.

  3. Backend-приложение (Backend App). Приложения, имеющие доступ к данным DataSpace на чтение/запись посредством GraphQL/Json RPC/gRPC API без дополнительного контроля доступа в DataSpace.

Объекты доступа#

В качестве объектов доступа выступают сами GraphQL-операции, формируемые потребителями, и данные. Администратор фиксирует набор возможных GraphQL-операций, определяя для каждой из них:

  • набор проверочных запросов с учетом содержимого JWT исходной GraphQL-операции;

  • условия дополнительной фильтрации набора данных, возвращаемых исходной GraphQL-операцией с учетом содержимого JWT.

Пример построения системы с разграничением доступа к GraphQL операциям и данным#

Вариант схемы работы системы с разграничением прав доступа:

img

На схеме:

  • server.port — определяет номер порта для работы пользователя с ролью «Администратор системы». Доступные точки доступа (endpoint’ы): /packet, /search, /graphql, /dictionaries, /security.

  • dataspace.security.jwt-only.port — определяет номер порта для работы пользователя с ролью «Пользователь системы». Доступные endpoint’ы: /graphql. При обработке GraphQL-запросов применяется механизм проверки и применения политик разграничения прав.

  • dataspace.security.unsecure.port — определят номер порта для работы пользователя с ролью «Backend-приложение». Доступные endpoint’ы: /packet, /search, /graphql, /dictionaries.

Пример разграничения доступа к GraphQL-операциям и данным#

Для иллюстрации правил и возможностей функциональности разграничения доступа рассмотрим ее использование на примере «Система учета заказов пользователями определенных видов товаров» (далее — «Система»).

Модель данных «Системы» и граф статусов для сущности «Заказ» приведены по ссылке model.xml.

Требования к разграничению доступа#

Примечание

Роли пользователей назначаются в системе аутентификации и управления доступом пользователей (IAM), у которой пользователь получает JWT с его ролью и идентификационными данными.

Для пользователей предусмотрены роли:

  • менеджер (роль manager в IAM);

  • клиент (роль customer в IAM);

  • супервайзер (роль supervisor в IAM).

Привилегии в «Системе» должны быть назначены в соответствии с таблицей:

Привилегия

Менеджер

Клиент

Супервайзер

Просматривать список товаров

Разрешено

Разрешено

Разрешено

Создавать/изменять типы товаров (GoodType)

Разрешено

Создавать заказ (Order)

Разрешено

Просматривать список заказов (Order)

все заказы в статусе „FIXED“

Только свои заказы в любом статусе (включая детали заказа)

все заказы в любом статусе

Наполнять заказ деталями (Detail), определяющими список товаров в рамках заказа

Только «открытые» заказы (в статусе „DRAFT“)

Иметь только один «открытый» заказ (в статусе „DRAFT“)

Разрешено

Удалять детали только своего «открытого» заказа

Разрешено

Переводить свои заказы в статус „FIXED“

Разрешено

Определять свои персональные данные (CustomerPersonalData)

Разрешено

Просматривать персональные данные клиентов

Разрешено

В этом примере в качестве идентификатора пользователя (Customer.id) используется его e-mail.

GraphQL-запросы#

Для реализации прикладной логики используются GraphQL-запросы, приведенные по ссылке (requests.graphql):

Перечень GraphQL-запросов:

Поименованный GraphQL-запрос

Описание

query getCustomerInfo($cond: String!)

Получить имя и адрес клиента

mutation addCustomerInfo($customerInput: _CreateCustomerInput!)

Создать клиента

query searchGoodType

Получить список типов товаров

mutation addGoodTypeInfo($goodTypeInput: _CreateGoodTypeInput!)

Добавить тип товара

query searchOrder($cond: String)

Получить список заказов

query searchAllOrder($cond: String)

Получить список всех заказов

mutation addOrderDetail($customerId: String!, $goodTypeId: String!)

Добавить товар в заказ

mutation fixOrder($orderId: ID!)

Перевести заказ в статус FIXED

mutation deleteDetail($detailId: ID!)

Удалить товар из заказа

Разрешения#

Разрешения для GraphQL-запросов, реализующие требования по разграничению доступа, представлены в файле: (permissions.json). Наиболее значимые из них:

  • addCustomerInfo: добавить/изменить информацию о пользователе может только сам пользователь, за это отвечает проверка checkSelects на равенство передаваемого идентификатора ${customerInput.id} полю JWT ${jwt:email}.

    {
      "name": "addCustomerInfo",
      "body": "mutation addCustomerInfo($customerInput: _CreateCustomerInput!) {\n  packet {\n    updateOrCreateCustomer(input: $customerInput) {\n      returning {\n        ...CustomerAttributes\n      }\n    }\n  }\n}\n    fragment CustomerAttributes on _E_Customer {\n  id\n  __typename\n  data {\n    firstName\n    lastName\n  }\n  defaultDeliveryAddress {\n    ...AddressAttributes\n  }\n}\n    fragment AddressAttributes on _G_DeliveryAddress {\n  countryIso\n  regionIso\n  city\n  street\n  flatNumber\n}",
      "allowEmptyChecks": false,
      "disableJwtVerification": false,
      "checkSelects": [
        {
          "conditionValue": "${customerInput.id} == ${jwt:email}",
          "orderValue": "0",
          "typeName": "",
          "description": "Only user can define his own personal data"
        }
      ]
    }
    
  • deleteOrderDetail: удалить деталь имеет право только сам собственник заказа, если заказ находится в статусе „DRAFT“. Следует обратить внимание на то, что в данном ограничении для поиска также указана сущность Detail, так как для проверки необходимо обратиться непосредственно к уже хранящимся в DataSpace данным:

    • найти деталь, которую хочет удалить пользователь it.$id == ${detailId};

    • проверить, что владельцем заказа является пользователь, инициировавший запрос: it.order.customer.entityId == ${jwt:email};

    • убедиться, что найденный заказ находится в статусе „DRAFT“: it.order.statusForCUSTOMER == 'DRAFT'.

    {
      "name": "deleteOrderDetail",
      "body": "mutation deleteOrderDetail($orderDetailId: ID!) {\n  packet {\n    deleteOrderDetail(id: $orderDetailId)\n  }\n}",
      "allowEmptyChecks": false,
      "disableJwtVerification": false,
      "checkSelects": [
        {
          "conditionValue": "it.$id == ${orderDetailId} && it.order.customer.entityId == ${jwt:email} && it.order.statusForVendor.code == 'DRAFT'",
          "orderValue": "0",
          "typeName": "OrderDetail",
          "description": "Only owner can delete detail of his order AND only when status of this order is DRAFT"
        }
      ]
    }
    
  • searchAllOrder: используется для отображения всех заказов для пользователей с ролями manager и supervisor. В отличие от supervisor, пользователи с ролью manager могут просматривать заказы только в состоянии „FIXED“. Данное ограничение реализуется дополнительным условием фильтрации paramAdditions : cond: 'supervisor' $in ${[]:jwt:realm_access.roles} || ('manager' $in ${[]:jwt:realm_access.roles} && it.statusForCUSTOMER.code == 'FIXED' ).

    {
      "name": "searchAllOrder",
      "body": "query searchAllOrder($cond: String) {\n  searchOrder(cond: $cond, sort: {crit: \"it.customer.entityId\", order: DESC}) {\n    elems {\n      customer {\n        entityId\n      }\n      ...OrderAttributes\n    }\n  }\n}\n    fragment OrderAttributes on _E_Order {\n  id\n  __typename\n  openOrderFlag\n  orderDate\n  comment\n  statusForVendor {\n    code\n  }\n  orderDetailList {\n    elems {\n      ...OrderDetailAttributes\n    }\n  }\n}\n    fragment OrderDetailAttributes on _E_OrderDetail {\n  id\n  __typename\n  goodType {\n    entity {\n      ...GoodTypeAttributes\n    }\n  }\n}\n    fragment GoodTypeAttributes on _E_GoodType {\n  id\n  __typename\n  name\n  descr\n  price\n  vendor {\n    entity {\n      id\n      __typename\n      name\n    }\n  }\n}",
      "allowEmptyChecks": false,
      "disableJwtVerification": false,
      "checkSelects": [
        {
          "conditionValue": "'supervisor' $in ${[]:jwt:realm_access.roles} || 'manager' $in ${[]:jwt:realm_access.roles}",
          "orderValue": "0",
          "typeName": "",
          "description": "Only manager or supervisor can see all orders"
        }
      ],
      "paramAdditions": [
        {
          "paramName": "cond",
          "paramAddition": "'supervisor' $in ${[]:jwt:realm_access.roles} || ('manager' $in ${[]:jwt:realm_access.roles} && it.statusForVendor.code == 'FIXED' )"
        }
      ]
    }
    
  • searchGoodType: список товаров разрешен для просмотра всем пользователям. Установлен флаг Разрешить без проверок (allowEmptyChecks).

    {
      "name": "searchGoodType",
      "body": "query searchGoodType {\n  searchGoodType {\n    elems {\n      ...GoodTypeAttributes\n    }\n  }\n}\n    fragment GoodTypeAttributes on _E_GoodType {\n  id\n  __typename\n  name\n  descr\n  price\n  vendor {\n    entity {\n      id\n      __typename\n      name\n    }\n  }\n}",
      "allowEmptyChecks": true,
      "disableJwtVerification": false
    }
    
  • searchOrder: используется для поиска заказов для пользователей с ролями customer. Дополнительно накладывается ограничение, что выбираются только заказы текущего пользователя. Данное ограничение реализуется дополнительным условием фильтрации pathConditions : {path: searchOrder, cond: it.customer.entityId == ${jwt:email}}.

    {
      "name": "searchOrder",
      "body": "query searchOrder($cond: String) {\n  searchOrder(cond: $cond, sort: {crit: \"it.orderDate\", order: DESC}) {\n    elems {\n      ...OrderAttributes\n    }\n  }\n}\n    fragment OrderAttributes on _E_Order {\n  id\n  __typename\n  openOrderFlag\n  orderDate\n  comment\n  statusForVendor {\n    code\n  }\n  orderDetailList {\n    elems {\n      ...OrderDetailAttributes\n    }\n  }\n}\n    fragment OrderDetailAttributes on _E_OrderDetail {\n  id\n  __typename\n  goodType {\n    entity {\n      ...GoodTypeAttributes\n    }\n  }\n}\n    fragment GoodTypeAttributes on _E_GoodType {\n  id\n  __typename\n  name\n  descr\n  price\n  vendor {\n    entity {\n      id\n      __typename\n      name\n    }\n  }\n}",
      "allowEmptyChecks": false,
      "disableJwtVerification": true,
      "checkSelects": [
        {
          "conditionValue": "'customer' $in ${[]:jwt:realm_access.roles}",
          "orderValue": "0",
          "typeName": "",
          "description": "Only customer can see his own orders"
        }
      ],
      "pathConditions": [
        {
          "path": "searchOrder",
          "cond": "it.customer.entityId == ${jwt:email}"
        }
      ]
    }