Глава 16. Почему модель нужно строить заранее
16.1 Когда решение о доступе становится вычислением
До этого момента мы рассматривали доступ как результат применения правил к конкретной ситуации.
Есть субъект. Есть действие. Есть объект. Между ними существует набор отношений и фактов. Часть этих фактов является непосредственной, часть — производной. Из них формируется контекст, затем определяется эффективное разрешение, и только после этого принимается решение о доступе.
Для небольшого числа объектов и отношений такую модель можно вычислять непосредственно в момент запроса.
Допустим, пользователь обращается к объекту. Система может последовательно проверить:
-
является ли пользователь владельцем;
-
состоит ли он в нужной группе;
-
связана ли группа с проектом;
-
опубликован ли объект в этой области;
-
назначена ли пользователю соответствующая роль;
-
какие разрешения даёт эта роль;
-
какие ограничения действуют для данного объекта;
-
какие из найденных отношений действительно применимы.
Пока таких проверок немного, это выглядит естественно.
Но сама модель уже содержит важное свойство: отношения образуют зависимости.
Изменение одного отношения может повлиять не на один объект и не на одного пользователя.
Если пользователь вступил в группу, это может изменить доступ к множеству объектов. Если группа была включена в другую группу, изменяется положение всех её участников. Если роль назначена на проект, измениться может доступ к целому набору объектов внутри этого проекта. Если объект был опубликован в новой области, для него появляется новый путь видимости и действия.
Следовательно, вопрос постепенно меняется.
Нужно вычислить не просто:
имеет ли пользователь право на этот объект?
Нужно вычислить:
какие отношения и факты должны быть учтены, чтобы ответить на этот вопрос?
А это уже может быть нетривиальным вычислением.
Причём сложность определяется не только количеством объектов. Она определяется количеством зависимостей между фактами.
Пусть есть пользователь U, группа G, проект P и объект O.
Связь пользователя с объектом может выглядеть так:
U → G → P → O
Но эта цепочка сама по себе ещё ничего не разрешает. На каждом переходе существует собственная семантика.
Например, членство пользователя в группе может определять принадлежность к области. Роль может действовать в проекте. Публикация может связывать объект с проектом. Правило доступа должно определить, как эти отношения соединяются и какое разрешение из них получается.
Если такую цепочку строить заново для каждого запроса, запрос к данным постепенно превращается в запрос к самой модели безопасности.
И это важное архитектурное изменение.
Система начинает выполнять одну и ту же работу снова и снова.
При этом исходные факты, из которых строится результат, обычно меняются гораздо реже, чем читается результат.
Пользователь может оставаться членом одной группы часами или месяцами. Роль может быть назначена на проект на длительный срок. Публикация объекта также может оставаться неизменной долгое время.
Но за это время система может выполнить тысячи или миллионы проверок доступа.
Получается асимметрия:
изменения модели происходят относительно редко, а чтение результата происходит очень часто.
Именно здесь возникает необходимость строить модель заранее.
16.2 Что значит «заранее»
«Заранее» не означает, что система должна заранее принять все решения о доступе.
Это было бы слишком сильным утверждением.
Нельзя заранее знать все будущие запросы. Субъект, действие и объект конкретного запроса могут быть неизвестны до самого момента обращения.
Заранее нужно построить не само решение, а ту часть модели, которая нужна для его быстрого получения.
Это принципиальное различие.
Можно заранее определить производные факты:
-
какие группы связаны с пользователем;
-
какие разрешения получает пользователь;
-
в каких областях эти разрешения действуют;
-
какие области связаны с объектом;
-
какие отношения между субъектом и объектом могут участвовать в проверке.
А уже в момент запроса сопоставить эти факты с конкретным действием и объектом.
То есть архитектура разделяется на два разных момента.
Построение модели:
Исходные факты → Производные факты
Проверка конкретного запроса:
Субъект + Действие + Объект + Производные факты → Решение
Такое разделение позволяет не выполнять одну и ту же работу при каждом обращении.
Важно и другое: заранее построенная модель не становится новым источником истины.
Исходные отношения по-прежнему остаются исходными фактами. Производные данные лишь представляют результат их обработки в форме, удобной для последующего использования.
Это особенно важно для безопасности.
Если производная модель перестала соответствовать исходным фактам, проблема заключается не в том, что производный факт «стал неправильным источником истины». Проблема заключается в том, что производное состояние устарело.
Следовательно, архитектура должна решать две разные задачи:
-
построить производную модель;
-
поддерживать её в соответствии с исходными фактами.
Первая задача отвечает на вопрос:
как получить модель?
Вторая:
как не дать ей устареть?
Именно вторая задача становится особенно важной при изменении отношений.
16.3 Почему нельзя просто пересчитывать всё
На первый взгляд можно предложить простое решение.
Каждый раз, когда меняется любой факт безопасности, полностью пересчитывать модель.
Такой подход действительно имеет одно очевидное преимущество: его проще рассуждать.
Изменился факт — построили всё заново.
Но стоимость такого решения растёт вместе с размером системы.
Предположим, изменение одного членства пользователя в группе потенциально влияет на несколько тысяч объектов. Нет необходимости пересчитывать права остальных пользователей, если их отношения не изменились.
Если роль была изменена для одного проекта, нет необходимости заново строить модель для всей системы.
Если объект был опубликован в одной области, не требуется пересчитывать отношения, не связанные с этим объектом.
Значит, у изменения есть область влияния.
Это одна из самых важных характеристик производной модели.
Изменился исходный факт F.
Необходимо определить:
F → затронутые производные факты
а затем обновить только их.
Получается уже не полный пересчёт, а распространение изменения по зависимостям.
Такой подход требует знания того, от каких исходных фактов зависит каждый производный результат.
Например, эффективное разрешение пользователя может зависеть одновременно от:
-
назначения роли;
-
области этой роли;
-
членства пользователя;
-
иерархии групп;
-
публикации объекта;
-
владельца объекта;
-
других ограничений модели.
Если изменился один из этих фактов, система должна понимать, какие производные результаты могли измениться.
Поэтому производная модель — это не просто набор удобных таблиц или кешей.
За ней стоит структура зависимостей.
И чем сложнее модель доступа, тем важнее становится эта структура.
16.4 Почему модель должна быть готова до проверки данных
Есть ещё одна причина строить модель заранее.
Проверка доступа часто выполняется уже на уровне данных.
Система должна не просто сказать:
пользователю разрешено читать объект.
Она должна обеспечить, чтобы пользователь действительно получил только те данные, которые соответствуют этому разрешению.
Значит, решение об авторизации должно быть доступно в момент выполнения запроса к данным.
Если для каждой строки таблицы необходимо сначала пройти по исходным отношениям, построить цепочки, вычислить области, определить эффективное разрешение и только после этого решить, можно ли вернуть строку, то сама проверка данных становится зависимой от сложного графа отношений.
Это плохо масштабируется.
Чем глубже модель отношений, тем больше работы приходится выполнять непосредственно во время чтения.
Поэтому полезно перенести сложную часть вычисления из момента проверки в момент построения модели.
Тогда проверка становится значительно проще.
Вместо:
найти отношения → пройти цепочки → вычислить разрешения → определить применимость → проверить объект
можно приблизиться к:
найти готовые производные факты → сопоставить их с объектом → проверить действие
Это не означает, что запрос перестаёт быть частью модели безопасности.
Наоборот.
Просто сложное вычисление выполняется раньше, а запрос использует уже подготовленный результат.
Так появляется принцип:
дорогие вычисления, зависящие от отношений, по возможности выполняются при изменении модели, а не при каждом чтении данных.
16.5 Что именно нужно строить заранее
Теперь можно точнее определить, что означает «построить модель безопасности».
Не нужно заранее создавать запись для каждого возможного запроса.
Количество комбинаций:
Subject × Action × Object
может быть огромным.
Большая часть таких комбинаций никогда не будет использована.
Поэтому заранее строятся не все возможные решения, а устойчивые производные факты, из которых эти решения можно получать.
Например, вместо хранения ответа:
пользователь
Uможет выполнитьREADнад объектомO
можно хранить более фундаментальные производные факты:
пользователь
Uимеет такое-то разрешение в такой-то области;
и отдельно:
объект
Oсвязан с такой-то областью.
Тогда конкретный запрос соединяет уже подготовленные части модели.
Это позволяет избежать двух крайностей.
Первая — вычислять всю модель с нуля при каждом запросе.
Вторая — заранее материализовать все возможные решения для всех пользователей, действий и объектов.
Первая крайность слишком дорога во время чтения.
Вторая создаёт огромный объём производных данных и делает каждое изменение чрезвычайно дорогим.
Между ними находится более устойчивый архитектурный вариант:
материализовать те производные факты, которые являются стабильными результатами модели и могут многократно использоваться при проверках.
Это и есть смысл проекционного подхода.
Проекция представляет исходную модель в другой форме — не для изменения её смысла, а для эффективного использования.
16.6 Изменение становится частью модели
После этого становится очевидно, почему изменение исходного факта нельзя рассматривать как локальную операцию.
Если пользователь был добавлен в группу, изменился не только один факт членства.
Могли измениться:
-
его эффективные отношения с областями;
-
его эффективные разрешения;
-
доступ к объектам, связанным с этими областями.
Если изменилась публикация объекта, изменился не только факт публикации.
Могла измениться видимость объекта для множества субъектов.
Если изменилась роль, результат может распространиться на все области, в которых эта роль действует.
Поэтому изменение должно рассматриваться как начало цепочки:
Исходный факт изменён
→ определена область влияния
→ пересчитаны затронутые производные факты
→ новое состояние модели готово к чтению
Это означает, что модель безопасности должна иметь не только структуру данных, но и механизм её построения и обновления.
В этот момент архитектура начинает отличаться от обычной проверки ролей.
Мы уже не просто проверяем разрешение.
Мы поддерживаем отдельное состояние, которое является производным от отношений системы.
И это состояние должно быть достаточно полным для быстрых проверок, достаточно точным для безопасного доступа и достаточно локальным, чтобы его можно было обновлять без полного пересчёта всей системы.
16.7 От вычисления к проекции
Итак, к этому моменту в модели появилась новая необходимость.
Исходные факты нельзя просто оставить в исходном виде и каждый раз заново проходить все отношения.
Но и хранить готовый ответ на каждый возможный запрос тоже не требуется.
Нужен промежуточный слой.
Он должен отвечать на вопросы, которые повторяются при большом количестве запросов:
-
какие отношения пользователя уже известны системе;
-
какие эффективные разрешения из них следуют;
-
какие области связаны с объектом;
-
какие производные факты изменились после изменения исходного состояния.
Такой слой можно рассматривать как проекцию модели безопасности.
Проекция не заменяет исходные факты.
Она представляет их в форме, оптимизированной для конкретного класса запросов.
Отсюда следует важное архитектурное разделение:
Исходная модель
→ Построение производных фактов
→ Проекции безопасности
→ Проверка доступа
На этом уровне нам пока не важно, будут ли проекции таблицами базы данных, материализованными представлениями, индексами, отдельным хранилищем или другой структурой.
Это уже вопрос реализации.
Но сам принцип появляется именно здесь:
если модель доступа содержит отношения и производные факты, которые используются многократно, разумно построить их заранее и использовать при последующих проверках, а не вычислять заново весь путь отношений для каждого запроса.
Следующая глава будет посвящена тому, как выглядит такой проекционный слой и какие разные проекции нужны для разных частей модели.
16.8 Главный вывод
Сложная модель доступа создаёт две разные нагрузки.
Первая возникает тогда, когда меняются исходные факты.
Вторая — когда множество запросов читает результат этой модели.
Если всю сложность отношений переносить в каждый запрос, система платит за одно и то же вычисление снова и снова.
Поэтому часть модели строится заранее.
При этом заранее строится не окончательное решение для всех возможных запросов, а производные факты, из которых решение можно получить быстро.
Получается следующий цикл:
исходные факты → построение производной модели → проверка запросов → изменение исходных фактов → обновление производной модели.
С этого момента безопасность становится не только алгоритмом проверки.
Она становится состоянием системы, которое необходимо построить и поддерживать.
А следующий архитектурный вопрос уже вполне конкретен:
как представить это состояние так, чтобы оно было одновременно производным, обновляемым и пригодным для быстрого контроля доступа?