Skip to content

Авторизация и управление доступом

Система авторизации построена на трёх уровнях: SpiceDB (граф связей) + выражения-политики (expr) + PostgreSQL (хранение ролей и настроек). Синхронизация между PostgreSQL и SpiceDB реализована через Transactional Outbox.

Общая архитектура

В системе три основных потока данных:

  1. Проверка доступа — пользователь вызывает команду, авторизатор проверяет роли и политики
  2. Импорт данных — Puller тянет данные из внешней системы, SyncService пишет в PG + outbox, Worker доставляет в SpiceDB
  3. Администрирование — Admin API управляет политиками команд и связями

Проверка доступа к команде

CheckCommand — точка входа авторизации. Вызывается при каждом обращении пользователя к команде плагина.

Фаза 1: Роли (SpiceDB)

Роли проверяются через SpiceDB CheckPermission API. Три уровня:

УровеньSpiceDB object typeЛогикаПример
Systemsystem_roleОдна рольsystem_role:superadmin#is_member@user:123
Globalglobal_roleВсе роли (AND)global_role:moderator#is_member@user:123
Pluginplugin_roleОдна рольplugin_role:schedule_editor#is_member@user:123

Глобальные роли используют AND-логику: пользователь должен иметь все перечисленные роли.

Фаза 2: Политики (выражения)

Политики хранятся в таблице plugin_command_settings и представляют собой выражения на языке expr, возвращающие bool.

Контекст выражения (user объект):

ПолеТипОписание
user.idint64ID пользователя
user.external_idstringВнешний ID (из системы университета)
user.groups[]stringУчебные группы (из SpiceDB LookupResources)
user.roles[]stringРоли (из user_roles)
user.primary_channelstringОсновной канал (TELEGRAM / DISCORD)
user.localestringЯзык (ru / en)
user.nationality_typestringdomestic / foreign
user.funding_typestringbudget / contract
user.education_formstringfull_time / part_time / remote

Встроенные функции:

ФункцияОписание
check(permission, objectType, objectID)SpiceDB CheckPermission
is_member(objectType, objectID)Проверка связи member
has_role(name)Есть ли роль в user.roles
has_any_role(name1, name2, ...)Есть ли хотя бы одна из ролей

Примеры выражений:

has_role("teacher")
user.funding_type == "budget" && has_any_role("admin", "moderator")
check("view_all_students", "stream", "STREAM001")
user.primary_channel == "TELEGRAM" && is_member("study_group", "972203")

SubjectContext

Контекст пользователя собирается параллельно и кэшируется (TTL 30 секунд).

SpiceDB: схема и связи

Организационная иерархия

Схема (deployments/schema.zed) описывает иерархию университета с каскадными правами:

Каждый уровень связан с родителем через relation parent. Права наследуются вверх по иерархии:

permission admin = dean + parent->admin
permission view_all = head + staff + parent->view_all

Типы связей

ОбъектСвязи (relations)Permissions
facultydean, staff, parentadmin, view_all
departmenthead, staff, parentadmin, view_all
programdirector, staff, parentadmin, view_all
streamcurator, teacher, foreign_teacher, parentadmin, view_all, view_foreign
study_groupcurator, teacher, foreign_teacher, member, parentadmin, view_all, view_foreign, view_own_data
subgroupmember, parentview_own_data
system_rolememberis_member
global_rolememberis_member
plugin_rolememberis_member
nationality_categorymember, curatorview_members

Примеры tuples

study_group:972203#member@user:ext-12345
faculty:IT#dean@user:ext-00001
stream:S001#teacher@user:ext-67890
stream:S001#parent@program:CS101
nationality_category:foreign#curator@user:ext-11111
system_role:superadmin#member@user:ext-00001

Transactional Outbox

Синхронизация PostgreSQL → SpiceDB реализована через паттерн Transactional Outbox, исключающий рассогласование данных при сбоях.

Проблема dual-write

Решение: outbox в одной транзакции

Операции outbox

ОперацияОписаниеSpiceDB метод
TOUCHСоздать/обновить tuplesWriteRelationships (OPERATION_TOUCH)
DELETEУдалить конкретные tuplesWriteRelationships (OPERATION_DELETE)
DELETE_BY_OBJECTУдалить все связи объектаDeleteRelationships по object filter
DELETE_BY_SUBJECTУдалить все связи субъектаDeleteRelationships по subject filter
REPLACEАтомарная замена: delete + touchDeleteRelationships + WriteRelationships

Обработка ошибок

  • Ретраи: до 10 попыток с экспоненциальным backoff (2s, 4s, 8s, ..., max 5 минут)
  • Идемпотентность: все операции SpiceDB идемпотентны (TOUCH = upsert, DELETE несуществующего = no-op)
  • Блокировка: FOR UPDATE SKIP LOCKED позволяет нескольким worker'ам работать параллельно
  • Порядок: обработка по id ASC сохраняет причинно-следственный порядок

Таблица authz_outbox

sql
CREATE TABLE authz_outbox (
    id            BIGSERIAL     PRIMARY KEY,
    operation     VARCHAR(30)   NOT NULL,   -- TOUCH, DELETE, ...
    payload       JSONB         NOT NULL,   -- tuples и/или фильтры
    created_at    TIMESTAMPTZ   DEFAULT now(),
    processed_at  TIMESTAMPTZ,              -- NULL до обработки
    attempts      INT           DEFAULT 0,
    last_error    TEXT,
    locked_until  TIMESTAMPTZ               -- backoff-блокировка
);

Синхронизация университетских данных

Данные поступают из внешней университетской системы через pull-модель: Puller периодически опрашивает внешний источник и прогоняет данные через SyncService, который в одной PG-транзакции записывает данные и создаёт outbox-записи для SpiceDB.

Архитектура импорта

DataSource — интерфейс внешнего источника

DataSource — контракт, который должен реализовать разработчик для подключения конкретной внешней системы (REST API, SOAP, прямой доступ к БД и т.д.):

go
type DataSource interface {
    FetchPersons(ctx)              ([]PersonInput, error)
    FetchCourses(ctx)              ([]CourseInput, error)
    FetchSemesters(ctx)            ([]SemesterInput, error)
    FetchFaculties(ctx)            ([]FacultyInput, error)
    FetchDepartments(ctx)          ([]HierarchyNodeInput, error)
    FetchPrograms(ctx)             ([]HierarchyNodeInput, error)
    FetchStreams(ctx)              ([]HierarchyNodeInput, error)
    FetchGroups(ctx)               ([]HierarchyNodeInput, error)
    FetchSubgroups(ctx)            ([]HierarchyNodeInput, error)
    FetchTeacherPositions(ctx)     ([]TeacherPositionInput, error)
    FetchStudentPositions(ctx)     ([]StudentPositionInput, error)
    FetchStudentSubgroups(ctx)     ([]StudentSubgroupInput, error)
    FetchTeachingAssignments(ctx)  ([]TeachingAssignmentInput, error)
    FetchAdminAppointments(ctx)    ([]AdminAppointmentInput, error)
}

Каждый метод возвращает полный текущий список сущностей. Возврат (nil, nil) означает, что источник не предоставляет данный тип — шаг будет пропущен.

В проекте есть StubDataSource с полями BaseURL и Token — заглушка, в которой нужно реализовать тела методов.

Puller — фоновый импорт

Puller запускается как горутина и опрашивает DataSource с заданным интервалом.

Конфигурация (config.yaml):

yaml
university_sync:
  enabled: true
  interval: "1h"
  base_url: "https://university-api.example.com"
  token: "secret"

Порядок синхронизации — 4 фазы с учётом зависимостей:

При ошибке отдельной сущности Puller логирует её и продолжает — не останавливает весь цикл. Метод PullOnce() позволяет запустить синхронизацию вручную (для тестов или admin API).

SyncService — запись в PostgreSQL + outbox

Каждый метод SyncService выполняет upsert в PG и (для сущностей с авторизацией) создаёт outbox-запись — всё в одной транзакции:

МетодТаблица PGOutboxSpiceDB tuple
SyncPersonpersons
SyncCoursecourses
SyncSemestersemesters
SyncFacultyfaculties
SyncTeacherPositionteacher_positions
SyncHierarchyNodedepartments, programs, streams, groups, subgroupsREPLACEdepartment:CS#parent@faculty:IT
SyncStudentPositionstudent_positionsDELETE_BY_SUBJECT + TOUCHstudy_group:972203#member@user:ext-123
SyncStudentSubgroupstudent_subgroupsTOUCHsubgroup:A#member@user:ext-123
SyncTeachingAssignmentteaching_assignmentsTOUCHstream:S001#teacher@user:ext-456
SyncAdminAppointmentadministrative_appointmentsTOUCHfaculty:IT#dean@user:ext-001

Справочные сущности (persons, courses, semesters, faculties, teacher_positions) не создают SpiceDB-tuples — они не участвуют в графе авторизации напрямую.

Admin API для ручного импорта

Помимо pull-модели, доступны HTTP-эндпоинты для batch-импорта из внешних скриптов. Все принимают JSON-массив и возвращают {total, success, errors}:

POST /api/admin/university/persons
POST /api/admin/university/courses
POST /api/admin/university/semesters
POST /api/admin/university/faculties
POST /api/admin/university/departments
POST /api/admin/university/programs
POST /api/admin/university/streams
POST /api/admin/university/groups
POST /api/admin/university/subgroups
POST /api/admin/university/teacher-positions
POST /api/admin/university/student-positions
POST /api/admin/university/student-subgroups
POST /api/admin/university/teaching-assignments
POST /api/admin/university/admin-appointments

Все эндпоинты защищены Bearer-токеном (admin.api_key). HTTP-коды: 200 — все ок, 206 — частичный успех, 422 — все записи с ошибками.

Настройка через Admin API

Управление правами команд

GET    /api/admin/plugins/{id}/commands/settings        — все настройки команд плагина
PUT    /api/admin/plugins/{id}/commands/{cmd}/enabled    — включить/выключить команду
PUT    /api/admin/plugins/{id}/commands/{cmd}/policy     — задать expression-политику

Включение/выключение:

json
{"enabled": false}

Установка политики:

json
{"expression": "has_role('teacher') || check('admin', 'faculty', 'IT')"}

Пустая строка "" удаляет политику (команда становится доступна всем, если enabled = true).

Управление связями SpiceDB

POST   /api/admin/relationships             — создать связь
DELETE /api/admin/relationships             — удалить связь
GET    /api/admin/relationships/lookup      — поиск ресурсов по permission
GET    /api/admin/schema/definitions        — текущая схема SpiceDB

Кэширование

КэшКлючTTLСброс
SubjectContextuser_id30 секInvalidateUser()
Command policyplugin_id + command60 секInvalidateCommandPolicy()
Compiled expressionsexpression stringбез TTLsync.Map, in-memory

Eventual consistency

Между коммитом PG-транзакции и обработкой worker'ом SpiceDB может быть неактуален (обычно менее секунды). Это допустимо, поскольку:

  1. SubjectContext уже кэшируется с TTL 30 секунд
  2. Для данного домена (расписание, доступ к учебным данным) задержка в секунды не критична
  3. SpiceDB-операции идемпотентны — ретраи безопасны