SaaS для enterprise: квоты, фичефлаги, SSO, audit log
Афиша хранит тенантов порознь и собирает с них деньги. Однажды в отдел продаж приходит сеть кинотеатров на пятьсот сотрудников, и сделка начинается не с демо продукта, а с анкеты: опросник на сотню пунктов от службы безопасности клиента. Есть ли SSO через наш корпоративный каталог? Кто и как увольняет сотрудника из вашей системы? Ведёте ли журнал действий и отдадите ли его аудитору? Как выгрузить и удалить наши данные? Этот урок про четыре функции, которых нет на лендинге, но без которых enterprise-контракт не подписывается. И про их общую черту: все четыре дешевле заложить в архитектуру заранее, чем вкручивать под дедлайн сделки.
Entitlements: что можно этому тенанту
План подписки из прошлого урока это цена и период. Но продукту от плана нужно другое: ответ на вопрос, что конкретно этому тенанту можно. Этот ответ оформляют как entitlements, явную структуру с правами:
type Entitlements = {
seats: number;
projectsMax: number;
apiRps: number;
integrations: string[];
sso: boolean;
};
const PLAN_ENTITLEMENTS: Record<PlanId, Entitlements> = {
free: { seats: 3, projectsMax: 1, apiRps: 5, integrations: [], sso: false },
pro: { seats: 25, projectsMax: 50, apiRps: 50, integrations: ['slack'], sso: false },
enterprise: { seats: 500, projectsMax: 1000, apiRps: 500, integrations: ['slack', 'jira'], sso: true },
};
Главное правило: код проверяет entitlement, а не план.
if (entitlements.sso), никогда if (plan === 'enterprise').
Причина в том, как живут продажи: появится кастомная сделка (про SSO
на pro-плане за доплату), появятся grandfathered-клиенты на давно
закрытом тарифе, появится per-tenant override (включили лимит повыше
конкретному клиенту, пока тот решает про апгрейд). План меняется
редко и живёт в биллинге; права читаются на каждый запрос и живут в
своей структуре: план лишь выбирает дефолтный набор, поверх которого
ложатся overrides тенанта.
Раз права читаются на каждый запрос, в hot path им нужен кэш: снапшот entitlements тенанта в Redis или в памяти процесса с коротким TTL, инвалидация по событию смены плана (которое, как мы теперь знаем, приедет webhook-ом от биллинга). Поход в Postgres за правами на каждый запрос это самодельный noisy neighbour, только теперь жертва ты сам.
Вторая ось: что делать при превышении. Hard limit блокирует операцию: тридцать первый проект на плане с тридцатью не создаётся, API сверх частоты получает 429. Soft limit пропускает операцию и пишет превышение в metering из прошлого урока: enterprise-клиент, у которого в горячий день продаж отвалилась интеграция из-за жёсткой квоты, это испорченные отношения, а overage в счёте это выручка. Практичная конвенция: частотные лимиты и free-план держат hard, объёмные лимиты платящих клиентов держат soft с overage. Сами частотные лимиты реализуются token bucket per tenant: ведро в Redis на ключ из тенанта и группы эндпоинтов, ёмкость из entitlements. И поверх любых планов живёт fair-use потолок: глобальный предел, защищающий платформу от аномалии или злоупотребления, какой бы тариф ни был куплен.
Feature flags: рубильники поверх кода
Entitlements отвечают, что можно тенанту по контракту. Рядом живёт похожий, но другой механизм: feature flag, рубильник, который меняет поведение системы без деплоя. Сходство обманчиво, и чтобы не свалить всё в одну кучу, флаги принято делить на четыре типа с разной судьбой:
| Тип | Зачем | Кто переключает | Живёт |
|---|---|---|---|
| Release | выкатить фичу постепенно: 1%, 10%, все | разработчик | недели |
| Ops | kill-switch: выключить подсистему при инциденте | дежурный | годы |
| Permission | открыть фичу плану или тенанту | продукт, продажи | годы |
| Experiment | A/B-тест с метрикой | продукт | до результата |
Release-флаг это страховка деплоя: код уехал на прод выключенным, включается на процент трафика, при проблеме выключается за секунды вместо отката релиза. Ops-флаг это рубильник дежурного, родственник circuit breaker из урока про resilience, только дёргает его человек: рекомендации легли, выключили блок рекомендаций, страница живёт дальше. Permission-флаг это entitlement под другим именем, и зрелые системы их сращивают: долгоживущие права уезжают в entitlements, флаговая система остаётся для короткоживущего. Experiment-флаг делит трафик ради метрики, и у него есть жёсткое требование: стабильность ветки для пользователя (один и тот же пользователь всегда в одной группе).
Архитектурных правила два. Первое: решение принимает сервер. SDK на клиенте получает уже вычисленные значения флагов; клиент, который сам решает по конфигу, можно убедить решить иначе, и для permission-флагов это дыра в монетизации, если не в безопасности. Второе: у каждого флага есть владелец и дедлайн. Главный накопленный долг флаговых систем: кладбище release-флагов, выкаченных на сто процентов два года назад. Каждая ветка мёртвого флага это код, который никогда не исполняется, но который все читают и тестируют. Поэтому флаг заводится с датой удаления, реестр флагов ревьюится, а просроченный флаг превращается в задачу (или сразу в автоматический PR) на удаление. Инструменты: LaunchDarkly, Unleash, GrowthBook, Flagsmith; для старта хватает таблицы в своей базе с кэшем, важна не платформа, а дисциплина жизненного цикла.
Enterprise SSO: вход через чужую дверь
У сети кинотеатров пятьсот сотрудников, и её IT-отдел не будет заводить пятьсот аккаунтов в твоей афише руками. У него уже есть IdP, и требование звучит так: сотрудники входят через корпоративный каталог, а уволенный сотрудник теряет доступ ко всем SaaS в момент увольнения. Для тебя это значит поддержать два протокола входа и один протокол синхронизации (модели авторизации мы разбирали в уроке про auth, здесь надстройка над ними).
SAML 2.0 это ветеран: XML-документы, которые браузер пользователя переносит от IdP к тебе (SP-initiated flow, когда вход начался у тебя, и IdP-initiated, когда сотрудник кликнул плитку в корпоративном портале), подпись по X.509-сертификату, обмен настройками через XML-метаданные. Протокол старый и неудобный, но это родной язык Okta, ADFS и Microsoft Entra, поэтому из enterprise-требований он никуда не делся. OIDC его современный наследник: JSON и JWT вместо XML, автоматическое обнаружение конфигурации, проверка подписи по публикуемым ключам. Если IdP клиента умеет OIDC, выбирай его; SAML поддерживай потому, что у половины клиентов выбора нет. И в обоих случаях: не пиши протокольный слой сам. У SAML богатая история уязвимостей разбора XML и подделки подписей, бери проверенную библиотеку или SSO-брокера, который отдаст тебе обе аббревиатуры одним API.
Вход решает аутентификацию, но не жизненный цикл: кто создаёт
аккаунты и, главное, кто их удаляет? Минимальный ответ:
JIT-provisioning,
аккаунт рождается при первом входе. Полный ответ:
SCIM 2.0,
REST-эндпоинты /Users и /Groups, которые дёргает сам IdP:
наняли сотрудника, у тебя появился аккаунт; уволили, аккаунт
деактивировался в течение минут, без участия людей. Заметь, что
deprovisioning здесь главная часть: автоматическое создание это
удобство, автоматическое удаление это безопасность, и именно про
него спрашивает та самая анкета.
Последний кусочек: как вообще понять, что входящий
anna@cinema-park.ru должен идти через IdP кинотеатров? IdP
discovery по домену email: домен привязан к тенанту, тенант знает
свой IdP, форма логина после ввода email редиректит на нужный вход.
Это ровно тот же приём, что определение тенанта по поддомену из
урока про tenancy, только ключом
служит домен почты.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.