Перейти к содержанию

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:

identity (IAM) → revocation → entitlement → policy → транзакционные гейты сервиса
Сервис Отвечает Не отвечает
iam-service кто это, какие scopes лицензирован ли продукт
entitlement-service лицензирован ли продукт/feature, хватает ли квоты и мест доступ к задаче, namespace памяти и т. п.
resource service доменная политика и гейты цена и биллинг

Отзыв лицензии не отзывает identity, а отзыв identity не удаляет лицензию tenant'а. Биллинга и расчёта цены в сервисе нет.

Развёртывание

Сервис compose Назначение
entitlement-db PostgreSQL 16, база entitlement
entitlement-service API, порт 8020
make up PROFILES="core entitlement edge"

Порт на хосте — 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

См. также