Enterprise редакция
SaaS — глубокий справочник
Краткий обзор
Заголовок раздела «Краткий обзор»Модуль Enterprise SaaS предоставляет многоарендные строительные блоки для сервиса на базе NextPDF.
TenantContext— неизменяемый value-объект идентичности, разрешаемый только из аутентифицированного контекста.ApiKeyGeneratorиApiKeyAuthenticatorвыпускают и проверяют API-ключи с префиксом, контрольной суммой и хранением в виде хеша.QuotaCheckerограничивает запросы по квотам на каждого арендатора: предупреждение на 80%, отклонение на 100%, защищённый отказ при неизвестном использовании.SidecarJwtMinterвыпускает краткосрочные служебные токены HS256 для вызовов между компонентами.UsageMeterиStripeMeteringSyncerполучают события использования и синхронизируют их с поставщиком биллинга с детерминированной идемпотентностью.
Доступность и лицензирование
Заголовок раздела «Доступность и лицензирование»Эта возможность поставляется в NextPDF Enterprise (nextpdf/enterprise) и активируется лицензионным конвертом уровня Enterprise. Развёртывание без этого права не загружает классы возможности. Сравните редакции и получите лицензию.
Поверхность SaaS — базовая возможность Enterprise; отдельного флага на каждую функцию нет. У NextPDF Core (Apache-2.0) и NextPDF Pro нет модели арендности, API-ключей или квот; у этой возможности нет эквивалента в более низком уровне.
composer require nextpdf/enterprise:^3Публичная поверхность API
Заголовок раздела «Публичная поверхность API»Все символы находятся в пространстве NextPDF\Enterprise\SaaS.
| Символ | Параметры | Поведение по умолчанию | Возвращает | Бросает или завершается с | Примечания |
|---|---|---|---|---|---|
TenantContext | string $tenantId, string $source, array $scopes = ['read'] | Неизменяемый value-объект идентичности | value-объект | Ничего | Источники: jwt, mtls, api_key; hasScope() / hasAnyScope() проверяют области |
TenantContext::singleTenant() | нет | Фиксированный арендатор default с read, write, admin | TenantContext | Ничего | Развёртывания с одним арендатором |
ApiKeyAuthenticator::authenticate() | string $rawKey | Шестишаговая проверка, затем разрешение контекста | TenantContext | ApiKeyAuthenticationException (HTTP 401) | Контекст source — api_key; области копируются из записи ключа |
ApiKeyAuthenticator::requireScope() | TenantContext $context, ApiKeyScope $requiredScope | Явное утверждение области | void | ApiKeyAuthenticationException::insufficientScope() (HTTP 403) | Проверка области — отдельный явный шаг |
ApiKeyGenerator::generateLive() / ::generateTest() | нет | Новый ключ: префикс, тело из 32 символов base62 (энтропия 192 бита), контрольная сумма из 4 символов | array{key, hash, prefix} | Ничего | Префиксы npf_live_ / npf_test_; hash — дайджест для хранения |
ApiKeyGenerator::validateChecksum() | string $key | Проверка формы префикса, длины и контрольной суммы CRC32 | bool | Ничего | Отбраковка опечаток до любого обращения к хранилищу данных; не средство безопасности |
ApiKeyGenerator::hashKey() (статический) | string $key | Шестнадцатеричный дайджест SHA-256 от сырого ключа | string | Ничего | Единственное сохраняемое представление ключа |
ApiKeyGenerator::isLiveKey() / ::isTestKey() | string $key | Проверка префикса | bool | Ничего | Среда видна без поиска |
ApiKey | id, арендатор, хеш ключа, отображаемый префикс, маска областей, моменты создания/истечения/отзыва | Сохранённая запись ключа; открытый текст никогда не сохраняется | value-объект | Ничего | isActive(), isRevoked(), isExpired(), scopeNames() |
ApiKeyScope | перечисление (backed enum): Read = 1, Write = 2, Admin = 4 | Модель областей на основе битовой маски | перечисление | Ничего | maskFromNames(), fromName(), fullAccess(); неизвестные имена игнорируются построителем маски |
ApiKeyRepositoryInterface | — | Контракт хранения; сохранение только хеша | — | Определяется реализацией | findByHash(), findActiveByTenant(), store(), revoke() |
SidecarJwtMinter::__construct() | string $secret, издатель, аудитория, int $ttlSeconds = 300 | Отклоняет подписывающий секрет короче 16 байт при конструировании | экземпляр | InvalidArgumentException | Нижний порог стойкости ключа 128 бит; рекомендуется 32 и более случайных байт |
SidecarJwtMinter::mint() | TenantContext $tenant | HS256 JWT с iss, aud, sub, scope, tenant_id, iat, exp, jti | string | JsonException при сбое кодирования утверждений | Время жизни по умолчанию пять минут; jti — 16 случайных байт в шестнадцатеричном виде |
QuotaChecker::check() | TenantContext $tenant, TenantQuota $quota | Читает текущее использование; предупреждает на 80%; отклоняет на 100%; отказывает при неизвестном использовании | array{allowed: bool, warning_percentage: float|null} | QuotaExceededException, QuotaUnavailableException | Коллбэк оповещения вызывается на обоих порогах |
TenantQuota | float $maxCuPerPeriod, коллекции, байты хранилища, параллельные задания | Лимиты на период; константа мягкого порога 80% | value-объект | Ничего | Значения по умолчанию fromConfig(): 10,000 CU, 100 коллекций, 10 GB, 10 заданий |
QuotaExceededException::toErrorEnvelope() | нет | Конверт ошибки SPEC-QUOTA-001 | array | — | HTTP 402, без повтора; несёт текущее значение, лимит и момент сброса |
QuotaUnavailableException::toErrorEnvelope() | нет | Конверт ошибки SPEC-QUOTA-503 | array | — | HTTP 503, с повтором; причина usage_undeterminable |
UsageMeter::pullUsage() | array<string, int> $watermarks | Опрашивает каждый настроенный хост-источник использования от его курсора | array{events, instance_id} | UsageMeterException, когда недоступны все хосты | Частичный сбой допустим; недоступные хосты журналируются и пропускаются |
UsageMeter::getCurrentUsage() | string $tenantId | Использование вычислительных единиц за текущий период | float | UsageMeterException, когда использование неопределимо | Разбираемый ноль авторитетен; неизвестное использование бросает исключение |
StripeMeteringSyncer::sync() | array<string, int> $watermarks | Один цикл: получение, преобразование, отправка | array{watermarks, sent, failed} | Ничего; сбои отправки направляются в коллбэк DLQ | Сбой получения возвращает холостой цикл, сохраняющий курсор |
StripeAdapter::sendMeterEvent() | MeterEvent $event | POST-запрос поставщику с заголовком идемпотентности | void | StripeSyncException | HTTP 429 и 5xx — с повтором; прочие 4xx — без повтора |
StripeAdapter::sendBatch() | list<MeterEvent> $events | Отправляет каждое событие; собирает сбои | list<StripeSyncException> | Ничего | Пустой список означает, что все события успешны |
MeterEvent | имя счётчика, арендатор, значение, ключ идемпотентности, временная метка | Неизменяемый value-объект события счётчика | value-объект | Ничего | toStripePayload() сериализует полезную нагрузку поставщика |
final readonly class ApiKeyAuthenticator{ public function __construct( private ApiKeyRepositoryInterface $repository, private ApiKeyGenerator $generator, private LoggerInterface $logger, ) {}
public function authenticate(string $rawKey): TenantContext {}
public function requireScope(TenantContext $context, ApiKeyScope $requiredScope): void {}}final class QuotaChecker{ public function __construct( private readonly UsageMeterInterface $usageMeter, private readonly LoggerInterface $logger, private readonly Closure $quotaAlertCallback, ) {}
/** @return array{allowed: bool, warning_percentage: float|null} */ public function check(TenantContext $tenant, TenantQuota $quota): array {}}interface UsageMeterInterface{ /** @return array<string, mixed> */ public function pullUsage(array $watermarks): array;
public function getCurrentUsage(string $tenantId): float;}final class StripeMeteringSyncer{ public function __construct( private readonly UsageMeterInterface $usageMeter, private readonly StripeAdapterInterface $stripeAdapter, private readonly LoggerInterface $logger, private readonly Closure $dlqCallback, ) {}
/** @return array{watermarks: array<string, int>, sent: int, failed: int} */ public function sync(array $watermarks): array {}}final readonly class SidecarJwtMinter{ public function __construct( private string $secret, private string $issuer = 'nextpdf-enterprise', private string $audience = 'nextpdf-spectrum', private int $ttlSeconds = self::DEFAULT_TTL_SECONDS, ) {}
public function mint(TenantContext $tenant): string {}}Контракт поведения
Заголовок раздела «Контракт поведения»- Идентичность арендатора. Контекст арендатора неизменяем: идентификатор арендатора, источник разрешения, области. Идентичность разрешается только из аутентифицированного контекста (
jwt,mtls,api_key) — никогда из заголовка или параметра запроса, предоставленного клиентом. Развёртывание с одним арендатором использует фиксированный контекстdefaultс полными областями. - Порядок аутентификации. Аутентификация по API-ключу выполняется в фиксированном порядке: контрольная сумма, хеш SHA-256, поиск в репозитории, проверка отзыва, проверка срока действия, разрешение контекста. Неизвестный, отозванный и истёкший ключи — три отдельных результата, все HTTP 401; недостаточная область — HTTP 403.
- Секретность ключа. Сырой ключ никогда не сохраняется и не журналируется; сохраняется и ищется только его дайджест SHA-256. Аутентификатор сам не выполняет побайтового сравнения секрета; поиск дайджеста за константное время — контракт реализации репозитория.
- Пороги квоты. На мягком лимите 80% запрос продолжается, возвращается процент предупреждения и срабатывает коллбэк оповещения. На жёстком лимите 100% запрос отклоняется с
SPEC-QUOTA-001(HTTP 402), несущим момент сброса — первый день следующего месяца, полночь по UTC. - Защищённый отказ квоты. Неопределимое использование отклоняет запрос с
SPEC-QUOTA-503(HTTP 503, с повтором). Неизвестное использование никогда не считается нулём. Настоящее, разбираемое нулевое использование авторитетно и допускает запрос. - Дедупликация оповещений. Контролёр не дедуплицирует оповещения; дедупликация за период — ответственность коллбэка.
- Синхронизация учёта. Цикл запланирован, никогда не на пути запроса. Он возобновляется с отметок для каждого источника и продвигает каждый курсор к идентичности самого позднего успешно отправленного события. Ключ идемпотентности детерминирован — арендатор, период, идентичность события — поэтому повторно отправленное событие схлопывается при дедупликации на стороне поставщика.
- Сбой получения. Сбойное получение возвращает холостой цикл (
sent0,failed0), сохраняющий отметки; следующий цикл повторяет то же окно, а не пропускает его. - Служебные токены. Токены — HS256 с общим секретом, несут
iss,aud,sub,scope,tenant_id,iat,expи уникальныйjti. Время жизни по умолчанию — пять минут. Конструирование отклоняет секрет короче 16 байт, защищённым отказом.
Граничные случаи и режимы сбоя
Заголовок раздела «Граничные случаи и режимы сбоя»- Некорректный ключ не проходит контрольную сумму и отклоняется до любого обращения к хранилищу данных. Корректный, но неизвестный ключ отклоняется после поиска. Оба проявляются как результат недействительного ключа.
- Неизвестный, отозванный и истёкший ключи используют отдельные фабрики исключений; флаг
keyExpiredистинен только для результата истечения. Сопоставьте их с отдельными ответами клиенту. QuotaChecker::check()возвращает значение только при допуске; возвращаемыйallowedвсегдаtrue. Отклонение и недоступность — исключительные результаты.TenantQuota::usagePercentage()возвращает0.0для неположительной квоты;fromConfig()подставляет значения по умолчанию для отсутствующих величин и ограничивает целочисленные лимиты снизу значением 1.- Отметки существуют для каждого источника; отсутствующая отметка начинает с начала потока этого источника (курсор
0). Многоисточниковое развёртывание поддерживает независимые отметки. - Преобразование пропускает события, не являющиеся массивом, события с отсутствующей или пустой операцией либо арендатором, неположительным значением или несопоставленной операцией — не приводя к сбою цикла. Событие без пригодной положительной целочисленной идентичности отклоняется с предупреждением: случайный запасной ключ свёл бы на нет дедупликацию на стороне поставщика и мог бы привести к двойному выставлению счёта арендатору.
- Десять последовательных сбоев отправки эскалируют до критической записи в журнале; счётчик сбрасывается при любой успешной отправке. Каждое сбойное событие всё равно достигает коллбэка недоставленных сообщений.
- Некорректное тело JSON от хоста-источника использования даёт пустой список событий, а не сбой цикла.
pullUsage()бросает исключение только когда недоступны все настроенные хосты.
Поведение в режиме FIPS
Заголовок раздела «Поведение в режиме FIPS»- Примитивы дайджеста и MAC — SHA-256 и HMAC-SHA256 через криптопровайдер PHP хоста. Сборка с ограничениями FIPS завершается защищённым отказом на неодобренном алгоритме, а не понижает уровень; слой SaaS не добавляет собственной криптографической политики.
- Тела ключей и идентификаторы токенов берутся из CSPRNG (
random_int(),random_bytes()). - Контрольная сумма CRC32 не является криптографическим средством и не зависит от режима FIPS.
Соответствие
Заголовок раздела «Соответствие»Приведённые ниже утверждения описывают возможности относительно цитируемых пунктов. Это не заявления о сертификации; у NextPDF нет сертификации для этого модуля.
| Поведение | Ссылка |
|---|---|
Семантика not-after exp служебного токена | RFC 7519 §4.1.4 |
| Компактная сериализация JWS служебного токена | RFC 7515 §3.1 |
| Нижний порог секрета HS256 16 байт; никаких запоминаемых человеком паролей в качестве MAC-ключей | RFC 8725 §3.5 (threat: §2.2) |
| Контракт поиска дайджеста в репозитории за константное время | OWASP ASVS 5.0 §11.2.4 |
| Дайджест хранения API-ключа SHA-256 | FIPS 180-4 (code-declared) |
Цитаты RFC 8725 и OWASP ASVS 5.0 проверены по RAG; полные идентификаторы ссылок записаны во frontmatter этой страницы. Ссылки FIPS 180-4, FIPS 198-1 и BSI TR-02102-1 объявлены в коде исходников продукта (hash('sha256', …) и задокументированный нижний порог ключа в минтере); они не извлекались из корпуса RAG для этой страницы. Требование константного времени ASVS §11.2.4 связывает реализацию репозитория, которую предоставляет оператор, а не сам класс аутентификатора.
Заметки для разработки
Заголовок раздела «Заметки для разработки»- Предоставьте надёжные реализации
ApiKeyRepositoryInterfaceиStripeAdapterInterface; пакет поставляет контракты и клиент поставщика PSR-18, а не хранилище. - Зависимости — только абстракции PSR: логгер PSR-3, HTTP-клиент PSR-18, фабрики запросов и потоков PSR-17. SDK поставщика не требуется.
- Запускайте синхронизацию учёта как запланированное задание. Надёжно сохраняйте возвращаемые отметки после каждого цикла.
- Передавайте клиентам процент предупреждения квоты, например как заголовок предупреждения, и дедуплицируйте оповещения квоты за период в коллбэке.
- Задавайте секрет минтера токенов из конфигурации как случайное значение с высокой энтропией; рекомендуется 32 и более случайных байт. Никогда не выводите его из пароля.
- Префиксы ключей делают среду видимой без поиска; ключи sandbox и production никогда не сталкиваются, потому что префикс участвует в сохранённом дайджесте.
- Детали внутреннего механизма остаются во внутренней документации репозитория исходников и выходят за рамки этого руководства.
Граница публикации
Заголовок раздела «Граница публикации»Эта страница документирует только внешне наблюдаемое поведение и поддерживаемую публичную поверхность API. Внутренние пути пространств имён, вспомогательные классы, таблицы механизмов, имена файлов runbook и префиксы тикетов выходят за рамки.