Entitlement Service¶
entitlement-service отвечает на один вопрос: может ли subject в tenant'е
использовать продукт или feature в заданном количестве и временном окне.
Это коммерческое право (лицензия, план, места, квоты), а не доступ к
конкретному ресурсу. Статья описывает модель лицензий, API, строгие квоты,
подписанные projection и подключение Control Plane.
Заморожен
Профиль compose entitlement заморожен (TAI-ADR-0034 п.4): сервис
работает и исправляется при поломке, но новых функций не получает.
По умолчанию Control Plane лицензии не проверяет
(CP_ENTITLEMENT_ENABLED=false) — в его audit это видно как источник
решения disabled, а не как пропущенная проверка.
Граница ответственности¶
Порядок проверок в resource service фиксирован platform-auth-sdk:
| Сервис | Отвечает | Не отвечает |
|---|---|---|
| iam-service | кто это, какие scopes | лицензирован ли продукт |
| entitlement-service | лицензирован ли продукт/feature, хватает ли квоты и мест | доступ к задаче, namespace памяти и т. п. |
| resource service | доменная политика и гейты | цена и биллинг |
Отзыв лицензии не отзывает identity, а отзыв identity не удаляет лицензию tenant'а. Биллинга и расчёта цены в сервисе нет.
Развёртывание¶
| Сервис compose | Назначение |
|---|---|
entitlement-db |
PostgreSQL 16, база entitlement |
entitlement-service |
API, порт 8020 |
Порт на хосте — 127.0.0.1:${ENT_HOST_PORT:-18020}; через Caddy наружу не
публикуется. Образ собирается из корня суперпроекта
(entitlement-service/Dockerfile).
| Переменная | Значение в compose | Назначение |
|---|---|---|
ENT_DATABASE_URL |
postgresql+psycopg://entitlement:…@entitlement-db:5432/entitlement |
база |
ENT_BOOTSTRAP_TOKEN |
${ENT_BOOTSTRAP_TOKEN} |
заголовок X-Entitlement-Bootstrap-Token для каталога и лицензий |
ENT_ISSUER |
${TAIMEN_PUBLIC_URL}/entitlement |
issuer подписанных projection |
ENT_IAM_ISSUER |
${TAIMEN_PUBLIC_URL}/iam |
issuer токенов IAM |
ENT_IAM_JWKS_URL |
http://iam-service:8010/.well-known/jwks.json |
ключи IAM (или ENT_IAM_PUBLIC_KEY[_FILE]) |
ENT_IAM_AUDIENCE |
entitlement-service |
принимаемый audience |
ENT_SIGNING_PRIVATE_KEY_FILE |
/run/secrets/entitlement_signing_key |
RSA-ключ подписи projection (secrets/entitlement-signing.pem, создаёт make secrets) |
ENT_SIGNING_KEY_ID |
local-dev |
kid в JWKS |
ENT_PROJECTION_TTL_SECONDS |
300 |
TTL projection |
ENT_PROJECTION_MAX_TTL_SECONDS |
900 |
верхняя граница TTL |
ENT_RESERVATION_TTL_SECONDS |
120 |
срок жизни резерва квоты |
Смените bootstrap-токен
Если ENT_BOOTSTRAP_TOKEN не задан, compose подставляет значение
разработки. make secrets генерирует случайный токен; в промышленной
инсталляции убедитесь, что он задан. Bootstrap-заголовок нельзя
проксировать внешним клиентам — это временная граница администрирования.
Ключ подписи читается контейнером под непривилегированным uid; на Linux
выдайте файлу владельца 10001 при правах 600.
Модель¶
flowchart LR
P[Product] --> F["Feature (boolean / quota / seat)"]
P --> PL[Plan]
PL -->|"entitlements: feature, limitValue, period"| F
T[Tenant] --> G["License Grant: product + plan, seats, окно действия"]
G --> PL
G --> S[Seat Assignment: subject]
G --> Q[Quota counter: reserved, consumed]
| Сущность | Описание |
|---|---|
| Product | продукт поставки, ключ ^[a-z0-9][a-z0-9._-]{1,118}[a-z0-9]$ |
| Feature | boolean (есть/нет), quota (счётчик), seat (именное место) |
| Plan | набор entitlements: feature, limitValue, period = lifetime или monthly |
| License Grant | лицензия tenant'а: план, число мест, validFrom/validUntil, внешняя ссылка на SKU, статус |
| Seat Assignment | место, закреплённое за subject'ом |
| Quota counter | авторитетный счётчик квоты: reserved, consumed, version |
policyVersion tenant'а монотонно растёт при изменении его лицензий;
usageSnapshotVersion — при изменении счётчика.
API¶
Каталог и лицензии (bootstrap)¶
Все запросы — с заголовком X-Entitlement-Bootstrap-Token.
| Метод и путь | Что делает |
|---|---|
POST /api/v1/products |
{key, name} |
POST /api/v1/products/{productKey}/features |
{key, kind, name} |
POST /api/v1/products/{productKey}/plans |
{key, name, entitlements: [{feature, limitValue?, period?}]} |
POST /api/v1/tenants/{tenantId}/license-grants |
{product, plan, seats?, validFrom?, validUntil?, externalRef?} |
POST /api/v1/tenants/{tenantId}/license-grants/{grantId}:revoke |
отозвать лицензию |
POST /api/v1/tenants/{tenantId}/license-grants/{grantId}/seats |
{subjectId} — выдать место |
POST /api/v1/tenants/{tenantId}/license-grants/{grantId}/seats/{subjectId}:release |
освободить место |
GET /api/v1/events |
журнал событий |
Пример: лицензия на Control Plane с квотой запусков.
ENT=http://127.0.0.1:18020
H="X-Entitlement-Bootstrap-Token: $ENT_BOOTSTRAP_TOKEN"
curl -s -X POST $ENT/api/v1/products -H "$H" -H 'Content-Type: application/json' \
-d '{"key":"control-plane","name":"Control Plane"}'
for f in tasks runs harness events; do
curl -s -X POST $ENT/api/v1/products/control-plane/features -H "$H" \
-H 'Content-Type: application/json' -d "{\"key\":\"$f\",\"kind\":\"boolean\",\"name\":\"$f\"}"
done
curl -s -X POST $ENT/api/v1/products/control-plane/plans -H "$H" -H 'Content-Type: application/json' \
-d '{"key":"standard","name":"Standard","entitlements":[
{"feature":"tasks"},{"feature":"runs"},{"feature":"harness"},{"feature":"events"}]}'
curl -s -X POST $ENT/api/v1/tenants/$IAM_TENANT_ID/license-grants -H "$H" \
-H 'Content-Type: application/json' \
-d '{"product":"control-plane","plan":"standard","validUntil":"2027-01-01T00:00:00Z"}'
Решения (IAM-токен)¶
Вызываются resource service'ом с access token IAM audience
entitlement-service. Tenant — из токена (значение клиента игнорируется);
subject по умолчанию — sub токена, решение о другом subject требует scope
entitlement:check-on-behalf, иначе 403.
| Метод и путь | Что делает |
|---|---|
POST /api/v1/decisions:check |
решение по {product, feature, subjectId?, requiredAmount?, correlationId?} |
POST /api/v1/quota:reserve |
резерв квоты {product, feature, subjectId?, amount, idempotencyKey} |
POST /api/v1/quota/{reservationId}:consume |
списать резерв |
POST /api/v1/quota/{reservationId}:release |
вернуть резерв |
POST /api/v1/usage-events |
прямое списание {product, feature, subjectId?, eventId, amount} |
POST /api/v1/projections:issue |
подписанный снимок решений |
GET /.well-known/jwks.json |
ключи подписи projection |
Ответ решения:
{
"decisionId": "…",
"tenantId": "<tenant-id>",
"subjectId": "<principal-id>",
"product": "control-plane",
"feature": "tasks",
"allowed": false,
"reason": "license_expired",
"limits": {},
"usageSnapshotVersion": 3,
"policyVersion": 7,
"validFrom": "…",
"validUntil": "…"
}
Стабильные причины отказа: unknown_product, unknown_feature,
no_license_grant, license_not_yet_valid, license_expired,
license_revoked, license_suspended, feature_not_in_plan,
subject_required, seat_not_assigned, quota_exhausted.
Нет или неверен токен — 401; публичный ключ IAM не сконфигурирован — 503
(сервис закрывается, а не пропускает).
Строгая квота¶
reserve → reserved += amount (атомарно, с проверкой предела)
consume → reserved -= amount, consumed += amount
release → reserved -= amount
- Переход счётчика — один условный
UPDATEс проверкойversionи предела: конкурирующие резервы не превысят лимит. - Резерв идемпотентен по
idempotencyKeyв границах tenant'а: повтор возвращает исходный резерв. - Просроченный резерв (
ENT_RESERVATION_TTL_SECONDS) при попыткеconsumeпереходит вexpired, количество возвращается в счётчик. - Прямой usage event идемпотентен по
eventId: ответ несётduplicate: trueдля повтора.
Подписанные projection¶
POST /api/v1/projections:issue выдаёт RS256-снимок уже принятых решений
для ограниченной работы без онлайн-вызова. TTL — не больше
ENT_PROJECTION_MAX_TTL_SECONDS и не дольше validUntil лицензии. Снимок
несёт policyVersion и usageSnapshotVersion: проверяющая сторона
отбрасывает снимок старше известных ей версий. Устаревший снимок не
расширяет набор features и не увеличивает лимиты; для отзыва лицензии
действует онлайн-путь.
Журнал и audit¶
Доменное изменение, событие outbox и запись audit пишутся одной
транзакцией. Семейства событий: license_grant.*, seat_assignment.*,
quota.*, entitlement_policy.published, создание элементов каталога.
Токены, bootstrap-токен и подписанные projection в события не попадают.
Фоновой доставки во внешний брокер нет — журнал читается через
GET /api/v1/events.
Подключение Control Plane¶
| Переменная Control Plane | По умолчанию | Назначение |
|---|---|---|
CP_ENTITLEMENT_ENABLED |
false |
включить проверку лицензий |
CP_ENTITLEMENT_BASE_URL |
в compose http://entitlement-service:8020 |
адрес сервиса |
CP_ENTITLEMENT_PRODUCT |
control-plane |
ключ продукта |
CP_ENTITLEMENT_AUDIENCE |
entitlement-service |
audience токена service account'а |
CP_ENTITLEMENT_DEFAULT_FEATURE |
api |
feature для путей, не разобранных по правилу |
CP_ENTITLEMENT_CACHE_TTL_SECONDS |
30 |
кэш положительного решения |
CP_ENTITLEMENT_DEGRADED_MAX_AGE_SECONDS |
300 |
сколько работать на прошлом решении при недоступности сервиса |
CP_ENTITLEMENT_TIMEOUT_SECONDS |
3 |
таймаут вызова |
Feature выводится из пути запроса: /api/v1/<feature>/… — второй
сегмент после api (/api/v1/tasks/... → tasks, /api/v1/harness/context
→ harness). Пути, не подходящие под шаблон, проверяются по
CP_ENTITLEMENT_DEFAULT_FEATURE. Поэтому в плане продукта должны быть все
features, соответствующие используемым областям API, иначе запросы получат
отказ unknown_feature или feature_not_in_plan.
Control Plane спрашивает entitlement своим service account'ом от имени
пользователя: нужен audience entitlement-service со scope
entitlement:check-on-behalf (bootstrap заводит его в потолке service
account'а ядра) и CP_IAM_CLIENT_ID/CP_IAM_CLIENT_SECRET в окружении — без
них включённая проверка не стартует.
| Ситуация | Ответ Control Plane |
|---|---|
| лицензии нет или она не покрывает feature | 403 not_entitled |
| сервис недоступен, кэш старше degraded-окна | 503 entitlement_unavailable |
| сервис недоступен, решение в пределах окна | запрос проходит, решение помечено degraded |