Доступ сервисов и жизненный цикл API-ключей
Статус: целевая модель с реализованным ограниченным профилем, 2026-09-24. Работают managed keys и делегирование на точные аккаунты: 24 repository permissions (repository.read, 18 data actions и пять действий удаления, продвижения, хранилища и диагностики: artifact.delete, artifact.promote, storage.read, storage.manage, diagnostics.read), семь отдельных admin actions, ceiling, activation/rotation/revoke, CAS, SDK и проверки перед commit. Широкие system permissions, рекурсивное делегирование, notBefore, импорт старых identities и namespace selectors остаются проектом. Текущие лимиты и отличия задают runbooks. Карта API, план, ADR 0016, принятый профиль ADR 0019.
1. Совместимый прежний профиль
Сервисный файл содержит SHA-256 ключа, стабильный id принципала, список repositories, permissions: read|write, необязательный administrator. Один набор permissions применяется ко всем перечисленным репозиториям. Ключи загружаются при старте; API выдачи/отзыва, срока действия и отдельного key ID нет. Изменение файла требует согласованного перезапуска API/readers/worker. Несколько ключей одного id используют общий владелец uploads и сетевой бюджет.
Пользовательские сессии — отдельный механизм: учётные записи и точные grants групп хранятся в PostgreSQL, пересчитываются при каждом запросе. administrator позволяет управлять пользователями/группами, но не даёт автоматического доступа к байтам. write не подразумевает read; часть операций каталога сейчас требует оба права. Источник фактических проверок — application use cases, не название HTTP-метода.
2. Целевая модель
| Сущность | Назначение |
|---|---|
| ServiceAccount | Стабильный принципал отдельного сервиса и окружения: build-agent-prod, release-reader, catalog-indexer. Владеет uploads, jobs, references и общим бюджетом всех своих ключей. |
| ApiKey | Секрет для аутентификации, keyId, serviceAccountId, ограничения, состояние, срок, автор выдачи. Ротация ключа не меняет владельца данных. |
| Policy / binding | Список точных действий над ресурсами. Один binding связывает actions с одним selector; нельзя перемножать actions и ресурсы разных bindings. |
| Role template | Удобный именованный набор permissions. При назначении раскрывается в версионированную policy; изменение шаблона не расширяет уже выданные права. |
| AuthorizationContext | Внутренний результат проверки: тип принципала, subject ID, credential ID, ревизии policy, эффективные ограничения. Не сериализуется как секрет или публичная модель БД. |
Разделяем сервисные аккаунты, пользовательские сессии и будущие краткоживущие допуски передачи. Постоянный сервисный ключ нельзя встраивать в публичный браузер или распространяемый агент всем пользователям. Интеграция получает собственную identity; разделение на окружения задаётся отдельными аккаунтами и репозиториями.
Первая версия не вводит tenant isolation: репозиторные права не являются обещанием многопользовательского SaaS для независимых организаций. Такая изоляция потребует отдельной модели, ключей уникальности и тестов во всех запросах/очередях.
3. Правило принятия решения
Для каждой пары (action, canonicalResource) необходимы одновременно:
- Активные аккаунт и credential, наступивший
notBefore, не истёкшийexpiresAt, отсутствие отзыва. - Подходящий binding актуальной policy аккаунта.
- Подходящий binding ограничений конкретного ключа. Отсутствующий или пустой список запрещает всё, не означает «унаследовать всё».
- Выполнение обязательных условий: роль gateway, владелец upload/job/reference, состояние объекта, repository policy и квоты.
Permissions аккаунта и ключа пересекаются по действию и одному ресурсу, а не только по строковым именам actions. Нет подходящего allow — отказ. Wildcard actions, wildcard repositories, deny-исключения и произвольные выражения policy не входят в первую реализацию. Новое разрешение по умолчанию не выдаётся никому. Сервер не доверяет роли, owner или scope из тела запроса.
Разрешения не образуют числовую лестницу «уровень 1–5»: Publisher не старше Auditor. UI показывает понятные роли, сервер проверяет явные permissions. Глобальное управление и доступ к содержимому независимы.
4. Каталог permissions
Имена ниже — целевые стабильные идентификаторы. Они должны появляться в contracts и SDK только вместе с работающим use case. Наличие строки в документе не включает функцию сервера.
| Permission | Ресурс и смысл |
|---|---|
repository.read | Карточка разрешённого репозитория и его доступные возможности. |
artifact.read | Descriptor доступного артефакта; не сами байты. |
artifact.list | Обход артефактов и поиск; выдаёт только разрешённые объекты. |
content.read | Байты, HEAD и Range конкретного опубликованного объекта. |
upload.create | Резерв загрузки в репозитории в пределах квоты. |
upload.read | Состояние/части собственной загрузки. |
upload.write | Запись частей и whole-file PUT собственной загрузки. Whole-file PUT также публикует blob и требует upload.complete. |
upload.complete | Сборка/публикация собственной загрузки, включая async enqueue. |
upload.cancel | Отмена собственной неопубликованной загрузки. |
job.read | Состояние собственного задания с проверкой его репозитория. |
package.read | Manifest, идентичность и список доступных UPack. |
package.publish | Регистрация UPack после проверки исходного blob и manifest. |
asset.read | Список путей, текущий pointer и история разрешённых assets. |
asset.write | Создание или CAS-изменение pointer, с проверкой исходного artifact. |
asset.restore | Новая ревизия из прежней, с проверкой исходной ревизии и blob. |
annotation.read / annotation.write | Чтение / CAS-изменение метаданных, labels и collections. Редактирование не заменяет разрешение на скачивание. |
reference.write | Создание/снятие references своего аккаунта. Чужие ссылки не удаляются. |
audit.read | Аудит разрешённого репозитория; персональные и служебные поля определены отдельно. |
artifact.delete | Логическое удаление, retention и включение очистки. Только managed key, не часть Publisher. |
artifact.promote | Продвижение артефакта между репозиториями; прежним принципалам — при read+write. |
storage.read / storage.manage | Чтение / изменение политик хранения и очистки. Только managed key. |
diagnostics.read | События хранения и очистки репозитория. Только managed key. |
retention.manage | Будущая отдельная политика хранения. |
event.read / webhook.manage | Будущие журнал событий / API подписок в разрешённом репозитории. Вебхуки уже доставляются из файла оператора (ADR 0069) без этого права. |
identity.read / identity.manage | Глобальное чтение / управление пользователями и группами. |
service-account.read / service-account.manage | Глобальное чтение / создание и отключение машинных identities. Не даёт права назначать data permissions. |
credential.read / credential.manage | Метаданные / выпуск, ротация, отзыв ключей конкретного service account. Только в явно заданной области аккаунтов. |
policy.read / policy.manage | Чтение / изменение bindings. Для расширения действует отдельный предел делегирования. |
system.observe | Диагностика, будущие метрики и состояние gateways; без secrets и data access. |
auth/me, чтение собственной effective policy и OpenAPI доступны любой действующей identity без выдачи data permissions. Публичная liveness остаётся минимальной. Ограничение подробной readiness через system.observe не вводится незаметно для старых health-клиентов: отдельная версия/профиль, описанный в плане миграции.
5. Роли для интеграций
| Шаблон | Выдаваемые права |
|---|---|
| Downloader | repository.read, artifact.read, content.read, package.read, asset.read. Для прямой ссылки на известный artifact допустим ещё более узкий ключ с content.read. |
| CatalogReader | repository.read, artifact.read, artifact.list, package.read, asset.read, annotation.read; байты не выдаются. |
| Publisher | repository.read, artifact.read, upload.create/read/write/complete/cancel, job.read, package.publish, asset.write; запись формата upload.create/read/... в этой таблице означает отдельные permissions. Историю/скачивание и аннотации добавлять только при потребности. |
| MetadataEditor | artifact.read, annotation.read/write; не изменяет bytes, версии и pointers. |
| Auditor | repository.read, audit.read; не публикует и не скачивает. |
| Observer | system.observe; нет доступа к каталогу и байтам. |
| IdentityOperator | Отдельно назначенные identity.*, service-account.*, credential.*; policy delegation и data access не добавляются автоматически. |
В первой реализации selector — точный repository ID или точный service account ID для credential management; системные actions имеют явный system selector. В одном binding нельзя смешать несовместимые типы ресурса/actions.
Будущий selector группы пакетов/пути вводится только после сквозных тестов из раздела 8. До этого изоляция разных сервисов внутри каталога достигается отдельными репозиториями, а не неподдерживаемыми префиксами в ключе.
6. Жизненный цикл ключа
Проектируемый формат: arkvory_<keyId>.<secret>, где secret — 32 случайных байта в base64url без padding. Prefix — только идентификация формата. Храним hash полного credential, сравниваем constant-time, lookup по несекретному key ID; сырой secret доступен только в ответе выдачи. SHA-256 применим к случайному секрету высокой энтропии; пользовательские пароли продолжают использовать scrypt. Hash и внутренние поля проверки не попадают в list/get API.
Состояния: pending → active → revoked; expiresAt делает pending/active непригодным независимо от работы фоновой очистки. Revoked необратим. Отключение аккаунта блокирует все его credentials без их удаления. Audit/tombstone сохраняются по политике эксплуатации.
Проектные пределы первого инкремента: 1000 service accounts, не более 3 активных и 2 pending ключей на аккаунт, 64 bindings и 64 actions на policy, control JSON до 64 KiB. Активный срок по умолчанию 90 дней, максимум 365; pending активация до 15 минут. Это новые предлагаемые лимиты, не настройки текущего runtime; расширять их можно после измерений. Истёкшие/revoked метаданные имеют отдельный лимит/архивирование, чтобы lifetime-история не росла бесконечно.
Выдача и потерянный ответ:
- Оператор вызывает
POST /api/v1/service-accounts/{id}/keysсIdempotency-Key, названием, явными bindings и сроком; получает201, key metadata и один secret. Заголовок ответаCache-Control: no-store. - Новый клиент вызывает
POST /api/v1/auth/activate-keyс этим pending Bearer. Только этот маршрут принимает pending credential; он повторно проверяет актуальную policy и возвращает204. Повтор активным ключом —204без расширения прав/продления срока. - Повтор выдачи с тем же idempotency key и телом возвращает metadata существующего pending key (
200, без secret); другое тело —409. Сервер не хранит secret для replay. При потере ответа оператор отзывает этот key и создаёт новый с другим idempotency key; pending также истечёт автоматически.
Ротация создаёт новый pending key того же аккаунта с ограничениями не шире старого. Активация нового не отключает старый автоматически: сначала клиент проверяет запросы, затем оператор отзывает старый. План ротации задаёт дедлайн перекрытия не более 24 часов; при потере нового секрета старый не выключается раньше согласованного дедлайна. Ротация, activation и смена policy сериализуются на credential/account revision. Idempotency receipt содержит key ID и fingerprint запроса, не секрет.
Управление чужими ключами не означает право их использовать. credential.manage выпускает ключ только в пересечении effective policy целевого аккаунта и явно делегируемых bindings вызывающего оператора. policy.manage ограничено тем же delegation ceiling; владелец/оператор не может сам изменить этот ceiling. Первый root/bootstrap выдаётся локальной процедурой оператора, вне публичного API. В первой версии machine keys не делегируют permissions по умолчанию, нет цепочек самовоспроизводящихся ключей. Восстановление после утраты последнего администратора — локальная, журналируемая процедура, без публичного backdoor.
7. Отзыв, фоновые операции и квоты
Первый managed-key backend проверяет credential/account/policy по authoritative PostgreSQL при каждом новом запросе, без положительного cross-request auth cache. Недоступность authority закрывает новые managed-key запросы. Не читать права с асинхронной реплики с неизвестным lag. Это добавляет зависимость от БД, которую нужно резервировать вместе с содержимым.
После фиксации отзыва новые запросы запрещены. Запрос, прошедший проверку до отзыва, считается начатым; нельзя обещать прерывание уже отправленных байтов. Первый инкремент сохраняет текущую границу начатого download. Мутации повторно проверяют policy/credential в короткой транзакции публикации; отзыв и commit сериализуются на соответствующих ревизиях. Нельзя держать такую блокировку на протяжении 5 GiB передачи.
Job хранит stable subject ID, initiating credential ID, revision и требуемые action/resource; worker заново проверяет их при исполнении и непосредственно перед публикацией. Ревизия enqueue — аудит, не вечное разрешение. При отзыве initiating key задание не публикуется; новый ключ того же аккаунта может явно переавторизовать enqueue после проверки. Сохранённые parts принадлежат аккаунту, ротация не теряет их. Повторный enqueue не создаёт вторую публикацию.
Квоты суммируются на service account, repository и gateway/site; отдельные ключи только сужают лимит. Ротация и создание дополнительных credentials не дают дополнительную полосу. priority выбирается из разрешённого сервером класса, клиент не задаёт произвольный вес. HTTP requests/s, одновременные transfers, bytes/s и резерв staging — разные ограничения. Distributed leases не разрешают превышение общего бюджета при потере координатора; текущие фиксированные shares остаются до отдельной реализации scheduler.
8. Условия для ограничений по папке и группе
Это следующий этап, не параметр первого managed-key выпуска:
- Policy selector типизирован:
repository,package namespace,asset path,artifact. Путь нормализуется один раз теми же правилами, что каталог; сегментtools/не разрешаетtools-private/. Case, Unicode и percent-encoding совпадают с контрактом соответствующего namespace. - Прямой
artifactIdне обходит selector: доступ по bytes/descriptor требует разрешённой привязки к namespace и конкретной immutable версии/revision. Blob hash или deduplication не являются правом доступа. - Upload для узкого namespace заранее связывается с publication intent; на complete/register проверяется фактический UPack manifest или конечный asset path. Нельзя загрузить в разрешённую папку и опубликовать в чужую.
- Copy/pointer/restore проверяют источник и назначение; metadata, search, history, references, счётчики, audit и events фильтруются до лимита/пагинации. Фильтрация в UI или после LIMIT не подходит.
- Доступ к raw artifacts list/search для namespace-key запрещён, пока запрос не способен доказать все bindings. Никакого fallback к общему repository read.
- Скачивание по artifact ID, идентичности пакета и пути файла использует один application authorizer; namespace selector проверяется для разрешённого объекта, а не только для формы URL.
9. Пример будущей выдачи
Проект, не исполняемый запрос текущей версии. Целевой service account уже имеет право публикации в releases; оператор имеет разрешение делегировать перечисленные actions.
POST /api/v1/service-accounts/11111111-1111-4111-8111-111111111111/keys
Authorization: Bearer <operator-token>
Idempotency-Key: issue-ci-release-2026-09
Content-Type: application/json
{
"name": "ci-release-prod",
"expiresAt": "2026-12-23T00:00:00Z",
"bindings": [{
"resource": {"kind": "repository", "id": "releases"},
"actions": ["artifact.read", "upload.create", "upload.read", "upload.write", "upload.complete", "upload.cancel", "job.read", "package.publish"]
}]
}Этот ключ публикует только в releases; не скачивает чужие байты, не читает аудит, не управляет аккаунтами и не удаляет версии. Publisher не получает эти права из имени роли.
10. Приёмка модели
Проверить отказ при отсутствии action, несовпадении repository и при попытке собрать права из двух bindings; межсервисные uploads/jobs/references; истечение/revoke/disable; lost-response issuance; ротацию во время upload; параллельные лимиты ключей; отзыв между enqueue и commit; недоступность БД и stale cache; совпадение запретов для скачивания по ID, пакету и пути файла; отсутствие секретов в logs/audit/OpenAPI; суммарную полосу нескольких ключей одного аккаунта. Для namespace selectors дополнительно матрица alias/artifactId/encoding/search/copy/restore.
Нормативная основа передачи Bearer: RFC 6750; используем Authorization и TLS, не query-string secret. Проверки каждого запрашиваемого объекта соответствуют подходу OWASP API1:2023. Эти источники не задают наши роли, сроки или лимиты — это проектные решения Arkvory.
Managed actions storage.read, storage.manage, diagnostics.read относятся к точному repository binding. Legacy write не включает настройку квот/очистки. Включение и запуск требуют также artifact.delete; scheduled run использует live authorizing key. Контракт.