Воркспейсы и права
Воркспейс — корень аренды в OWT: сообщество со своим сайтом, своими турнирами, своими участниками и своими правами. Почти каждая доменная строка несёт workspace_id — напрямую либо через турнир или запись участника. Эта статья объясняет, как хост превращается в воркспейс, как воркспейс попадает в запрос к API, что видно из чужого воркспейса и как устроены роли, права и скоупы ключей.
Хост определяет сообщество
Есть ровно два взаимоисключающих способа привязать хост к воркспейсу.
Поддомен платформенной зоны. Метка длиной 1–63 символа из a-z, 0-9 и дефиса, без дефиса в начале и в конце, не из списка зарезервированных: www, api, auth, admin, app, assets, static, cdn, mail, ws. Многосегментная метка не принимается, апекс зоны — это сама платформа, а не арендатор.
Собственный домен. Полное доменное имя минимум из двух меток, уникальное и заведомо не под платформенной зоной. Домен обслуживается только после подтверждения владения записью DNS TXT: до этого он не резолвится ни во что — политика отказа по умолчанию.
Резолв доступен публично и без аутентификации:
GET /api/v1/workspaces/by-host?host=example.org HTTP/1.1
{"workspace_id": 7, "slug": "example"}
Ответ null означает «хост не назван, некорректен или не принадлежит ни одному воркспейсу» — в том числе если собственный домен добавлен, но ещё не подтверждён. Сайт использует тот же вызов: хост, похожий на арендаторский, но не резолвящийся, отдаёт 404, а временный сбой резолва — 503 с заголовком Retry-After, чтобы живой арендатор не исчезал из-за одной неудачной попытки.
Маршруты API не привязаны к хосту. Сайт сообщества на поддомене или на собственном домене отвечает по тем же путям /api/..., что и https://owt.craazzzyyfoxx.me.
У воркспейса есть ещё два независимых флага, которые легко перепутать. is_active выключает воркспейс, а is_hidden убирает его только из публичного каталога и из чужих списков — прямой доступ по slug, поддомену или подтверждённому домену при этом сохраняется. Отдельная ось — уровень доверия verification_status (unverified, verified, trusted), который меняет только суперпользователь: он управляет доступом к тяжёлым вычислениям и попаданием в публичный каталог.
Воркспейс в запросе
Хост определяет сообщество для сайта, но сам запрос к API называет воркспейс явно — параметром workspace_id. Доменные чтения без него отвечают 400: отдать строки всех арендаторов сразу было бы утечкой между сообществами, поэтому отсутствие скоупа считается ошибкой, а не «читай всё».
GET /api/v1/tournaments?workspace_id=7 HTTP/1.1
Authorization: Bearer <токен или API-ключ>
Одно послабление шлюз делает сам: если предъявленная учётка привязана ровно к одному воркспейсу — а API-ключ привязан всегда, — и workspace_id в запросе не передан, шлюз подставит идентификатор из самой учётки. Явно переданное значение всегда важнее. Если у учётки воркспейсов несколько, шлюз не угадывает и оставляет 400 в силе.
Подстановка не расширяет права: она называет воркспейс, который учётка и так держит, а проверку прав в нём всё равно выполняет доменный воркер.
Что видно из чужого воркспейса
Границу держат три разных механизма, и снаружи они выглядят по-разному:
- Списки и коллекции фильтруются по
workspace_id. Чужие строки не «запрещены» — их просто нет в ответе. - Операции над объектом (правки, админские действия) сначала определяют воркспейс самого объекта, а потом требуют нужное право именно в нём. Нет права — 403 с текстом вида
Permission denied for workspace 7: tournament.update required. Объекта не существует — 404. - Скрытые турниры отвечают 404 «не найдено» всем, кроме инсайдеров: суперпользователя, участника воркспейса-организатора и аккаунтов из списка предпросмотра. Это же правило действует на подписки WebSocket, см. Realtime.
Публичные чтения платформы (профиль игрока, справочники, карточка турнира по идентификатору) остаются публичными: аренда ограничивает доменные выборки и любые записи, а не превращает открытые данные в закрытые.
Участники
workspace_member — это якорь, за который цепляется всё локальное для сообщества: ростеры, заявки, драфты, достижения и ранги. Строка уникальна парой (workspace_id, player_id) и намеренно бедна: в ней нет ни auth_user_id, ни колонки роли.
Так получается потому, что идентичность разложена на слои:
| Слой | Что это | Ключевое свойство |
|---|---|---|
auth.user | Аккаунт: вход, сессии, роли | Может не существовать вовсе |
players.user | Игрок: BattleTag, история | auth_user_id уникален и может быть NULL — это теневой игрок из лога или таблицы, он не логинится |
public.workspace_member | Игрок внутри сообщества | Уникален парой воркспейс + игрок; хранит локальный display_name |
balancer.member_rank | Ранги участника | Слой поверх участника, не поверх аккаунта |
Из этого следуют два практических факта. Во-первых, заявка, ростер и ранг ссылаются на участника, а не на аккаунт, поэтому организатор может внести игрока, у которого аккаунта нет. Во-вторых, роль участника лежит не здесь, а в ролях аккаунта, привязанных к воркспейсу, — и участник без аккаунта роли не имеет в принципе. Когда аккаунт впервые появляется в воркспейсе и ролей в нём ещё нет, ему автоматически выдаётся системная роль member; уже имеющиеся роли эта выдача не трогает и никогда не понижает.
Роли и права
Права — это плоский каталог грантов вида resource.action (tournament.update, registration.approve), плюс единственный подстановочный элемент admin.*. Каталог — и есть список допустимого: права, которого в нём нет, выдать нельзя. Поверх каталога — шесть системных ролей воркспейса:
| Роль | Что даёт |
|---|---|
owner | admin.* — всё в этом воркспейсе |
admin | Всё, кроме управления ролями и правами и кроме удаления воркспейса и его участников |
referee | Чтения уровня member плюс match.result и registration.update / .approve / .reject / .check_in — результаты и допуск, ничего структурного |
host | Чтения уровня member плюс полный цикл custom_game — это и есть право проводить миксы |
member | Только *.read по доменным ресурсам сообщества |
player | Ничего |
Два гранта в этом каталоге уже, чем можно подумать по имени ресурса, — и именно на этом разделении держится роль referee:
| Право | Что покрывает | Чего не покрывает |
|---|---|---|
match.result | Результат встречи: поля home_score, away_score, status, closeness, started_at, ended_at, current_map_index в общем PATCH плюс эндпоинты результата — проставить и переоткрыть, результаты карт, правка отдельной игры, результаты и аннулирование игр FFA, живые правки пик-бана (сброс, действие, отправка, переоткрытие, выбор начинающего) | Всё, что относится к самой встрече |
match.update | Саму встречу: название, стадию, команды, раунд, формат серии, время начала, обмен слотами в сетке, конфигурацию пик-бана, форму отчёта, число игр FFA | Поля результата выше |
registration.roles | Роли заявки — включая answers.roles — её ранги и закрепление (pin, clear_pin), а также применение автозаполнения рангов | Остальную заявку, за неё отвечает registration.update |
PATCH, смешивающий обе стороны, требует обоих грантов, а PATCH заявки с ключом roles без права registration.roles отклоняется целиком, а не молча урезается.
Отдельно стоят способности, разрешённые всем по умолчанию: account.avatar, account.rename, account.social, custom_game.self_join, registration.self_register, workspace.self_create. Они существуют не чтобы их выдавать, а чтобы их можно было отобрать точечно.
Отбирает их запрещающий слой: персональная запись «этому аккаунту запрещено resource.action», глобальная или привязанная к одному воркспейсу. Запрет бьёт любой грант, включая суперпользователя, но совпадает только точно: он никогда не подстановочный, поэтому запрет tournament.update не трогает tournament.delete.
Порядок вычисления права в воркспейсе:
- Запрет на эту пару
resource.action— сразу нет. - Суперпользователь или глобальная роль
admin— да. - Глобальный грант (с учётом подстановок) — да.
- Роль
ownerилиadminв этом воркспейсе — да, кроме ресурсовroleиpermissionи кромеworkspace.deleteиworkspace_member.delete. - Грант, выданный в этом воркспейсе (с учётом подстановок) — да.
- Иначе нет.
Права, которые нужны интегратору
Имена прав — это ровно то, что будет проверено на эндпоинте, и ровно то, что вы перечисляете в скоупах ключа.
| Право | Зачем оно интегратору |
|---|---|
tournament.read, stage.read, match.read, standing.read, team.read, player.read | Чтение турнирной картины сообщества |
tournament.create, tournament.update | Заведение и правка турниров |
match.update, match.result | Правка встречи и внесение её результата |
registration.read | Чтение заявок |
registration.approve, registration.reject, registration.check_in | Допуск, отказ, чек-ин |
registration.roles | Смена ролей, рангов и закрепления в заявке |
registration_form.read, registration_form.update | Форма заявки |
team.create | Создание команд, в том числе результатом задачи балансировщика |
balancer.read, balancer.create | Задачи балансировки |
custom_game.create, custom_game.update, custom_game.delete | Проведение миксов |
log.create, log.read | Загрузка и чтение разбора логов |
analytics.read | Аналитика сообщества |
stream.update, rank.update, subscription.update | Ручной перезапрос статусов трансляций, рангов и подписок |
admin.* | Всё сразу; выдавайте только когда перечислить нужное действительно невозможно |
Скоупы ключа — это те же права
У API-ключа нет собственного словаря разрешений: скоуп ключа — это имя права из того же каталога. Когда ключ предъявляется, его скоупы пересекаются с реальными правами владельца в том единственном воркспейсе, к которому ключ привязан, и результат подставляется как обычный набор прав. Поэтому эндпоинты проверяют ключ той же проверкой, что и сессию, без отдельной ветки.
Следствия, на которые стоит рассчитывать при выдаче ключей:
- Ключ никогда не шире владельца: сняли у владельца право — ключ теряет его в тот же момент.
- Ключ видит ровно один воркспейс, и глобальных прав в его наборе нет никогда.
- Ключ без скоупов не может ничего, включая аутентификацию сокета.
admin.*— единственная подстановка; формы вродеtournament.*не существует.- Неизвестное имя скоупа отклоняется при создании ключа, а если право позже убрали из каталога — ключ просто теряет эту способность, а не ломается целиком.
Выпуск, перевыпуск и отзыв ключей делаются только сессией, не другим ключом; подробности и квоты — в статье Аутентификация и ключи API.
См. также
- Аутентификация и ключи API — сессии, ключи, скоупы, лимиты.
- HTTP API — версии, ошибки, пагинация.
- Realtime (WebSocket) — топики воркспейса и правила подписки.
- Модель данных — где живут
workspace,workspace_memberи роли.