Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Глава 28. Событие как источник изменения

В предыдущих главах мы рассматривали систему в момент проверки доступа.

Запрос приходит в систему.

Для него определяется субъект, действие и объект.

На основании исходных фактов и производных security-фактов вычисляется разрешение.

После этого результат применяется к данным.

Но модель безопасности не существует в неизменном состоянии.

Пользователь вступает в группу.

Пользователь выходит из группы.

Меняется роль.

Объект публикуется.

Публикация отзывается.

Меняется область действия отношения.

Удаляется или закрывается объект.

Каждое такое изменение может изменить множество будущих решений о доступе.

Значит, архитектуре нужен механизм, который связывает:

Изменение исходного факта
        ↓
Изменение производных фактов
        ↓
Новое состояние модели безопасности

Таким механизмом может быть событие.

Но здесь важно сразу провести границу.

Событие не является самой моделью безопасности.

Оно сообщает, что в системе произошло изменение, которое должно быть отражено в зависимых представлениях.

28.1 Изменение факта должно куда-то распространяться

Рассмотрим простое изменение:

User A → member of Group G

Само по себе это исходный факт.

Но если группа участвует в модели доступа, изменение может повлечь:

User A
  ↓
Group G
  ↓
Parent Group P
  ↓
Effective relation
  ↓
Effective permission
  ↓
Object access

Одной записи о членстве недостаточно.

Необходимо изменить все производные данные, которые зависят от этого факта.

Поэтому после изменения возникает задача:

какие части модели безопасности теперь стали недействительными?

Это уже задача распространения изменения.

28.2 Событие фиксирует изменение, а не результат авторизации

Очень легко смешать два разных понятия.

Событие:

UserGroupLinked

говорит:

произошло изменение отношения между пользователем и группой.

Оно не говорит:

User A can READ Object X

Это уже производный вывод.

Разница принципиальна.

Источник события находится на уровне исходного факта.

Результат обработки события находится на уровне производной модели.

Поэтому правильная цепочка выглядит так:

Source fact
    ↓
Event
    ↓
Projection update
    ↓
Derived security fact

А не:

Event
    ↓
ALLOW

Событие не должно содержать готовое решение авторизации, если это решение можно получить из модели.

28.3 Событие должно иметь понятную семантику

Событие имеет смысл только тогда, когда понятно, что именно изменилось.

Например:

UserGroupLinked
UserGroupUnlinked
RoleAssigned
RoleRevoked
ObjectPublished
PublicationRevoked
ObjectClosed

Названия здесь важны не сами по себе.

Важно, что каждое событие соответствует определённому изменению исходного состояния.

Например:

UserGroupLinked

не означает:

пользователю выданы все права группы.

Оно означает:

создано отношение членства пользователя в группе.

А уже правила модели определяют, какие производные факты должны измениться вслед за этим.

Это позволяет не зашивать правила авторизации внутрь механизма доставки событий.

28.4 Одно событие может иметь большой радиус воздействия

Изменение может быть маленьким физически и большим логически.

Например, изменение одной связи:

A ∈ G

может изменить эффективные отношения пользователя со множеством объектов.

Если группа является частью иерархии:

G
↑
P
↑
R

то изменение связи с G может повлиять на производные отношения, связанные с несколькими уровнями этой структуры.

А если эти отношения участвуют в формировании разрешений, изменяется ещё один слой:

Membership
   ↓
Effective groups
   ↓
Effective permissions
   ↓
Object access

Поэтому нельзя определять область изменения только количеством изменённых строк.

Одна строка может иметь очень большой радиус влияния.

28.5 Обработчик события не должен придумывать новую модель

Предположим, обработчик получил:

RoleAssigned

У него есть два возможных подхода.

Первый:

прочитать событие
→ самостоятельно решить,
  какие права выдать
→ записать их

Второй:

прочитать изменение
→ применить существующие правила модели
→ обновить соответствующие производные факты

Второй подход принципиально безопаснее с архитектурной точки зрения.

Иначе обработчик постепенно превращается в ещё один authorization engine.

Через некоторое время получится:

Application authorization
        +
Projection authorization
        +
Event-handler authorization
        +
Database authorization

И каждый слой начнёт трактовать отношения немного по-разному.

Событие должно запускать существующую модель, а не создавать новую.

28.6 Событие связывает время изменения с новым состоянием модели

Проекция всегда относится к некоторому состоянию исходных данных.

Пусть было:

State N

происходит изменение:

Event E

и после обработки должна появиться:

State N+1

Поэтому событие связывает два состояния:

Source state N
      ↓
    Event
      ↓
Source state N+1
      ↓
Projection state N+1

Если событие потеряно, проекция может остаться в состоянии N, хотя исходные данные уже находятся в N+1.

Это и есть одна из причин, почему безопасность производных данных требует особого внимания.

Для обычной аналитической проекции устаревшее значение может быть неприятным.

Для security projection устаревшее разрешение может изменить фактический доступ.

28.7 Событие не обязательно означает немедленную обработку

Изменение и обработка его производных последствий могут происходить в разное время.

Например:

T1  Source fact changed
T2  Event recorded
T3  Event processed
T4  Projection updated

Между T1 и T4 существует переходное состояние.

Это не обязательно ошибка.

Архитектура может сознательно использовать асинхронное обновление.

Но тогда необходимо определить:

какое состояние безопасности считается допустимым в этот промежуток?

Особенно важно различать два случая.

Если доступ должен быть отозван немедленно, задержка обновления проекции может быть недопустимой.

Если небольшая задержка допустима, это должно быть частью явно определённой семантики системы.

Нельзя считать eventual consistency свойством, которое автоматически подходит безопасности.

28.8 Отзыв доступа требует такой же надёжности, как выдача

Добавление разрешения обычно легко заметить:

+ permission

Но архитектурно гораздо опаснее другое:

- permission

Пользователь вышел из группы.

Роль отозвана.

Публикация закрыта.

Объект больше не находится в области действия отношения.

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

Поэтому поток должен поддерживать оба направления:

Grant  → projection update
Revoke → projection update

Система, которая надёжно добавляет права, но ненадёжно их удаляет, не является надёжной моделью безопасности.

28.9 Надёжность события и надёжность проекции — разные задачи

Даже если событие сохранено, остаётся другая проблема.

Обработчик может:

  • не запуститься;

  • завершиться с ошибкой;

  • обработать событие частично;

  • получить событие повторно;

  • обработать события не в том порядке;

  • остановиться между двумя операциями.

Поэтому архитектура должна отвечать как минимум на несколько вопросов:

Событие потеряно?
Событие обработано?
Обработка завершилась?
Проекция соответствует источнику?
Можно ли повторить обработку?
Можно ли восстановить проекцию?

Это уже не свойства самого события.

Это свойства всей цепочки распространения изменения.

28.10 Идемпотентность становится обязательным свойством

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

Например:

Event E
  ↓
Projection updated
  ↓
ack lost
  ↓
Event E again

Если повторная обработка создаёт дополнительный эффект, состояние может стать неверным.

Поэтому обработка должна быть идемпотентной по результату.

Это означает:

Apply(E)
Apply(E)

должно приводить к тому же состоянию, что и:

Apply(E)

Идемпотентность не решает проблему согласованности полностью.

Но без неё надёжная повторная обработка становится значительно сложнее.

28.11 Очередность имеет значение не всегда, но должна быть определена

Рассмотрим два события:

E1: UserGroupLinked
E2: UserGroupUnlinked

Очевидно, что итоговое состояние зависит от их порядка, если события описывают последовательные изменения одного отношения.

Поэтому система должна понимать, какие изменения требуют упорядоченной обработки.

Но это не означает, что все события всей системы должны обрабатываться глобально последовательно.

Глобальный порядок может оказаться слишком дорогим и ненужным.

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

Это ещё один пример того, почему архитектура должна начинаться с семантики данных, а не с выбранного механизма доставки.

28.12 Событие и транзакция должны иметь определённую связь

Есть важная граница.

Предположим, система изменила исходный факт:

INSERT membership

а событие записала отдельно:

INSERT event

Если первая операция завершилась успешно, а вторая нет, система получила изменение без сигнала для проекций.

Если наоборот событие появилось без фактического изменения, обработчик может попытаться построить производное состояние из того, чего нет.

Поэтому нужно определить связь между:

Source change

и:

Change notification

В ряде архитектур событие фиксируется в той же транзакционной границе, что и исходное изменение.

В других используется специальный механизм надёжной публикации изменений.

Конкретная технология здесь вторична.

Главное требование одно:

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

28.13 Событие не заменяет возможность полного пересчёта

Даже идеальная обработка событий не устраняет необходимость восстановления.

Проекция может быть:

  • повреждена;

  • удалена;

  • построена по ошибочной версии правила;

  • несовместима с новой схемой;

  • остановлена на середине обработки;

  • создана впервые после появления исходных данных.

Поэтому должна существовать возможность:

Source facts
    ↓
Rebuild
    ↓
Security projections

События обеспечивают инкрементальное распространение изменений.

Полный пересчёт обеспечивает восстановление и проверку модели.

Это не конкурирующие подходы.

Они решают разные задачи.

28.14 Событие должно быть частью наблюдаемого пути

Если изменение доступа прошло через несколько этапов:

Source
 → Event
 → Projection
 → Authorization
 → Data

то каждый этап должен быть наблюдаемым.

Иначе невозможно ответить на простой вопрос:

почему пользователь получил или потерял доступ?

Например:

Source fact changed      ✓
Event recorded            ✓
Event processed           ✓
Projection updated       ✗
Authorization             ?

Такой путь гораздо легче диагностировать, чем ситуацию, когда все изменения скрыты внутри нескольких независимых SQL-запросов.

Наблюдаемость здесь становится не только эксплуатационным удобством.

Она является способом проверить, действительно ли модель безопасности распространяется так, как задумано.

28.15 В Guardian событие соединяет исходные факты и проекции

В Guardian изменение отношений проходит именно через этот принцип.

Например:

KUserGroupLink
      ↓
security change
      ↓
effective_user_group
      ↓
effective_user_permission
      ↓
object authorization

А изменение публикации имеет другой путь:

KPublish
   ↓
object_scope
   ↓
object authorization

То есть событие не должно содержать готовое:

ALLOW user X object Y

Оно сообщает об изменении исходного факта.

Соответствующая проекция обновляется по правилам своей модели.

Именно поэтому разные исходные отношения могут иметь разные маршруты распространения.

28.16 Событие — не источник истины

Есть ещё одна принципиальная граница.

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

Это полезно для аудита и восстановления.

Но даже тогда нужно различать:

Source state

и:

History of changes

Событие отвечает на вопрос:

что произошло?

Исходное состояние отвечает на вопрос:

что существует сейчас?

Для построения текущей security model обычно нужны именно текущие исходные факты и правила.

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

28.17 Главный вывод

Событие нужно не потому, что «современная архитектура должна быть событийной».

Оно нужно потому, что изменение исходного факта должно надёжно распространиться на производные части модели.

Полная цепочка выглядит так:

Source fact changes
        ↓
Event records the change
        ↓
Affected projections are identified
        ↓
Derived security facts are recalculated
        ↓
New security state becomes available
        ↓
Future authorization uses the new state

При этом необходимо сохранить несколько принципов.

Событие описывает изменение, а не готовое разрешение.

Обработчик применяет существующую модель, а не создаёт вторую.

Выдача и отзыв доступа должны распространяться одинаково надёжно.

Повторная обработка не должна искажать результат.

Порядок событий должен учитываться там, где он влияет на состояние.

Проекция должна иметь возможность полного восстановления.

И главное:

событие — это механизм переноса изменения из мира исходных фактов в мир производных фактов.

Теперь возникает следующий вопрос.

Если событие сообщает об изменении, кто именно превращает его в новое состояние security model?

Где находятся правила построения проекций?

И как сделать так, чтобы разные проекции не начали независимо интерпретировать одни и те же факты?

Этим занимается следующий слой архитектуры — проекционный слой.