# Организации, команды и изоляция арендаторов

Арендатор — организация, бизнес-записи которой отделены от других организаций. Elang использует явные идентификаторы команд, проверки текущего членства и транзакции SQL. Он не скрывает идентичность арендатора в общей для процесса переменной «текущая команда». Это особенно важно для очередей и долгоживущих процессов, обслуживающих множество пользователей.

## Подготовка таблиц и создание команды

```bash
php elang platform:setup
```

```php
$teams = $app->container->get(Elang\Tenancy\Teams::class);
$team = $teams->create($user, 'Northwind');
$mine = $teams->list($user);
```

Исполнитель должен иметь действующую подтверждённую учётную запись без временного пароля. При создании команды её создателю назначается роль владельца. ID команды содержат 32 случайных шестнадцатеричных символа. Их непредсказуемость не является границей авторизации: каждая операция всё равно проверяет членство.

| Роль | Чтение данных | Запись данных | Приглашение и удаление обычных участников | Назначение ролей и передача владения |
| --- | --- | --- | --- | --- |
| Владелец | Да | Да | Да | Да |
| Администратор | Да | Да | Да | Нет |
| Участник | Да | Да | Нет | Нет |
| Наблюдатель | Да | Нет | Нет | Нет |

Только владелец назначает или удаляет администраторов. Владелец не может удалить себя или понизить собственную роль: сначала передайте владение существующему участнику. Передача в одной транзакции меняет роль прежнего владельца на администратора, а выбранного участника — на владельца.

## Явное ограничение бизнес-запросов

```php
$result = $teams->within(
    $user,
    $teamId,
    'write',
    function (Doctrine\DBAL\Connection $db, array $team) use ($title) {
        $id = bin2hex(random_bytes(16));
        $db->insert('projects', [
            'id' => $id,
            'team_id' => $team['id'],
            'title' => $title,
        ]);
        return $id;
    },
);
```

Создайте `projects` в собственной миграции, включая внешний ключ и индекс `team_id`. Используйте привязанные значения. Каждое чтение, обновление, удаление, получение связей, экспорт и задание должно ограничивать SQL авторизованным `team_id`. Параметр маршрута или переданный `team_id` никогда не должен перезаписывать это значение. Стартовый проект SaaS Elang демонстрирует этот подход на записях проектов и оптимистических версиях.

`within()` открывает транзакцию, блокирует строку команды, заново загружает членство и проверяет запрошенное разрешение. Изменения членства используют ту же блокировку, поэтому отзыв доступа и бизнес-запись сериализуются. Этот консервативный подход может сериализовать интенсивные операции записи внутри одной команды. Для больших нагрузок измеряйте конкуренцию за блокировку и проектируйте более узкие блокировки предметной области, сохраняя корректность авторизации. Не удерживайте транзакцию во время доставки почты, медленных вызовов S3 или удалённых платёжных запросов; используйте outbox или сагу при пересечении границ систем.

Неучастник получает 404; участник с недостаточной ролью — 403. Это позволяет не раскрывать существование организации посторонним. Кешированные роли интерфейса предназначены только для отображения. Каждую операцию определяют серверные проверки.

## Приглашения

```php
$raw = $teams->invite($owner, $teamId, 'member@example.com', 'member');
$accepted = $teams->accept($signedInVerifiedUser, $raw);
```

Роли приглашений — `admin`, `member` или `viewer`; только владелец может пригласить администратора. Токены истекают через семь дней и хранятся только как хеши SHA-256. Выдача нового приглашения для той же команды и электронной почты заменяет старое приглашение. Для принятия нужна вошедшая подтверждённая учётная запись с совпадающим нормализованным адресом электронной почты. Токен используется один раз. Роль существующего участника не повышается незаметно при принятии ещё одного приглашения.

Сервис один раз возвращает исходный токен, чтобы приложение могло доставить закрытое приглашение. Не помещайте его в аналитику или журналы. Используйте страницу принятия с запретом хранения и передачи источника перехода (no-store, no-referrer). Стартовый проект SaaS показывает закрытую ссылку приглашения, которой можно поделиться. Настройте реальный почтовый транспорт перед добавлением автоматической доставки писем.

## Изменение членства

```php
$teams->role($owner, $teamId, $memberId, 'viewer');
$teams->remove($owner, $teamId, $memberId);
$teams->transfer($owner, $teamId, $nextOwnerId);
```

Задания очереди должны переносить ID команды, пользователя и записи, заново загружать исполнителя и повторно входить в `within()` при выполнении. Членство могло измениться после отправки задания. Для задач, принадлежащих сервису, определите отдельную доверенную политику сервиса вместо создания вымышленного пользователя-администратора.

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

Далее: [файлы](/docs/v0.1/files), [уведомления](/docs/v0.1/notifications) и [эталонное SaaS-приложение](/docs/v0.1/saas).
