Перейти к содержимому

Безопасность

Эта страница переведена с английского. Читать страницу на английском

Безопасность в Zeyn объявляется рядом с кодом, который она защищает, и проверяется на сервере при каждом вызове. Значения по умолчанию строгие; ослабление любого из них заметно как отдельная строка в app.rs.

Разрешения

Каждое действие (action) и каждый экран (Screen) объявляют permission = "feature.resource.verb" или public. Без этого код не компилируется. Идентичность запроса несёт его разрешения:

Identity::user("ada").with_permissions(["stock.product.read", "stock.product.write"])

Пункты меню, кнопки панели инструментов, действия строк и команды, которые пользователю нельзя выполнять, не попадают в HTML. Это лишь удобство; сервер проверяет разрешение заново при каждом вызове.

Вход в систему

Приложение не хранит пароли. Authenticator определяет, кто выполняет вызов, обычно по прокси единого входа (single sign on) перед приложением:

App::new(t!("app.name")).auth(
    ForwardAuth::new("x-forwarded-user")
        .groups("x-forwarded-groups")
        .require_header("x-proxy-secret", secret.expose())
        .role("stock-admins", ["stock.product.read", "stock.product.write"]),
)

Используйте ForwardAuth только тогда, когда каждый запрос проходит через прокси: любой, кто может обратиться к приложению напрямую, способен отправить эти заголовки. require_header проверяет общий секрет, который добавляет прокси. DevAuth::superuser(name) даёт каждому запросу полный доступ для локальной разработки, и сервер выводит предупреждение, пока он включён. Релизная сборка отказывается запускаться с ним (App::check сообщает об этом); демонстрация, которая должна работать в релизе, может вызвать App::allow_dev_auth(). Приложения, созданные через zeyn new, включают его только в отладочных сборках.

Охранники (Guards)

Охранник выполняется до открытия любого экрана Feature и до вызова любой из его таблиц, форм или команд:

fn warehouse_open(ctx: &Ctx) -> GuardResult {
    if ctx.identity().permissions.contains("stock.warehouse.read") { GuardResult::Allow } else { GuardResult::Deny(t!("stock.closed")) }
}

Feature::new("stock").guard(SignedIn).guard(warehouse_open)

Встроенные guard-ы: SignedIn, TenantRequired и Licensed.

Перехватчики (Interceptors)

Перехватчик выполняется вокруг каждого вызова экрана, таблицы, формы, API и команды после проверок доступа. before может остановить вызов; after видит, чем он закончился. AuditLog(sink) встроенный перехватчик, который записывает, кто что сделал.

CSRF, CSP и заголовки

  • Каждая страница устанавливает случайную cookie zeyn_csrf (HttpOnly, SameSite=Strict) и помещает тот же токен в meta-тег. Каждый вызов из среды выполнения отправляет его в заголовке x-zeyn-csrf, а сервер сравнивает оба значения за постоянное время.
  • Исключение составляют публичные экраны, открытые без сессии: они не устанавливают cookie и не несут токена, поэтому их можно кэшировать. Среда выполнения получает токен через GET /_zeyn/csrf перед первым вызовом, и каждый POST по-прежнему проверяется так же (см. Публичные страницы).
  • Content Security Policy разрешает скрипты, стили и изображения только с собственного источника (origin) приложения. Встроенных скриптов нет; темы являются таблицами стилей, а не встроенными стилями.
  • App::csp открывает политику для указанных хостов, по одной директиве за раз. Заголовок строится один раз при запуске.
App::new("Shop").csp(|c| {
    c.img_src(["https://images.example.com"])
        .frame_src(["https://www.youtube-nocookie.com", "https://*.vimeo.com"])
        .media_src(["https://media.example.com:8443"])
        .media_blob()
        .connect_src(["https://api.example.com"])
        .font_src(["https://fonts.example.com"])
})

Принимаются только https://host, https://host:port и https://*.host (подстановочный знак соответствует поддоменам, но не самому хосту). .media_blob() добавляет blob: в media-src для библиотек потоковой передачи. App::check отклоняет *, http:, голые схемы вроде data: или https:, ключевые слова в кавычках вроде 'unsafe-inline' или 'unsafe-eval', пути, имена пользователей и значения с пробелами, запятыми или точками с запятой; каждое сообщение об ошибке подсказывает, что написать вместо этого. Директивы script-src, object-src, base-uri и frame-ancestors изменить нельзя. Embed и Image читают те же списки, поэтому хост, который браузер заблокировал бы, никогда не отображается (см. Отображение).

  • Ответы содержат X-Content-Type-Options: nosniff, Referrer-Policy: same-origin и X-Frame-Options: DENY.
  • App::secure_cookies(true) помечает cookie как Secure; включайте это за HTTPS.

Экранирование

Пользовательские данные в HTML всегда проходят через render::escape. Компоненты экранируют каждое отображаемое значение, поэтому товар с названием <script> показывается как текст.

Лимиты

App::new(t!("app.name"))
    .rate_limit(RateLimit::per_ip(600, Duration::from_secs(60)))
    .write_limit(RateLimit::per_user(60, Duration::from_secs(60)))
    .trusted_proxies(["10.0.0.0/8"])
    .allow_only_ips(["10.0.0.0/8", "192.168.0.0/16"])
    .deny_ips(["203.0.113.7"])
  • rate_limit применяется к каждому запросу, кроме статических ресурсов и чтения публичных медиаисточников (Feature::public_media); write_limit добавляет более строгий лимит для вызовов, изменяющих данные: отправка форм, правка в таблицах, действия над строками, массовые действия и действия меню, команды, импорт, сохранённые представления таблиц, раскладки док-панелей и темы, вызовы API, вызовы MCP и ассистент. Отклонённые запросы получают 429 с Retry-After.
  • X-Forwarded-For принимается на веру только от trusted_proxies. Учитывается каждая строка заголовка по порядку, а берётся последний адрес, не являющийся доверенным прокси.
  • Раскрытия значений, вызовы островов (island) и источников данных тоже считаются записью, как и экспорт и печатные документы (GET /_zeyn/grid/export, GET /_zeyn/pdf).
  • Размеры страниц, число строк, длина запросов и размеры загрузок ограничиваются на сервере независимо от того, что отправляет клиент.

Сессии

App::idle_timeout(d) завершает сеанс пользователя на странице после периода без ввода, предварительно показав диалог с предупреждением. Действия с пометкой reauth требуют недавнего входа (App::reauth_within).

Защита данных

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

API и агенты

Вызовы API требуют заголовка Authorization; одной cookie браузерной сессии никогда не хватает для вызова API или конечной точки MCP, поэтому CSRF-токен им не нужен. Агенты получают ровно те разрешения, что и пользователь, от имени которого они действуют. См. API и OpenAPI и MCP и ИИ-агенты.