Тенанты, пользователи и аутентификация/авторизация
Рабочее пространство — это граница ресурсов и прав aihelpme.ru: базы знаний, модели, агенты, сессии и квоты хранилища принадлежат пространству. Один пользователь может входить в несколько пространств и иметь в каждом из них разные роли. Организации служат для связи нескольких пространств и общего доступа к базам знаний и агентам. Во внутренней модели пространство представлено сущностью Tenant.
Ниже перечислены точки входа для приглашения участников, общего доступа к ресурсам и доступа через API. Для управления всем развёртыванием требуются отдельные права платформы.
| Действие | Точка входа и требования |
|---|---|
| Пригласить участников команды | Настройки пространства → Участники → Пригласить, задав приглашённому роль (Owner / Admin / Contributor / Viewer) |
| Поделиться базой знаний с другой командой | Создать организацию → добавить в неё оба пространства → в базе знаний выбрать «Поделиться с организацией» |
| Подключиться к API | Настройки пространства → API Key, при необходимости отметить возможности (поиск / вопросы-ответы / загрузка / управление) и ограничить доступные базы знаний |
| Управлять всем развёртыванием (глобальные настройки, очередь задач, аудит между пространствами) | Требуется роль системного администратора, которая выдаётся независимо от Owner пространства; см. Управление платформой и системный администратор |
| Удалить всё пространство | В настройках пространства запускает Owner (DELETE /tenants/:id); пространство и все связи участников помечаются как удалённые (soft delete), участники немедленно теряют доступ, восстановить через интерфейс нельзя |
Viewer может просматривать и задавать вопросы, Contributor может создавать базы знаний и загружать документы, Admin управляет участниками и настройками пространства, Owner также может удалить или передать пространство. Изменение ресурсов дополнительно ограничено правами владения или общего доступа; матрица ролей приведена в справочном разделе.
Пользователи могут входить по паролю или через единый вход OIDC; программы получают доступ по API Key. Права вошедшего пользователя определяются ролью в пространстве и принадлежностью ресурсов, а API Key проверяется по выданным возможностям и области баз знаний.
Приглашение участников и назначение ролей
В разделе «Участники» настроек пространства пригласите пользователей и назначьте роли в соответствии с их рабочим диапазоном. После принятия приглашённый присоединяется к текущему пространству; когда публичная регистрация отключена, зарегистрироваться можно по действующему приглашению. Существующие пользователи также могут принять приглашение и присоединиться к новому пространству, не создавая второй аккаунт.
Роли участников пространства и роли участников организации управляются отдельно. Когда нужно поделиться материалами, следует одновременно проверить права общего доступа к базе знаний, роль принимающего пространства в организации и роль самого пользователя в пространстве.
Общий доступ к базам знаний и агентам
Сначала добавьте исходное и принимающее пространства в одну организацию, затем участник с достаточными правами делится базой знаний или агентом с этой организацией. Фактические права получателя ограничиваются одновременно записью о совместном доступе и ролью участника; общий доступ в организации не повышает автоматически роль пользователя в пространстве.
Настройка API Key для программ
Создайте API Key в настройках пространства и выберите для задачи возможности поиска, вопросов-ответов, загрузки или управления; если нужно ограничить диапазон материалов, укажите доступные базы знаний. Программа передаёт учётные данные в заголовке X-API-Key; конкретные возможности и ограничения ресурсов приведены в справочном разделе.
Управление пространством и правами платформы
Пространство удаляет Owner. Запись пространства и связи участников помечаются как удалённые, и все участники сразу теряют доступ; данные внутри пространства — базы знаний, модели и т. п. — не удаляются физически немедленно, но начиная с v0.8.2 задачи Wiki в очереди удалённого пространства больше не вызывают модель. Глобальными настройками, очередью задач платформы и аудитом между пространствами управляет системный администратор, подробнее см. Управление платформой и системный администратор.
Обзор концепций
Ключевые моменты:
- Один User может одновременно принадлежать нескольким Tenant через таблицу
tenant_members, причём у каждой связи участника своя роль. - Связь участника организации имеет уровень тенанта (
OrganizationTenantMemberс единицейtenant_id), и совместный доступ тоже устроен как «некоторый тенант делится KB с некоторой организацией». - API Key — это полностью независимый машинный субъект, отдельный от JWT-пользователя, не переиспользующий лестницу ролей тенанта.
Справочник по аутентификации и авторизации
Регистрация и вход
Режимы регистрации (invite-only)
internal/handler/auth.go + internal/config/config.go:
type AuthConfig struct {
RegistrationMode string // "self_serve" (по умолчанию, публичная регистрация) | "invite_register" (регистрация только по приглашению) | "invite_only" (регистрация запрещена, сохраняется старое значение конфигурации)
DefaultTenantMode string // "create_personal" (по умолчанию, автоматически создаётся личный тенант) | "tenantless" (без тенанта, в ожидании приглашения)
}
func (c *AuthConfig) IsInviteOnly() bool {
return c != nil && c.RegistrationMode == AuthRegistrationModeInviteOnly
}Проверка делится на два уровня: понимание этого объясняет, почему «изменил env, а эффекта нет»:
При запуске (applyAuthAndTenantDefaults()) синтезируется cfg.Auth.RegistrationMode: DISABLE_REGISTRATION=true напрямую переписывает его в invite_only, перекрывая значение из YAML. Env перекрывает YAML для того, чтобы два барьера — «интерфейс отклоняет регистрацию» и «фронтенд скрывает вход в регистрацию» (фронтенд читает /auth/config) — были согласованы, иначе остаётся кнопка, нажатие на которую даёт 403.
При каждом запросе (resolveRegistrationMode()) сравниваются только два источника: строка auth.registration_mode в базе данных system_settings > синтезированное выше значение cfg > жёстко зашитый запасной вариант self_serve. DISABLE_REGISTRATION при этом не перечитывается на каждый запрос.
Следствие: после того как системный администратор установит auth.registration_mode в self_serve через интерфейс, публичная регистрация будет открыта, даже если в развёртывании всё ещё указано DISABLE_REGISTRATION=true. Чтобы полностью отключить её, нужно сбросить эту строку в базе данных (DELETE /system/admin/settings/auth.registration_mode).
У режима регистрации есть три ступени, переключаемые в глобальных настройках на лету:
| Режим | Публичная регистрация | Регистрация по действующей ссылке-приглашению | Существующий аккаунт принимает приглашение |
|---|---|---|---|
self_serve (открытая регистрация) | Разрешена | Разрешена | Разрешена |
invite_register (регистрация только по приглашению) | Запрещена | Разрешена | Разрешена |
invite_only (регистрация запрещена, сохраняется старое значение конфигурации) | Запрещена | Запрещена | Разрешена |
DISABLE_REGISTRATION=true по-прежнему отображается в исходный invite_only. Чтобы приглашённые новые пользователи могли зарегистрироваться, измените глобальную настройку на invite_register. Когда регистрация запрещена, POST /auth/register-by-invite также возвращает 403, что согласуется с поведением страницы входа.
Эти ступени управляют регистрацией по паролю. Первый вход через OIDC по-прежнему определяется политиками видимости приложения в IdP, группами пользователей и т. п.; создание аккаунта администратором на эту настройку также не влияет.
Регистрация/вход по паролю
POST /auth/register:{username(2-50), email, password}; в соответствии сDefaultTenantModeрешается, создавать ли личный тенант автоматически (TenantProvisioningCreatePersonal/TenantProvisioningTenantless).POST /auth/login:{email, password}, возвращаетLoginResponse{user, active_tenant, memberships[], token, refresh_token}; активный тенант восстанавливается поPreferences.LastActiveTenantID.- Регистрация, регистрация по приглашению, изменение пароля и установка нового пароля администратором подчиняются единой политике паролей: по умолчанию 8–32 символа, минимум буквы и цифры.
GET /auth/configвозвращает текущееcomplex_password_enabled; при включении дополнительно требуются буквы разного регистра и специальные символы. - Системный администратор может настроить это через
auth.complex_password_enabled, при отсутствии значения в базе происходит откат кWEKNORA_AUTH_COMPLEX_PASSWORD_ENABLED. Изменение политики ограничивает только пароли, создаваемые или изменяемые после этого, и не заставляет существующие аккаунты немедленно менять пароль. - Самостоятельная смена пароля в профиле требует ввода старого пароля, новый пароль не может совпадать; после успеха все сессии пользователя отзываются, требуется повторный вход. Ошибки интерфейса и параметры см. в API аутентификации.
Регистрация по приглашению (register-by-invite)
internal/handler/auth_register_by_invite.go. Общая ссылка-приглашение (share link), создаваемая Owner тенанта (см. Общая ссылка-приглашение (invite link)), содержит token; страница регистрации завершает регистрацию по token, поддерживаются режимы self_serve и invite_register:
// POST /auth/register-by-invite
type registerByInviteRequest struct {
Token string `binding:"required"`
Email string `binding:"required,email"` // заполняется регистрирующимся, с token не связан
Username string `binding:"required"`
Password string `binding:"required"` // дополнительно проверяется единой политикой паролей (8–32 символа, буквы и цифры)
}Процесс: проверка token (LookupByToken) → проверка, что email не зарегистрирован (если зарегистрирован — 409) → создание пользователя в режиме tenantless → установка тенанта-приглашающего как первого тенанта пользователя → AcceptByToken создаёт строку tenant_members (статус active, роль берётся из приглашения).
Сопутствующий эндпоинт POST /auth/invitations/lookup (без аутентификации) возвращает контекст приглашения {tenant_id, tenant_name, role, expires_at} для отображения на странице регистрации; token передаётся в теле POST-запроса, чтобы не попадать в журналы доступа по URL; недействительный или отозванный token возвращает 410.
Присоединение зарегистрированного пользователя по ссылке-приглашению
В развёртываниях invite_only страница приглашения предлагает пользователю сначала войти, а затем отправить token в POST /me/invitations/accept-by-token, чтобы присоединиться к пространству; создавать аккаунт для уже зарегистрированного email не нужно. Пользователь без пространства по умолчанию после первого присоединения получает это пространство как пространство по умолчанию. В режиме invite_register новый пользователь может зарегистрироваться по действующему приглашению; существующий аккаунт может переключиться на форму входа, чтобы присоединиться, и переключиться обратно на форму регистрации.
Приглашение по email для уже зарегистрированного пользователя дополнительно управляется tenant.auto_accept_invitation: по умолчанию false — создаётся приглашение в состоянии pending и ожидается подтверждение из входящих; при true пользователь присоединяется сразу, возвращается активный участник, а существующие приглашения в состоянии pending обрабатываются. Фронтенд узнаёт об этом переключателе из capabilities.auto_accept_invitation в GET /auth/me. Он не превращает произвольную общую ссылку во вход без аутентификации.
Участники тенанта, приглашения и ссылки-приглашения
Управление участниками и адресные приглашения
Handler: internal/handler/tenant_member.go, tenant_invitation.go. Группа /tenants/:id единообразно вешает PathTenantMatch() (тенант из URL должен совпадать с активным тенантом в token, кроме суперпользователя).
| Эндпоинт | Минимальная роль | Описание |
|---|---|---|
GET /tenants/:id/members | Viewer | Постраничный список активных участников, q фильтрует по email/имени пользователя нечётко |
POST /tenants/:id/members | Owner | Напрямую добавить существующего пользователя {email, role} |
PUT /tenants/:id/members/:user_id | Owner | Изменить роль |
DELETE /tenants/:id/members/:user_id | Owner | Удалить участника |
POST /tenants/:id/invitations | Owner | Адресное приглашение существующего пользователя {email, role, message} |
GET /tenants/:id/invitations | Viewer | Список приглашений |
DELETE /tenants/:id/invitations/:inv_id | Owner | Отозвать приглашение |
GET /me/invitations | Сам пользователь | Входящие приглашения |
POST /me/invitations/:inv_id/accept / .../decline | Сам пользователь | Принять / отклонить |
Машина состояний TenantInvitation: pending → accepted / declined / revoked / expired (истечение выполняется ленивой очисткой с аудитом rbac.invitation_expired). Весь жизненный цикл участников и приглашений сопровождается событиями аудита: rbac.member_added / member_removed / member_role_changed / member_left / invitation_sent / invitation_accepted / invitation_declined / invitation_revoked (internal/types/audit_log.go).
Общая ссылка-приглашение (invite link)
internal/handler/tenant_invite_link.go. Хранится в той же таблице, что и адресные приглашения: пустой InviteeUserID — общая ссылка (могут использовать многие, считает AcceptedCount), непустой — адресное приглашение.
POST /tenants/:id/invite-links(Owner):{role, message}→ возвращаетinvite_url({FrontendBaseURL}/register?token=..., гдеFrontendBaseURLберётся из YAMLfrontend_base_url→ переменной окруженияFRONTEND_BASE_URL→ относительного пути как запасного варианта);- Ссылки-приглашения и адресные приглашения хранятся в одной таблице; список —
GET /tenants/:id/invitations(Viewer), отзыв —DELETE /tenants/:id/invitations/:inv_id(Owner).
Ссылка действует до истечения срока или отзыва; вместе с регистрацией по приглашению (register-by-invite) она замыкает цикл создания аккаунта в режиме invite-only.
Организации и общие пространства
Жизненный цикл организации
internal/application/service/organization.go:
- При создании организации генерируется уникальный
InviteCodeсо сроком действияinvite_code_validity_days ∈ {0(бессрочно), 1, 7, 30}, по умолчанию 7 дней (белый списокValidInviteCodeValidityDays, недопустимое значение возвращаетErrInvalidValidityDays); GetOrganizationByInviteCodeвступает в группу по коду-приглашению (различаетErrInviteCodeNotFound/ErrInviteCodeExpired); приRequireApproval=trueсоздаётся запрос на присоединение, ожидающий одобрения;- Организации с
Searchable=trueмогут быть найдены черезSearchSearchableOrganizations; - Код-приглашение и число ожидающих одобрения видны только «администратору или владельцу организации» (проверка
isAdmin || isOwnerвinternal/handler/organization.go).
Кандидаты для приглашения: точное разрешение по ID пространства
Приглашение в организацию нацелено на рабочее пространство. GET /organizations/:id/search-tenants?q=<ID пространства> может вызывать только администратор организации; начиная с v0.8.2 кандидаты разрешаются только по точному ID пространства, без поиска по имени между пространствами (чтобы не давать возможности перечислять названия других пространств): если q не является допустимым ID, пространство уже в организации или не существует — возвращается пустой список, иначе возвращается единственный {tenant_id, tenant_name}. Приглашаемая сторона может узнать ID пространства в своих настройках пространства и сообщить его администратору организации; также можно по-прежнему присоединиться по коду-приглашению или ссылке-приглашению организации.
Старый эндпоинт GET /organizations/:id/search-users сохранён как совместимый псевдоним с тем же поведением, что и search-tenants.
POST /organizations/:id/invite (только администратор организации) добавляет участника напрямую: предпочтительно через tenant_id, с совместимостью со старым путём user_id из SDK (обратный поиск пространства по умолчанию этого пользователя). Прямое добавление больше не прикладывает представительного пользователя (representative_user_id игнорируется); список участников организации возвращает email представительного пользователя только для собственного пространства вызывающего.
Модель совместного доступа к KB и расчёт прав
internal/types/organization.go + internal/application/service/kbshare.go:
type KnowledgeBaseShare struct {
ID string
KnowledgeBaseID string
OrganizationID string
SharedByUserID string
SourceTenantID uint64 // тенант-источник общего доступа
Permission OrgMemberRole // максимальные права, выданные общим доступом (viewer/editor/admin)
}
// Структура AgentShare имеет тот же вид, но для Agent.Предусловия для общего доступа (ShareKnowledgeBase): тенант вызывающего должен владеть этим KB (kb.TenantID == tenantID), а его роль в целевой организации должна быть editor+. Повторный общий доступ превращается в обновление прав.
Кто может управлять общим доступом (canManageShare, общий для KB и Agent, используется для смены прав/отзыва):
- Изначальный «расшаривший», всё ещё действующий из тенанта-источника, с ролью в тенанте Contributor+;
- Admin+ тенанта-источника (владение уровнем тенанта: после ухода «расшарившего» Admin тенанта всё ещё может управлять);
- Тенант с ролью admin в целевой организации: его пользователи Admin+ могут отозвать общий доступ или понизить права, но не могут поднять их выше текущего значения, иначе это передало бы право записи в чужой KB всем участникам-editor.
share_id в маршруте должен принадлежать KB / Agent из пути, иначе возвращается 404.
Ограничения получателя на настройку общей KB: только пространство, где находится KB, может вызывать POST /initialization/initialize/:kbId; другим пространствам для изменения настроек KB (PUT /initialization/config/:kbId) нужен общий доступ уровня admin, и они не могут менять привязку хранилища; при чтении настроек получатель видит только, настроены ли учётные данные, но не видит Base URL модели, расположение бакета и т. п. Детали KB также больше не возвращают встроенные в старую версию учётные данные хранилища/VLM.
Когда общий доступ перестаёт действовать: он действует только пока организация не удалена и тенант-источник остаётся участником организации. Когда тенант-источник выходит или удалён, его KB и Agent, расшаренные в эту организацию, отзываются вместе с ним; сторона запросов также фильтруется по связям участника тенанта-источника, и общий доступ, оставшийся от старых версий, тоже больше не действует.
Действующие права = пересечение слоёв (берётся минимум):
// Итоговые права = Min(Permission из записи общего доступа, OrgMemberRole тенанта вызывающего в организации)
// Затем накладывается потолок по роли тенанта:
func applyTenantRoleCap(p types.OrgMemberRole, callerTenantRole types.TenantRole) types.OrgMemberRole {
// Пользователь, который в тенанте только Viewer, даже если на стороне общего доступа дано editor+, понижается до viewer
if callerTenantRole == types.TenantRoleViewer && p.HasPermission(types.OrgRoleEditor) {
return types.OrgRoleViewer
}
return p
}Операции, связанные с общим доступом, пишутся в ленту активности KB: kb.share_added / kb.share_permission_changed / kb.share_removed.
Дополнительные правила совместного доступа к Agent (internal/application/service/agent_share.go, agent_share_scope.go):
- Встроенные агенты нельзя расшаривать: в каждом пространстве есть встроенный агент с тем же ID, и после расшаривания получатель не сможет их различить. Запросы диалога без указания исходного пространства всегда сначала используют агента своего пространства.
- Совместный доступ к Agent в режиме только для чтения открывает его область KB участникам организации, поэтому «расшаривший» должен иметь право напрямую делиться этими KB, то есть быть создателем KB или Admin+ тенанта.
kb_selection_mode: allавтоматически включает KB, созданные впоследствии, и делиться ими может только Admin+. При редактировании уже расшаренного Agent к вновь добавленным в область KB применяется то же правило. - При совместном выполнении, если у Agent не задан режим выбора MCP, он трактуется как «не использовать», что соответствует отображению в области общего доступа; в режиме быстрых вопросов-ответов веб-поиск должен быть включён у самого Agent, а
summary_model_idв запросе игнорируется. - Получатель видит у расшаренного Agent без системного промпта и ID пользователя-создателя, но возможности и область баз знаний по-прежнему видны.
- После расшаривания Agent с включёнными навыками навыки выполняются в песочнице пространства-источника и получают переменные окружения, заданные администратором для навыка (например, API Key); участники могут заставить агента прочитать эти значения. Страница настроек общего доступа предупреждает об этом.
RBAC: роли, владение и матрица охранников
Авторизация состоит из трёх ортогональных механизмов, все они сходятся в rbacGuards в internal/router/rbac.go:
- Охранник роли (role-only):
Viewer()/Contributor()/Admin()/Owner()/SystemAdmin(), спрашивает «какова роль вызывающего в этом тенанте». - Охранник владения (ownership-or-role):
OwnedKBOrAdmin()и т. п., спрашивает «является ли вызывающий создателем именно этого ресурса или как минимум Admin+». - Охранник доступа к KB (KB-access):
KBAccessRead()/KBAccessWrite(), спрашивает «может ли тенант вызывающего дотянуться до этого KB» (собственный / общий в организации / видимый через расшаренный Agent).
Матрица возможностей ролей
| Возможность | Owner (40) | Admin (30) | Contributor (20) | Viewer (10) |
|---|---|---|---|---|
| Удаление тенанта / передача владения / управление API Key | ✓ | ✗ | ✗ | ✗ |
| Добавление / удаление участников, смена ролей, отправка приглашений | ✓ | ✗ (в handler ограничено Owner) | ✗ | ✗ |
| Настройка инфраструктуры тенанта (модели / векторные хранилища / IM / MCP / веб-поиск / бэкенды хранилища / источники данных) | ✓ | ✓ | ✗ | ✗ |
Очистка содержимого базы знаний (DELETE /knowledge-bases/:id/knowledge) | ✓ | ✓ | ✗ | ✗ |
| Изменение / удаление KB / Agent / знаний / chunk / Wiki / тегов, созданных другими | ✓ | ✓ | ✗ | ✗ |
| Создание KB / Agent; копирование Agent себе | ✓ | ✓ | ✓ | ✗ |
| Изменение / удаление созданных собой KB и их дочерних ресурсов | ✓ | ✓ | ✓ | ✗ |
Создание/управление своими сессиями, запуск вопросов-ответов (/sessions, /knowledge-chat, /agent-chat — все Viewer+) | ✓ | ✓ | ✓ | ✓ |
| Просмотр списка участников / приглашений / списка KB / знаний / поиска / предпросмотра | ✓ | ✓ | ✓ | ✓ |
Проектный комментарий в начале internal/router/rbac.go резюмирует продуктовую семантику:
- Owner / Admin: управляют всем в тенанте;
- Contributor: управляет созданными собой ресурсами, чужие ресурсы для него эквивалентны «только чтение»;
- Viewer: всё только для чтения;
- Для создания нового ресурса нужен как минимум Contributor; для настройки инфраструктуры тенанта нужен Admin+.
Две легко упускаемые исключения: добавление/удаление участников, смена ролей и отправка приглашений — прерогатива только Owner, Admin это тоже не может (в routes_auth_tenant.go повешен g.Owner(), а список участников — Viewer+); Viewer не означает «не может создать ничего» — сессии принадлежат его рабочим данным, Viewer тоже может создавать сессии и задавать вопросы, просто не может создавать базы знаний и Agent.
Правила выбора охранника (Q1 / Q2)
rbac.go явно задаёт метод выбора охранника для новых маршрутов:
- Q1: есть ли у ресурса creator? Есть (KB, Agent, документ знаний, Chunk, WikiPage, запись FAQ, тег KB) → для маршрутов изменения используется
OwnedXxxOrAdmin; нет (Model, VectorStore, канал IM, WebSearchProvider, DataSource, MCPService и прочая инфраструктура уровня тенанта) → используетсяAdmin(); точка входа создания (ресурс ещё не существует) →Contributor(). - Q2: побочный эффект приватный или публичный? Приватный (например,
POST /agents/:id/copyкопирует только себе) → достаточноContributor(); публичный (расшарить KB в организацию, отключить Agent всего тенанта, передать владение) →OwnedXxxOrAdminилиAdmin.
Список охранников владения
| Охранник | Путь разрешения | Применяемые маршруты |
|---|---|---|
OwnedKBOrAdmin | :id → KB.CreatorID | Обновление / удаление / pin / загрузка знаний / CRUD тегов KB |
OwnedKBOrAdminFromKbIDParam | :kbId → KB.CreatorID | Маршруты настройки KB /initialization/* |
OwnedAgentOrAdmin | :id → Agent.CreatorID (у встроенного Agent creator пуст, менять может только Admin+) | Изменение Agent |
OwnedKnowledgeKBOrAdmin | knowledge :id → CreatorID KB-владельца | Обновление / удаление / перепарсинг / редактирование изображений знания |
OwnedChunkKBOrAdmin / ...FromChunkID | :knowledge_id или chunk :id → KB.CreatorID | Изменение chunk |
OwnedWikiKBOrAdmin | :kb_id → KB.CreatorID | CRUD страниц Wiki |
Дочерние ресурсы должны наследовать контроль родительского KB (в комментарии прямо названы исправленные баги с подключением FAQ/Tag, agent share, KB share не к той оси).
Семантика middleware (internal/middleware/rbac.go)
Порядок проверки в RequireRole / RequireOwnershipOrRole:
- Субъект API Key пропускается напрямую (его авторизация идёт через APIKeyGate механизма объявления маршрутов, а синтетический системный пользователь не может совпасть с
creator_id); - Роль достаточна → пропуск;
- Суперпользователь между тенантами (
IsCrossTenantSuperuser) → пропуск; - RBAC не принудительно (
tenant.enable_rbac=false, режим постепенного внедрения) → пропуск только с записью в журнал; - Охранник владения выполняет запрос creator: ресурс не существует → пропуск, чтобы handler вернул 404; запрос не удался → 503; creator == текущий пользователь → пропуск;
- Иначе 403 + журнал аудита (
AuditActionAccessDenied = "rbac.access_denied").
Переключатель принудительного применения TenantConfig.EnableRBAC: nil или true = принудительно (текущее значение по умолчанию), false = только журнал без отказа (для переходного периода выпуска); переопределяется переменной окружения WEKNORA_TENANT_ENABLE_RBAC. Этот переключатель действует только на проверки ролей внутри пространства; охранник доступа к KB (RequireKBAccess) при доступе между пространствами блокирует всегда.
RequireSystemAdmin: JWT-пользователь должен иметь IsSystemAdmin=true; API Key должен быть platform key (tenant key всегда даёт 403).
Охранник доступа к KB (канал общего доступа между тенантами)
middleware/kb_access.go (оборачивается серией KBAccess* из rbac.go, правила решения находятся в internal/application/access) объединяет три пути доступа:
1. Собственный KB → эквивалент полного доступа уровня Admin
2. Общий KB организации (Plan 3) → ограничен правами общего доступа
3. Видимый через общий Agent → только чтение (активируется только на уровне KBAccessRead)После успеха охранник сохраняет (KB, действующий ID тенанта, права) в context и переписывает ID тенанта запроса на действующий тенант, так что downstream handler не нужно знать, свой это KB или общий. Варианты KBAccessReadFromKnowledgeIDParam / ...FromChunkIDParam позволяют найти KB по knowledge / chunk ID. Минимум для маршрутов чтения — OrgRoleViewer, для маршрутов записи — OrgRoleEditor.
Система API Key
Список возможностей (Capabilities)
internal/types/tenant_api_key.go. API Key не переиспользует роли тенанта: ключ либо FullAccess, либо несёт явный набор возможностей; маршруты без объявленной политики для API Key по умолчанию отклоняются (default-deny).
| Возможность | Описание |
|---|---|
retrieve | Чтение / поиск данных баз знаний (список KB, детали знания, hybrid-search и т. д.) |
chat | Поток сессий: создание session, knowledge-chat / agent-chat, загрузка и удаление сообщений |
read_agents | Просмотр и список Agent (без создания и изменения) |
ingest | Запись содержимого: загрузка документов, редактирование chunk / FAQ / тегов / Wiki, массовое удаление и перемещение знаний |
manage_kbs | Жизненный цикл KB: создание / копирование / дублирование / обновление / удаление / начальная настройка |
manage_agents | Добавление, удаление, изменение и копирование Agent |
message_history | Поиск и просмотр истории чата уровня тенанта (POST /messages/search и т. п., отдельно от chat) |
manage_models | Управление определениями моделей и учётными данными |
manage_mcp_services | Управление сервисами MCP и учётными данными |
manage_datasources | Управление коннекторами источников данных и задачами синхронизации |
manage_channels | Управление интеграциями каналов Embed / IM |
manage_vector_stores | Управление векторными хранилищами и парсерами |
manage_storage_backends | Управление бэкендами объектного хранилища |
manage_web_search | Управление настройками веб-поиска |
run_evaluations | Запуск и просмотр задач оценки |
manage_members | Управление участниками и приглашениями тенанта |
manage_spaces | Управление связями участников организаций / общих пространств |
manage_tenant_settings | Чтение и запись сводных настроек тенанта |
system_tenants_read / system_tenants_manage | Уровень платформы: управление тенантами (только platform key) |
system_settings_read / system_settings_manage | Уровень платформы: системные настройки |
system_runtime_read / system_runtime_manage | Уровень платформы: очереди / задачи времени выполнения |
system_audit_read | Уровень платформы: журнал аудита |
Механизм объявления маршрутов
В internal/router/rbac.go каждый доступный по API Key маршрут через apiKeyGroup / apiKeyRoute явно регистрирует одну APIKeyRoutePolicy (middleware.APIKeyRouteAuthorizer — единственный источник истины):
// Конструкторы политик
apiKeyAny() // любой действительный ключ
apiKeyFullAccess() // только ключ FullAccess
apiKeyPlatform(caps...) // только platform key + указанные возможности
apiKeyRetrieve(base) / apiKeyChat(base) / apiKeyIngest(base) / ...При запуске assertAPIKeyPoliciesMatchRoutes проверяет, что каждая объявленная политика соответствует реально зарегистрированному маршруту; расхождение конфигурации приводит к panic. Типичные соответствия, подтверждённые router_api_key_capabilities_test.go:
| Маршрут | Требуемая возможность |
|---|---|
POST /sessions, POST /knowledge-chat/:session_id, POST /agent-chat/:session_id, GET /messages/:session_id/load | chat |
GET /agents, GET /agents/:id, GET /agents/:id/suggested-questions | read_agents |
POST/PUT/DELETE /agents, POST /agents/:id/copy | manage_agents |
PUT/DELETE /knowledge-bases/:id, POST /initialization/initialize/:kbId | manage_kbs |
POST /messages/search, GET /messages/chat-history-stats | message_history (не chat) |
GET /system/admin/settings | platform key + system_settings_read |
POST /system/admin/runtime/queues/:queue/tasks/:task_id/actions/:action | platform key + system_runtime_manage |
KB Allow-list
Когда KnowledgeBaseIDs непуст, ключ может дотянуться только до KB из списка (подтверждено knowledge_api_key_scope_test.go):
// Один KB за пределами области → 403
requireTenantAPIKeyKnowledgeBase(ctx, "kb-2") // scope содержит только kb-1 → forbidden
// Любой KB за пределами области в массовой операции → 403 целиком (частичное пересечение отклоняется)
requireTenantAPIKeyKnowledgeBases(ctx, "kb-1", "kb-2") // → forbiddenПрочие жёсткие ограничения: platform key не может создать другой platform key; субъект API Key не участвует в определении ownership (см. RBAC: роли, владение и матрица охранников).
Единый вход OIDC
Конфигурация
internal/config/config.go, OIDCAuthConfig:
| Параметр | Описание |
|---|---|
enable | Включён ли OIDC |
issuer_url | Ожидаемый Issuer, участвует в проверке id_token |
jwks_uri | Адрес набора открытых ключей подписи; переменная окружения OIDC_AUTH_JWKS_URI |
discovery_url | Адрес OpenID Connect Discovery (.well-known/openid-configuration) |
provider_display_name | Отображаемое имя кнопки входа |
client_id / client_secret | Учётные данные клиента (secret сериализуется как json:"-", не отдаётся фронтенду) |
authorization_endpoint / token_endpoint / user_info_endpoint | Явно заданные эндпоинты |
scopes | Запрашиваемые scope (например, openid email profile) |
user_info_mapping.username / .email | Сопоставление полей claims (по умолчанию name / email) |
Эндпоинты авторизации/token можно задать явно; даже если оба заполнены, при неполном issuer или jwks_uri информацию для проверки всё равно нужно дополнить через discovery. При отсутствии надёжной конфигурации проверки нельзя войти, просто разобрав полезную нагрузку id_token.
Маршруты (internal/router/routes_auth_tenant.go):
r.GET("/auth/oidc/config", handler.GetOIDCConfig) // фронтенд проверяет, включено ли
r.GET("/auth/oidc/url", handler.GetOIDCAuthorizationURL) // получить URL авторизации
r.GET("/auth/oidc/start", handler.OIDCStart) // сразу 302 для начала входа
r.GET("/auth/oidc/callback", handler.OIDCRedirectCallback) // обратный вызов с кодом авторизацииКорпоративный портал может напрямую ссылаться на /api/v1/auth/oidc/start: бэкенд возвращает 302 с переходом в IdP и строит адрес обратного вызова по origin запроса. При развёртывании за обратным прокси следует корректно передавать внешние scheme/host и зарегистрировать соответствующий адрес обратного вызова в IdP. Этот интерфейс не принимает произвольную цель перехода после входа.
Процесс и проектные решения по безопасности
internal/application/service/user.go:
GetOIDCAuthorizationURL: генерирует 24-байтовый случайныйnonce, с помощьюsecutils.SignOIDCStateподписывает{nonce, redirect_uri}внутрь state (защита от CSRF / повторного воспроизведения / подмены адреса обратного вызова); nonce отдаётся через HttpOnly cookie (в JSON ответа опускается черезjson:"-").LoginWithOIDC: обмен кода авторизации на token → если используется id_token, сначала проверяется подпись, issuer, audience и срок действия через JWKS → объединяется информация о пользователе с эндпоинта UserInfo (сопоставляется поuser_info_mapping) → сопоставление локального пользователя по email; если не найден,provisionOIDCUserавтоматически создаёт аккаунт → выдаётся точно такая же локальная пара JWT, как при входе по паролю.
При наличии только access_token личность можно взять из UserInfo; без JWKS нельзя использовать непроверенные claims id_token, но при наличии access_token и эндпоинта UserInfo всё ещё можно пройти через UserInfo. Проверенный id_token может служить запасным вариантом, если запрос к UserInfo не удался.
Детали автоматического создания аккаунта:
- Режим тенанта берётся из
auth.default_tenant_mode(create_personal— автоматически создаётся личный тенант /tenantless— ожидание приглашения); - Варианты имени пользователя: OIDC username → префикс email →
oidc-user; при конфликте добавляется числовой суффикс-1..-20, при продолжающемся конфликте используется метка времени Unix; - Генерируется и записывается случайный пароль из 32 символов (пользователь его не знает и может входить только через OIDC);
- В ответе есть
is_new_userдля первичного онбординга в SPA; аккаунт сIsActive=falseотклоняется при входе.
Быстрый справочник по конфигурации
| Параметр | Значения | По умолчанию | Назначение |
|---|---|---|---|
auth.registration_mode | self_serve / invite_register / invite_only | self_serve | Режим регистрации по паролю (в system_settings БД можно менять на лету) |
auth.default_tenant_mode | create_personal / tenantless | create_personal | Создавать ли новому пользователю личный тенант автоматически |
tenant.enable_rbac | true / false | true | Принудительное применение RBAC / режим только журнала |
JWT_SECRET (переменная окружения) | произвольная строка | случайные 32 байта | Ключ HMAC для JWT |
SYSTEM_AES_KEY (переменная окружения) | ключ AES | не задано | Шифрование открытого текста API Key при сохранении в БД |
oidc.* | см. Конфигурация | выключено | Единый вход OIDC |
frontend_base_url / FRONTEND_BASE_URL | URL | относительный путь | Адрес страницы регистрации по ссылке-приглашению |
Tenant.StorageQuota | байты | 10737418240 (10GB) | Квота хранилища тенанта |
Механизм JWT
Реализовано в internal/application/service/user.go, используется github.com/golang-jwt/jwt (HMAC-SHA256).
Источник ключа
func getJwtSecret() string {
// 1) переменная окружения JWT_SECRET
// 2) иначе при запуске генерируется безопасный случайный ключ 32 байта (Base64); после перезапуска процесса старые token становятся недействительными
}Выдача (двойной token Access + Refresh)
accessClaims := jwt.MapClaims{
"user_id": user.ID,
"email": user.Email,
"tenant_id": activeTenantID, // область тенанта запроса жёстко зашита в token
"exp": time.Now().Add(24 * time.Hour).Unix(),
"iat": time.Now().Unix(),
"type": "access",
}
refreshClaims := jwt.MapClaims{
"user_id": user.ID,
"exp": time.Now().Add(7 * 24 * time.Hour).Unix(),
"type": "refresh",
}| Token | Срок действия | Основные Claims |
|---|---|---|
| Access Token | 24 часа | user_id / email / tenant_id / type=access |
| Refresh Token | 7 дней | user_id / type=refresh (без tenant_id) |
Оба token записываются в таблицу auth_tokens для серверного отзыва.
Проверка и обновление
Цепочка проверок ValidateToken:
- Алгоритм подписи должен принадлежать семейству HMAC (защита от атаки путаницы алгоритмов);
- token с
type=refreshне может использоваться как access token (isRefreshTokenClaims); - Проверка
IsRevokedпо таблицеauth_tokens(выход = запись об отзыве); - Из claims извлекается
user_idдля загрузки пользователя,tenant_idкак активный тенант.
Переключение тенанта = перевыпуск token: SwitchTenant после проверки активного членства в целевом тенанте (кроме суперпользователя между тенантами) сначала записывает целевое пространство в предпочтение «последний активный тенант» (Preferences.LastActiveTenantID), затем выдаёт пару token с новым claim tenant_id и по мере возможности отзывает старый refresh token. refresh JWT не содержит tenant_id, поэтому следующий вход и refresh попадают по этому предпочтению; если запись предпочтения не удалась, весь перевыпуск считается неудачным. При возврате к home записывается ID home (эквивалентно по семантике текущего места отправке 0 из SPA для очистки предпочтения). Это предпочтение уровня аккаунта: один перевыпуск меняет следующее место входа на всех устройствах.
Модель данных
Tenant (тенант / рабочее пространство)
internal/types/tenant.go:
type Tenant struct {
ID uint64 `json:"id" gorm:"primaryKey"`
Name string `json:"name"`
Description string `json:"description"`
Status string `json:"status" gorm:"default:'active'"`
RetrieverEngines RetrieverEngines `json:"retriever_engines" gorm:"type:json"`
Business string `json:"business"`
StorageQuota int64 `json:"storage_quota" gorm:"default:10737418240"` // по умолчанию 10GB
StorageUsed int64 `json:"storage_used" gorm:"default:0"`
ContextConfig *ContextConfig `json:"context_config" gorm:"type:jsonb"`
WebSearchConfig *WebSearchConfig `json:"web_search_config" gorm:"type:jsonb"`
ParserEngineConfig *ParserEngineConfig `json:"parser_engine_config" gorm:"type:jsonb"`
Credentials *CredentialsConfig `json:"credentials" gorm:"type:jsonb"`
StorageEngineConfig *StorageEngineConfig `json:"storage_engine_config" gorm:"type:jsonb"`
DefaultStorageBackendID *string `json:"default_storage_backend_id,omitempty"`
ChatHistoryConfig *ChatHistoryConfig `json:"chat_history_config" gorm:"type:jsonb"`
RetrievalConfig *RetrievalConfig `json:"retrieval_config" gorm:"type:jsonb"`
APIPrincipalConfig *APIPrincipalConfig `json:"-" gorm:"type:jsonb"`
// CreatedAt / UpdatedAt / DeletedAt (мягкое удаление)
}Тенант — точка привязки квот (StorageQuota / StorageUsed, по умолчанию 10GB) и разнообразных настроек уровня тенанта (движки поиска, веб-поиск, движки парсинга, учётные данные, движок хранилища, история чата и т. д.).
User (пользователь)
internal/types/user.go:
type User struct {
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
Username string `json:"username" gorm:"uniqueIndex;not null"`
Email string `json:"email" gorm:"uniqueIndex;not null"`
PasswordHash string `json:"-" gorm:"not null"`
Avatar string `json:"avatar"`
TenantID uint64 `json:"tenant_id" gorm:"index"` // предпочтительный/по умолчанию тенант
IsActive bool `json:"is_active" gorm:"default:true"`
CanAccessAllTenants bool `json:"can_access_all_tenants" gorm:"default:false"` // суперпользователь между тенантами
IsSystemAdmin bool `json:"is_system_admin" gorm:"default:false;index"` // администратор платформы
Preferences UserPreferences `json:"preferences" gorm:"type:jsonb"`
}
type UserPreferences struct {
// ID последнего активного тенанта, используется при входе для восстановления контекста
LastActiveTenantID *uint64 `json:"last_active_tenant_id,omitempty"`
}Два особых флага:
CanAccessAllTenants: суперпользователь между пространствами. Действует, только когда истинны оба переключателя —CanAccessAllTenantsв строке пользователя иtenant.enable_cross_tenant_access/WEKNORA_TENANT_ENABLE_CROSS_TENANT_ACCESSуровня развёртывания (IsCrossTenantSuperuser()вmiddleware/access.goсначала проверяет конфигурацию, затем пользователя; при выключенной конфигурации это поле в ответе входа также затирается в false). После активации можно обходить проверки ролей пространства и обращаться к эндпоинтам между пространствами, таким как/tenants/all,/tenants/search. Обратите внимание:POST /tenants(создание пространства) не относится к эндпоинтам между пространствами и доступен любому вошедшему пользователю (с ограничениями по политике самостоятельного создания и квоте).IsSystemAdmin: администратор уровня платформы (system admin), независимый от любой роли тенанта, используется для контрольной плоскости/system/admin/*. Он управляет всем развёртыванием, а не отдельным пространством; как получить первого и что можно делать — см. Управление платформой и системный администратор.
TenantMember и роли тенанта
internal/types/tenant_member.go:
type TenantRole string
const (
TenantRoleOwner TenantRole = "owner" // полный контроль: удаление тенанта, передача владения, управление API Key и участниками
TenantRoleAdmin TenantRole = "admin" // управление участниками, моделями, векторными хранилищами, MCP, IM и прочей инфраструктурой тенанта
TenantRoleContributor TenantRole = "contributor" // создание KB / Agent, редактирование созданных собой ресурсов
TenantRoleViewer TenantRole = "viewer" // только чтение
)
var tenantRoleLevel = map[TenantRole]int{
TenantRoleOwner: 40, TenantRoleAdmin: 30,
TenantRoleContributor: 20, TenantRoleViewer: 10,
}
func (r TenantRole) HasPermission(required TenantRole) bool {
return r.Level() >= required.Level()
}type TenantMember struct {
ID uint64
UserID string
TenantID uint64
Role TenantRole // по умолчанию contributor
Status TenantMemberStatus // active / invited / suspended
InvitedBy *string
JoinedAt time.Time
}В ответе входа возвращается список проекций Membership{TenantID, TenantName, Role}, по которому фронтенд отрисовывает переключатель рабочих пространств.
TenantAPIKey (API Key)
internal/types/tenant_api_key.go:
type TenantAPIKey struct {
ID uint64
TenantID *uint64 // для platform key равно NULL
ScopeType APIKeyScopeType // "tenant" | "platform"
Name string
KeyHash string `json:"-" gorm:"uniqueIndex"` // хеш для поиска в таблице
APIKey string // открытый текст (перед сохранением в БД шифруется AES-256-GCM, см. BeforeSave/AfterFind)
FullAccess bool // полный доступ (не ограничивается capabilities)
KnowledgeBaseIDs StringArray // KB allow-list (пусто = без ограничений)
Capabilities StringArray // список возможностей
LastUsedAt / ExpiresAt / RevokedAt *time.Time
}- Шифрование при сохранении: при настроенном
SYSTEM_AES_KEYхукBeforeSaveсохраняет столбецapi_keyзашифрованным через AES-GCM,AfterFindавтоматически расшифровывает; поиск в таблице всегда идёт по необратимомуKeyHash. - Процесс проверки: запрос несёт
X-API-Key→ вычисляется хеш → поиск поKeyHash→ проверкаRevokedAt/ExpiresAt→ в context внедряетсяTenantAPIKeyScope{KeyID, ScopeType, FullAccess, KnowledgeBaseIDs, Capabilities}, далее читается черезtypes.TenantAPIKeyScopeFromContext.
Organization (организация / общее пространство)
internal/types/organization.go:
type Organization struct {
ID string
Name / Description / Avatar string
OwnerID string // пользователь-создатель
OwnerTenantID uint64 // тенант, владеющий организацией
InviteCode string `gorm:"uniqueIndex"` // код-приглашение организации
InviteCodeExpiresAt *time.Time
InviteCodeValidityDays int // допускаются 0 (бессрочно)/1/7/30, по умолчанию 7
RequireApproval bool // для присоединения нужно одобрение
Searchable bool // доступна ли для обнаружения через поиск
MemberLimit int // по умолчанию 50
}
type OrganizationTenantMember struct { // единица участника — «тенант»
OrganizationID string
TenantID uint64
Role OrgMemberRole // admin / editor / viewer, по умолчанию viewer
RepresentativeUserID string // представительный пользователь (информационное поле)
}
const (
OrgRoleAdmin OrgMemberRole = "admin" // полный контроль организации и общих ресурсов
OrgRoleEditor OrgMemberRole = "editor" // может редактировать содержимое общих KB, но не может менять настройки организации
OrgRoleViewer OrgMemberRole = "viewer" // только чтение
)Справочник по реализации
Все приведённые ниже пути указаны относительно корня репозитория:
| Слой | Файл |
|---|---|
| Модель тенанта | internal/types/tenant.go |
| Модель пользователя | internal/types/user.go |
| Участники и роли тенанта | internal/types/tenant_member.go |
| Приглашения тенанта | internal/types/tenant_invitation.go |
| Модель API Key и возможности | internal/types/tenant_api_key.go |
| Модели организации / общего доступа | internal/types/organization.go |
| Обработчик регистрации / входа | internal/handler/auth.go |
| Обработчик регистрации по приглашению | internal/handler/auth_register_by_invite.go |
| Обработчики участников / приглашений / ссылок-приглашений | internal/handler/tenant_member.go, tenant_invitation.go, tenant_invite_link.go |
| Обработчик организации | internal/handler/organization.go |
| Сервис JWT / OIDC / пользователей | internal/application/service/user.go |
| Сервис организации / общего доступа к KB | internal/application/service/organization.go, kbshare.go |
| Middleware RBAC | internal/middleware/rbac.go |
| Матрица охранников маршрутов RBAC | internal/router/rbac.go |
| Конфигурация аутентификации | internal/config/config.go (AuthConfig / OIDCAuthConfig / TenantConfig) |
