Перейти к содержанию

Организация как граница данных

Сервис «Пространство» хранит проекты небольших компаний. В одной базе работают студия «Север» и мастерская «Юг». Анна состоит в обеих компаниях, а Борис — только в «Юге». После входа Анна выбирает компанию и видит её проекты. Переход между компаниями не создаёт второго пользователя: меняется область, в которой разрешено работать.

В этом уроке определим границу данных до написания обработчиков. Вам понадобятся основы C#, HTTP и SQL. Курс использует ASP.NET Core 10, Npgsql 10.0.0 и PostgreSQL 18; версии проверены по первичной документации. Учебный проект предназначен для локальной работы. Его выполнение, установка зависимостей и обращения к базе при подготовке уроков не проводились: состояние примеров — not_run, все приведённые ответы являются ожидаемыми.

Один человек и несколько членств

Пользователь отвечает на вопрос «кто выполняет запрос». Организация отвечает на вопрос «чьи данные обрабатываются». Членство связывает эти два понятия и задаёт роль. Нельзя хранить единственное поле organization_id в записи пользователя, если человеку разрешено состоять в двух компаниях: смена компании тогда уничтожала бы прежнюю связь либо вынуждала создавать копии личности.

Рассмотрим исходные данные. Идентификаторы пользователя приходят от системы входа и остаются строками: внешняя система не обязана выдавать UUID. Идентификаторы организаций и прикладных объектов создаёт наш сервис. Названия «Север» и «Юг» предназначены человеку, но не используются для проверки доступа. Компания может изменить название, не меняя принадлежность старых проектов.

Человек Организация Роль Разрешённое действие
Анна, anna Север owner Проекты и управление составом
Анна, anna Юг viewer Просмотр проектов
Борис, boris Юг owner Проекты и управление составом

Из таблицы видно, почему роль нельзя без уточнения компании положить в глобальный токен. Анна одновременно владелец и наблюдатель. Утверждение «Анна — owner» не даёт ответа, может ли она изменить проект «Юга». Для такой операции нужны подтверждённая личность, выбранная организация и её текущее членство. Эти сведения образуют доверенный контекст запроса.

Идентификатор в адресе ещё не даёт доступ

В учебном API компания явно указана в маршруте. Такой адрес удобно читать в журнале и легко передавать из интерфейса переключения компаний. Клиент вправе предложить любой UUID, включая UUID соседней организации. Поэтому маршрут является входным параметром, а не доказательством принадлежности пользователя к компании.

Ниже показан полный контракт двух будущих запросов; UUID принадлежат локальным исходным данным курса. Заголовок X-Dev-User используется только локальным имитатором входа, который мы добавим в отдельном уроке.

GET /orgs/11111111-1111-1111-1111-111111111111/projects
X-Dev-User: anna

GET /orgs/22222222-2222-2222-2222-222222222222/projects
X-Dev-User: boris

Ожидаемая разница заключается в содержимом ответа, а не в имени пользователя в заголовке. Первый запрос читает проекты «Севера», второй — проекты «Юга». Попытка Бориса обратиться к первому адресу должна получить 404: учебный контракт не раскрывает существование организаций постороннему человеку. Отсутствие входа получает 401, а недостаточная роль внутри доступной компании — 403.

Обратите внимание на различие этих ситуаций. Если каждый отказ обозначать как «объект не найден», владельцу будет трудно понять, почему он видит документ, но не может его изменить. Если каждому постороннему сообщать «компания найдена, но доступ запрещён», API позволит собирать сведения о существующих клиентах. Выбранные ответы согласованы с тем, какую информацию уже вправе видеть человек.

Общая база и отдельные строки

Курс использует одну базу и общие таблицы. В каждой прикладной строке сохраняется organization_id. Этот вариант позволяет показать ограничения, транзакции и Row-Level Security на небольшом примере. Он не означает, что отдельная база для компании всегда хуже. Выбор зависит от объёма, требований к восстановлению, договорных условий и способа эксплуатации.

Для двух небольших компаний общие таблицы удобны: изменение схемы применяется один раз, а общий отчёт по состоянию сервиса не требует подключения к сотне баз. Цена такого удобства — необходимость последовательно сохранять границу. Если один обработчик забудет условие по организации, физически соседние строки останутся доступными одному соединению. Позже добавим дополнительную защиту на уровне PostgreSQL.

Принадлежность должна распространяться и на связи. Задача проекта хранит организацию и идентификатор проекта. Простая ссылка только на project_id доказывает существование проекта, но не доказывает, что задача и проект принадлежат одной компании. Составной внешний ключ решит эту задачу в седьмом уроке. Сейчас важно увидеть, что граница компании — свойство модели, а не только дополнительный фильтр списка.

Не вся таблица обязана принадлежать компании. Справочник тарифов в следующем курсе будет общим. Таблица пользователей также общая, потому что одна личность имеет несколько членств. Для каждого нового объекта задавайте вопрос: компания владеет этой записью или только ссылается на общий справочник? Ответ определяет ключи, доступ и правила удаления.

Что войдёт в сквозной пример

Мы последовательно добавим проекты, задачи, роли, приглашения, защиту конкурирующих изменений, фоновые экспорты и аудит. Исходные имена и UUID сохраняются во всех уроках. Заключительный код находится в архиве примера. Архив содержит целые файлы; листинги в статьях отдельно обозначены как полные определения либо фрагменты этих файлов.

Сначала достаточно мысленно проследить четыре обращения: Анна читает «Север», Анна читает «Юг», Борис читает «Юг», Борис пытается читать «Север». Первые три разрешены, последнее отклоняется. Затем замените чтение изменением: Анна может редактировать «Север», но её роль в «Юге» такого права не даёт. Получилась небольшая матрица поведения, которая будет объяснять решения в коде.

У проекта есть ещё одна важная граница: учебный вход по заголовку не становится способом входа для настоящих клиентов. Полный пример отказывается запускаться вне Development и слушает loopback. Настоящий сервер должен получать личность из проверенного механизма входа. При этом модель членства и последующая проверка организации остаются применимыми: способ входа меняет источник личности, но не отменяет проверку доступа к компании.

Следующий урок создаёт каркас API и соединение с PostgreSQL. После него у модели появится техническое место: конфигурация, обработчики маршрутов и явный доступ к базе. До этого шага не нужно добавлять автоматическое определение компании по имени, cookie или произвольному заголовку: каждый такой механизм всё равно должен закончиться той же проверкой членства.

Оглавление серии