Селекторы и запросы
Глава 8 показала, как show, hide, group by и style выбирают срезы модели. Каждая из этих клауз принимала аргумент одного и того же вида — голое имя, атом аспекта, шаблон ребра, соединённые через and/or/not/in — не останавливаясь, чтобы объяснить, что это за аргумент такое. Это селектор: один небольшой язык запросов, дословно общий для проекций, для политик (forbid, require) и для именованных объявлений query, которые можно написать один раз и переиспользовать везде.
query PciServices: service and @@security.zone:"PCI"
policy PciIsolation { "PCI workloads never talk to the public zone directly" forbid PciServices > @@security.zone:"Public"}
view PciBoard { "PCI-scoped services, violations flagged" show PciServices style violating PciIsolation { color: crimson }}Один запрос — PciServices — питает и forbid политики, и show проекции; находки политики питают style violating проекции. Одна алгебра, три потребителя. В этом и суть главы.
На этих трёх идеях держится всё остальное:
- Одна алгебра, два сорта. Селектор именует либо множество узлов (модули, интерфейсы, процессы, пространства), либо множество рёбер (вызовы и инвокации
do, которые выводят процессы). Какой именно сорт — решается по одному только тексту: наличие стрелки делает селектор рёберного сорта, и ничто другое этого не делает. - Сигилы отмечают ось. Голое имя — это ссылка на элемент или тип;
@pathчитает поле или встроенный геттер;@@keyчитает ось аспекта;$nameчитает ручку;*значит «всё». Что означает фрагмент, понятно без знания того, где он стоит. - Селекторы и выражения встречаются только в порталах.
where (…),exists (…),count (…),sum @g of (…)— порталы: конструкции в скобках, вводимые ключевым словом. Внутри них грамматика начинается заново:*— это «всё» в селекторе и умножение в выражении. Два языка никогда не смешиваются в середине терма.
Одна алгебра, два сорта
Заголовок раздела «Одна алгебра, два сорта»Каждый селектор — либо узлового сорта (он обозначает элементы), либо рёберного сорта (он обозначает производные рёбра). Правило чисто текстовое: фрагмент, содержащий стрелку (>, >>, <>), — рёберного сорта; всё остальное — узлового.
query PciTeam: service and @@security.zone:"PCI" // узловой сорт — нет стрелки, множество элементовquery PciEgress: @@security.zone:"PCI" > * // рёберный сорт — решает стрелкаand/or требуют, чтобы обе стороны были одного сорта — and, смешивающий узловой терм с рёберным, это диагностика, а не молчаливая переинтерпретация. Приоритет, от самого слабого к самому сильному: or < and < not < голый терм (атом, стрелка, извлечение). Скобки переопределяют порядок:
view PrecedenceDemo { show service and in Payments or database // читается как (service and in Payments) or database hide not @@team // дополнение — всё без команды}hide not @@team допустим сам по себе — селектор, целиком построенный из предикатов (not, in, on, where), узлового сорта над неявным универсумом всего, что есть в области видимости.
Атомы узлов
Заголовок раздела «Атомы узлов»Строительные блоки селектора узлового сорта:
| Атом | Значение |
|---|---|
* | всё, что есть в области видимости |
service (имя типа) | экземпляры этого типа — включая подтипы |
module interface surface process subprocess space | каждый элемент этого встроенного вида, любого типа |
Payments (ссылка на элемент) | этот конкретный элемент |
@@key | всё, что несёт этот ключ аспекта, с любым значением |
@@key:"value" / @@key:Place | носители этого значения или этого материализованного места |
$name | ручка вида «элемент» |
this | связанный субъект — только внутри портала или обязательства require |
outside | элементы, не выбранные в этой проекции — только для сторон ребра в hide/style |
violating <Policy> | активные находки этой политики — сорт следует за политикой |
Две строки выглядят похоже, но значат разное: имя типа (service или пользовательский тип вроде pci_service) совпадает с экземплярами этого типа и его подтипов; ключевое слово вида (module, process, …) совпадает с каждым элементом этого структурного вида, независимо от типа. service и process — равноправные строки таблицы, но отвечают на разные вопросы: «какой тип» против «какая форма».
export type service pci_service { "A service in PCI scope."}
query PciByType: pci_service // совпадает с экземплярами pci_service — и любыми ЕГО подтипами тоже@@ в селекторе всегда означает ось аспекта — никогда не aspect, которое остаётся только для объявлений. Атом аспекта со значением слитный: между ключом, : и значением нет пробела:
view ZoneScope { "One security zone, picked at open time" knob zone from @@security.zone show @@security.zone:$zone}violating <Policy> выбирает только активные находки политики — исключённые находки живут в отчёте по исключениям, а не на доске:
policy PciIsolation { forbid @@security.zone:"PCI" > @@security.zone:"Public"}
query PciFindings: violating PciIsolation // здесь рёберный сорт — PciIsolation это forbid над стрелкойКомбинаторы и предикаты
Заголовок раздела «Комбинаторы и предикаты»Термы соединяются только явными and/or; not ставится перед любым термом. Наряду с атомами, три предиката фильтруют конъюнкцию — у них нет собственного сорта, поэтому они принимают тот сорт, что и остальная часть конъюнкции:
| Предикат | Значение (для узлового сорта) |
|---|---|
in <Module | Space> | структурный потомок или участник этого пространства |
in <Process> / in this | участвует в этом процессе |
on <plane> | присутствует на этой плоскости |
where ( <expr> ) | предикат-выражение — см. Выражения ниже |
query PaymentsInternals: in Payments // всё, что структурно находится внутри Paymentsquery DeploymentTargets: service and on deployment // сервисы, присутствующие на плоскости deploymentquery BigFanIn: where (@connections > 20) // предикат-выражение, сам по себеin this — единственный предикат, чей смысл зависит от того, к чему он привязан: привязанный к процессу, он значит «участвует в этом процессе»; привязанный к структурному субъекту (модулю, пространству) — «является потомком этого». Он разбирается только там, где субъект действительно связан — внутри портала where/exists/агрегата или как субъектная сторона обязательства require.
Шаблоны рёбер
Заголовок раздела «Шаблоны рёбер»У стрелки всегда записаны обе стороны — никаких открытых сторон, никакого соположения. Сторона — это атом, this, $knob, * или селектор узлов в скобках:
query DirectToPayments: service > Payments // прямые рёбра service → Paymentsquery UpstreamOfPayments: * >> Payments // весь восходящий конус, как рёбраquery NearPayments: * <> Payments within 2 // рёбра в пределах 2 неориентированных переходовX > Y— базовые рёбра от X к Y.X >> Y— рёбра на любом ориентированном пути длины ≥ 1 от узла-X к узлу-Y.exists (this >> Payments)читается как «в конце концов достигает Payments».X <> Y— рёбра с одним концом на каждой стороне, в любом направлении.X <> Y within Nограничивает длину пути; голоеX <> Y— этоwithin 1.
within N (число или $knob) допустим только при >>/<> — если написать его после голого >, это адресная диагностика, указывающая на >>.
Цепочки — это объединение переходов, а не ограничение пути. A > B > C выводится точно так же, как собственная цепочка вызовов процесса, — два независимых ребра, объединённых, а не «путь через B»:
query CheckoutHops: Customer > Gateway > Orders > Payments// ≡ (Customer > Gateway) or (Gateway > Orders) or (Orders > Payments)Поскольку это объединение, цепочка внутри обязательства require — это ловушка: только первый переход ограничивает this, поэтому require service: this > B > C проходит впустую в тот момент, когда где угодно существует хоть одно ребро B > C. Тянитесь к this >> C (достижимость), когда имеете в виду настоящее многопереходное обязательство.
Предикаты рёбер
Заголовок раздела «Предикаты рёбер»in <Process>/in this, on <plane> и where (<expr>) тоже сочетаются с конъюнкцией рёберного сорта — where там ограничен одним рёберным встроенным геттером, @kind (call или do):
query InvocationEdges: (* > *) and where (@kind = "do") // только рёбра-инвокации (do)in this, привязанный к процессу, втягивает собственные производные рёбра этого процесса — включая рёбра через любой подпроцесс или процесс, который он вызывает через do. Это сквозное включение важно: без него аудиторское обязательство ложно отмечало бы каждый процесс, который отчитывается через общий хелпер do EmitOpsEvent, вместо прямого вызова журнала аудита.
policy ProcessesAudited { require process: (* > AuditLog) and in this}Извлечение — в множества узлов
Заголовок раздела «Извлечение — в множества узлов»Четыре извлечения превращают селектор рёберного сорта обратно в множество узлов — единственное преобразование сорта в языке:
query CallersOfPayments: sources of (* > Payments) // исходные конечные точкиquery CalleesOfPayments: targets of (Payments > *) // конечные точки назначенияquery PaymentsNeighborhood: Payments or nodes of (* <> Payments within 2)query PaymentsProcessOwners: owners of (process and in Payments) // кто владеет шагами этих процессовsources of/targets of нуждаются в ориентированном шаблоне (>/>>); для <> поддерживается только nodes of. И они намеренно точны — количество входящих рёбер и количество входящих вызывающих — это разные вопросы с разной записью: count (* > Payments) против count (sources of (* > Payments)) (два ребра от одного и того же вызывающего во втором случае считаются один раз, в первом — дважды).
nodes of/sources of/targets of никогда не изобретают якорь: nodes of (* <> X) пусто, если у X вообще нет рёбер. Пишите якорь явно, как это делает PaymentsNeighborhood выше (Payments or nodes of (…)).
owners of — единственное извлечение, которое заглядывает внутрь процесса: оно возвращает всех, кто владеет каким-либо владеемым шагом — вызовом, заметкой, await, головой управляющей конструкции — для процессов/подпроцессов в своём аргументе. Шаг, владеемый TODO, добавляет свою синтезированную заглушку, которая не несёт никаких аспектов, — поэтому приведённое ниже правило про зоны отказывает по умолчанию (fail closed) на неразмеченной работе, а не молчаливо её пропускает:
policy PciControl { "No Public-zone decision-maker inside a PCI process" forbid (process and @@security.zone:"PCI" and where (exists ((owners of (this)) and not @@security.zone:"PCI")))}Выражения
Заголовок раздела «Выражения»where (<expr>), exists (<selector>), count (<selector>) и <agg> @g of (<selector>) — это порталы, где живёт язык выражений: одна и та же грамматика, общая для предикатов where, столбцов таблиц и вычисляемых значений style (Глава 8).
Геттеры снабжены сигилами, как и везде: @version/@repo.url читают поля (Глава 9), @@team/@@security.zone читают аспекты. Голый идентификатор в выражении — это литерал, а не ссылка — where (@status = active) сравнивает со значением-идентификатором active, а не с элементом по имени active.
Небольшой набор встроенных геттеров — зарезервированных имён геттеров, годных без каких-либо объявлений:
| Встроенный геттер | Субъект | Значение |
|---|---|---|
@name | любой узел | имя элемента |
@type | любой узел | тип элемента |
@connections | любой узел | число касающихся его рёбер |
@todos | любой узел | число открытых TODO |
@kind | ребро | call | do |
@estimatedLatency | процесс/подпроцесс | наихудшая системная задержка |
@dist | процесс/подпроцесс | истинно, если хоть одно владеемое действие разрешается в эмерджентное управление |
@reversible / @compensated | процесс/подпроцесс | содержит сагу / эта сага полностью покрыта компенсацией |
policy LatencyBudget { severity: warning forbid process and where (@estimatedLatency > 500ms)}Литерал длительности вроде 500ms сравнивается напрямую с @estimatedLatency — сравнение приводит обе стороны к секундам, поэтому геттер поля в форме latency работает точно так же.
Операторы: сравнения = != < <= > >=, булевы and or not, арифметика + - * /, приоритет от самого слабого к самому сильному: or < and < not < сравнение < + - < * /. Скобки группируют как обычно:
policy CapacityGuards { severity: advisory forbid @@domain and where (sum @connections of (service and in this) > 300)}Агрегаты: count (<sel>) над любым сортом, count distinct @g of (<sel>) и sum|min|max @g of (<sel>) (им нужен селектор узлового сорта, поскольку они читают геттер для каждого элемента):
view ProcessAudit { show process and in Checkout table { column @name column "Teams": count distinct @@team of (service and in this and not Gateway) }}
policy PaymentsCostBudget { severity: warning forbid Payments and where (max @cost of (* > Payments) > 500)}exists вычисляет селектор с this, привязанным к текущему субъекту, и истинен, если результат непуст, — одно правило и для членства (exists (this and PiiServices)), и для достижимости (exists (this >> PiiServices)). Именно это заставляет работать правило про owners of выше (Извлечение): exists ((owners of (this)) and not @@security.zone:"PCI") читается как «какой-то из владельцев шагов этого процесса НЕ в зоне PCI».
Функции — это явные приведения, никогда не неявные: colorize(v) (категориальное значение → легенда с образцом на каждое значение), heat(v [, min, max]) (числовое значение → градиент), css(v) (значение уже цвет, используется как есть). Они существуют потому, что значение никогда не становится цветом рендера молча — вы видели, как они управляют style в Главе 8; приведение — это одна и та же функция, независимо от того, написана она внутри блока style или в любом другом слоте выражения.
Именованные запросы
Заголовок раздела «Именованные запросы»query Name: <selector> объявляет переиспользуемый селектор — на верхнем уровне файла или внутри тела модуля. Запрос несёт свой сорт: запрос узлового сорта используйте там, где ожидается селектор узлового сорта, рёберного — там, где ожидается селектор рёберного сорта. Использование там, где сорт не подходит, — это адресная диагностика, а не молчаливая переинтерпретация.
query PiiServices: service and @@data:"pii"export query публикует запрос через границы пакета так же, как экспортированный подпроцесс пересекает пространства, — экспортировать можно только запросы верхнего уровня файла:
export query PciServices: @@security.zone:"PCI"Вступительный пример уже показал выгоду: один запрос питает show, forbid политики и получившийся style violating. Вот вся дуга ещё раз, расписанная от начала до конца — объявить один раз, переиспользовать в выборке проекции, в правиле политики и в стилизации соответствия этой же проекции:
query PiiServices: service and @@data:"pii"
policy PiiAudited { "Every PII-touching service is audited somewhere downstream" severity: warning require PiiServices: this >> Audit}
view PiiExposure { "Everything that can reach PII data, PII services flagged" show PiiServices or nodes of (* >> PiiServices within 3) style violating PiiAudited { color: crimson } style PiiServices { widget { icon: shield } }}PiiServices никогда не приходится выводить заново — show проекции, субъект require политики и (транзитивно, через политику) style violating этой же проекции читают одно и то же объявление. Переименуйте ключ аспекта один раз, в одном месте, — и каждый потребитель останется корректным.
Что дальше
Заголовок раздела «Что дальше»Глава 32: Политики и управление → — forbid, require, исключения except и гейты изменений when, построенные на ядре селекторов из этой главы.