0062. Короткоживущие ссылки на скачивание
Статус: accepted. Owner: Ian Panaev. Дата: 2026-10-03. Частное исключение из единой схемы аутентификации ADR 0045; реализует пункт «короткоживущий transfer token» из PROJECT_PLAN.
Контекст
Агенту развёртывания, скрипту curl/wget или коллеге часто нужен один конкретный файл. Сейчас для этого выдаётся долгоживущий ключ с правами на весь репозиторий, и его приходится хранить там, где файл скачивают. План предусматривал короткоживущие токены передачи, ограниченные объектом, действием и сроком. Они же понадобятся для docker login, если появится реестр образов.
Решение
Выдача. POST /api/v1/repositories/{repository}/artifacts/{id}/links с телом {} или {"ttlSeconds": n}:
- Выдать ссылку может любой, кто вправе читать содержимое артефакта (
content.read; для файловых ключей и групп —read). Артефакт должен быть опубликован. - Срок — час по умолчанию, от 60 секунд до суток.
- Ответ
201 {token, url, expiresAt}:url— путь содержимого с?token=от корня API. Токен показывается только в этом ответе. - Ссылка сама ссылку выдать не может.
- Активных ссылок на одного выдавшего — не больше 1000 (
capacity_exceeded/token_limit). - Повтор запроса автоматически не выполняется: каждый вызов создаёт новую ссылку.
Использование. GET|HEAD …/artifacts/{id}/content?token=dtl_… без заголовка Authorization:
- Ссылка — единственный учётный признак, который принимается в query, и только на этом маршруте, для этого артефакта этого репозитория. На любом другом маршруте параметр ничего не значит, и запрос без ключа получает
401 credential_missing. - Чужой артефакт, неизвестная или повреждённая ссылка —
401 credential_invalid; истёкшая —401 token_expired. - Если есть заголовок
Authorization, он имеет приоритет, а параметр игнорируется. - Субъект ссылки (
credential: transfer-token) получает только чтение этого репозитория. Хук аутентификации пропускает его только на маршрут содержимого с совпадающим id. Дальше путь выдачи общий: допуск, пины, Range, ETag, ограничение скорости. - Очередь и полоса пропускания считаются на выдавшего (id субъекта — его id), поэтому ссылкой нельзя обойти его лимиты.
- Удалённый или снятый с публикации артефакт ссылкой не скачивается (
404). - В
/auth/meи журнал безопасности этот вид учётных данных не попадает: субъект-ссылка не доходит до этих маршрутов, поэтому публичные перечисления не расширены.
Хранение. Таблица arkvory_transfer_links (миграция 29, только расширение схемы):
- хранится SHA-256 секрета, репозиторий, артефакт, выдавший и срок; срок проверяется по часам БД,
CHECKограничивает его сутками; - истёкшие строки удаляются порциями при выдаче новых;
- в резервные копии таблица не входит: ссылки живут не дольше суток.
Утечки. Сам Arkvory ссылку не журналирует: в журналах и метриках только шаблон маршрута, в ответах Cache-Control: private, no-store. Но прокси перед сервером может записать строку запроса. Поэтому пример nginx ведёт журнал только с путём (log_format arkvory_path), а документация предупреждает о журналах своих прокси и истории браузера.
Отзыв. Ссылку нельзя отозвать до истечения срока. Отзыв ключа или прав выдавшего на ней не сказывается — это та же принятая в плане задержка, ограниченная сроком. Выход: выдавать короткие ссылки, а для срочного случая — снять артефакт с публикации или удалить его.
Контракт. Новая операция createDownloadLink. У downloadArtifact и headArtifactContent — необязательный параметр token и вторая схема безопасности downloadLink (apiKey в query). Изменения аддитивные.
- SDK:
client.createDownloadLink(repository, id, {ttlSeconds})иinRepository(r).artifacts.link(id);urlв SDK абсолютный, от базового адреса клиента, включая префикс прокси. - CLI:
arkvoryctl link ID [--ttl СЕКУНДЫ]. - Консоль: кнопка «Ссылка на скачивание» на странице артефакта копирует ссылку в буфер, а если браузер не даёт — показывает её для ручного копирования. Ссылка исчезает со страницы при открытии другого артефакта.
Последствия
- Вторая схема аутентификации ограничена одним маршрутом чтения; маршруты изменений и все остальные чтения по-прежнему принимают только
Bearer. - Ссылки на версию пакета по диапазону или на файл по пути не выдаются: ссылка всегда указывает на неизменяемый артефакт. Чтобы дать «последнюю версию», выдают ссылку на конкретный результат разрешения.
- Не сделано: явный отзыв ссылки, ссылки на загрузку, токены реестра образов (следующий шаг, если появится OCI).
Проверки
tests/integration/download-links.test.mjs:- скачивание без ключа, Range и HEAD;
- ссылка не работает для чужого артефакта и на других маршрутах;
- ссылка не выдаёт ссылок, заголовок имеет приоритет;
- истёкшая и подделанная ссылки;
- проверка срока и полей, хранится только дайджест;
- путь через SDK с абсолютным адресом.
tests/download-links.test.mjs: правила сценария, субъект ссылки, чтение ответа.tests/api-contract.test.mjs: вторая схема только у содержимого артефакта.tests/browser/download-link.mjs: кнопка, буфер или ручное копирование, ссылка работает без ключа, RU/EN, 390 и 1440 px, сброс при открытии другого артефакта.