Глава 23. Один объект — разные физические данные
В предыдущей главе мы дошли до базы данных как последней границы безопасности.
Но здесь возникает новая проблема.
Модель авторизации работает с логическими объектами:
Subject → Action → Object
А база данных работает со строками, таблицами и связями между ними.
Между этими двумя представлениями нет обязательного соответствия «один объект — одна строка».
Один объект может быть представлен множеством физических записей.
И наоборот, одна физическая таблица может содержать данные множества логических объектов.
Поэтому для надёжной защиты недостаточно знать, разрешён ли доступ к объекту.
Нужно ещё понимать, какие физические данные принадлежат этому объекту и как его контекст переносится на эти данные.
23.1 Логический объект и физическое представление — разные уровни
Логический объект существует на уровне предметной модели.
Например:
Document-123
может быть самостоятельным объектом с собственной идентичностью, жизненным циклом, владельцем и отношениями доступа.
Но физически он может выглядеть так:
object
id = 123
object_attribute
object_id = 123
...
object_revision
object_id = 123
...
object_file
object_id = 123
...
Для модели безопасности это один объект.
Для базы данных — несколько наборов строк.
Поэтому проверка:
ALLOW(Document-123)
не является сама по себе проверкой:
ALLOW(all rows related to Document-123)
Связь между этими утверждениями должна быть частью архитектуры.
Именно она определяет, как решение на уровне объекта превращается в ограничение на уровне данных.
23.2 Один объект может иметь несколько физических частей
Разделение объекта на несколько таблиц возникает по самым разным причинам.
Это может быть:
-
нормализация данных;
-
разделение редко используемых атрибутов;
-
хранение версий;
-
отдельные таблицы связей;
-
специализированные индексы;
-
хранение больших значений;
-
исторические записи;
-
технические данные.
При этом не все физические части объекта обязательно имеют одинаковую семантику доступа.
Например:
Document
├── metadata
├── attributes
├── revisions
└── files
Для пользователя всё это может восприниматься как один документ.
Но с точки зрения безопасности доступ к каждой части может определяться отдельно.
Это приводит к важному принципу:
Логическая принадлежность данных объекту ещё не означает одинаковую политику доступа ко всем этим данным.
Иногда доступ действительно должен распространяться на весь объект.
Иногда отдельная часть должна иметь более жёсткое ограничение.
Иногда часть данных вообще не должна быть доступна через пользовательский интерфейс.
Следовательно, физическая модель не должна заставлять нас делать необоснованные выводы о модели доступа.
23.3 Одна таблица может содержать множество объектов
Обратная ситуация встречается ещё чаще.
Например:
object_attribute
----------------
object_id | name | value
101 | ...
102 | ...
103 | ...
Все эти строки принадлежат одной таблице.
Но относятся к разным логическим объектам.
Если субъект имеет доступ только к объекту 101, запрос:
SELECT *
FROM object_attribute;
не должен возвращать ему строки объектов 102 и 103.
Значит, физическая таблица должна иметь возможность связать каждую строку с тем объектом или контекстом, который определяет её допустимость.
Эта связь может быть прямой:
row.object_id → object.id
или опосредованной:
row → revision → object
или:
row → project → object
Но она должна быть определена.
Иначе невозможно надёжно перенести решение об авторизации на физический уровень.
23.4 Контекст объекта не должен теряться при переходе к строке
До этого мы рассматривали контекст как совокупность фактов и отношений, необходимых для конкретного решения.
Теперь появляется ещё один вопрос:
Что происходит с этим контекстом, когда мы переходим от объекта к физической строке?
Допустим:
Object A
↓
Project P
↓
Subject S
Именно эта цепочка отношений позволила получить эффективное разрешение.
Но SQL уже работает с конкретной строкой:
row #8472
Чтобы защита сохранилась, должна существовать связь:
row #8472
↓
Object A
↓
security context
↓
Subject S
Иначе контекст закончится на уровне объектной проверки.
Это одна из наиболее важных архитектурных связей всей модели:
Logical Object
│
│ identity / relation
▼
Physical Data
│
│ security boundary
▼
RLS / enforcement
Физическая модель должна позволять определить, к какому логическому объекту относится защищаемая информация.
23.5 Не всякая связь с объектом означает одинаковый доступ
Здесь легко сделать ещё одну ошибку.
Если строка связана с объектом, можно попытаться сказать:
если есть доступ к объекту,
то есть доступ ко всем связанным строкам.
Но это не универсальное правило.
Связь может иметь разный смысл.
Например, строка может быть:
-
основной частью объекта;
-
исторической записью;
-
техническим журналом;
-
связью между объектами;
-
производным представлением;
-
служебной информацией.
Поэтому нужно различать:
Object relationship
и
Data access relationship
Первая отвечает на вопрос:
как эта запись связана с объектом?
Вторая:
должен ли доступ к объекту распространяться на эту запись?
Иногда ответы совпадают.
Но совпадение должно быть следствием модели, а не предположением, сделанным только по внешнему ключу.
23.6 Один объект может иметь разные уровни данных
Рассмотрим более сложный объект:
Object
├── public metadata
├── internal metadata
├── confidential attributes
└── binary content
Все эти данные относятся к одному объекту.
Но доступ к ним может различаться.
Например, субъект может иметь право:
READ(Object)
но конкретная модель может разрешать ему только часть представления объекта.
Тогда понятие «доступ к объекту» оказывается недостаточно точным.
Нужно различать как минимум:
доступ к объекту
доступ к данным объекта
доступ к отдельной части данных
Это не означает, что каждая система должна вводить отдельные права для каждого поля.
Такой уровень детализации нужен только там, где он соответствует предметной модели.
Главное — не предполагать, что физическое представление автоматически наследует все свойства логического объекта.
23.7 Связь объекта и данных особенно важна для массовых запросов
Для единичного запроса проблему иногда можно скрыть.
Например:
GET /objects/123
Приложение знает конкретный объект и может проверить его доступ.
Но представим:
GET /objects
или:
SELECT *
FROM object_attribute
WHERE name = 'status';
Теперь заранее неизвестно, какие объекты попадут в результат.
Запрос должен сам ограничить множество данных:
All rows
│
▼
rows belonging to accessible objects
│
▼
actual result
Именно здесь становится особенно важной связь:
Physical Row → Logical Object
Если она хорошо определена, политика доступа может работать независимо от того, был ли запрос единичным или массовым.
Если её нет, разработчику приходится вручную восстанавливать эту связь в каждом запросе.
А это возвращает нас к проблеме, которую мы уже видели: безопасность становится свойством отдельных программных путей.
23.8 Как это выглядит в архитектуре Guardian
В Guardian KObject является логическим объектом, для которого существует модель владельца, публикаций, отношений и эффективных разрешений.
Физические данные при этом не обязаны быть одной строкой KObject.
Архитектурно важно другое: конкретная таблица должна иметь определённую связь с объектом или с тем security context, который определяет допустимость строки.
Поэтому путь выглядит примерно так:
KObject
│
├── object identity
│
├── KContext
│
├── publications / grants / relations
│
▼
security projections
│
▼
effective permission
│
▼
table-specific RLS
│
▼
physical rows
При этом нельзя делать обратный вывод:
если строка находится в таблице, связанной с
KObject, значит правила доступа кKObjectавтоматически одинаковы для этой таблицы.
В Guardian разные классы данных имеют разные RLS-политики.
Именно поэтому has_object_permission и RLS остаются отдельными уровнями.
Объектная авторизация определяет допустимость действия.
RLS определяет фактическую границу данных для конкретной таблицы.
23.9 Физическая модель становится частью модели безопасности
На этом этапе становится видно, что безопасность нельзя проектировать полностью отдельно от структуры данных.
Если система должна гарантировать:
Subject S
может читать
Object O
то физическая модель должна позволять однозначно определить:
какие строки представляют O
и:
какие связанные строки должны быть доступны вместе с O
или, наоборот:
какие связанные строки не должны становиться доступными автоматически.
Поэтому при проектировании защищённой системы нужно рассматривать как минимум три слоя:
Domain model
↓
Security model
↓
Physical data model
Первый определяет, какие объекты и отношения существуют.
Второй определяет, какие из них имеют значение для доступа.
Третий определяет, как эти объекты и отношения представлены физически.
Если между слоями нет явного соответствия, безопасность начинает зависеть от случайных особенностей SQL и структуры отдельных таблиц.
А значит, архитектура безопасности должна учитывать не только вопрос:
«Кому разрешён доступ к объекту?»
но и вопрос:
«Где именно в физических данных находится граница этого объекта?»
23.10 Главный вывод
Логический объект и физические данные — разные представления одной системы.
Один объект может состоять из множества строк.
Одна таблица может содержать данные множества объектов.
Связанные с объектом данные могут иметь разную семантику доступа.
Поэтому:
ALLOW(Object)
не означает автоматически:
ALLOW(All related rows)
Между ними должна существовать явная архитектурная связь.
В полной цепочке она выглядит так:
Object
↓
Security context
↓
Effective permission
↓
Object authorization
↓
Object ↔ Physical Data mapping
↓
Data enforcement
↓
Physical rows
Именно эта связь позволяет сохранить смысл модели доступа после перехода от абстрактного объекта к реальной базе данных.
Но остаётся ещё одна проблема.
Что происходит, когда система только запускается, а производные security projections ещё не построены?
Источник истины уже содержит объект и отношения, но слой производных данных ещё пуст.
Следующая глава — «Холодный старт» — посвящена именно этому состоянию.