Зоны доступа и разграничение данных между пользователями

Разграничение данных между пользователями в приложениях, использующих IndexedDB и Dexie.js, строится вокруг принципа логического разделения записей внутри одной базы данных. В отличие от серверных систем, здесь отсутствует встроенный механизм строгой изоляции пользователей, поэтому вся модель безопасности реализуется на уровне структуры данных, индексов и правил доступа в коде приложения.

Базовый принцип изоляции данных

Основной подход к разделению данных в Dexie.js заключается в добавлении идентификатора владельца ко всем сущностям, которые подлежат разграничению. Обычно используется поле userId, tenantId или аналогичный ключ, определяющий принадлежность записи.

const db = new Dexie("appDatabase");

db.version(1).stores({
  notes: "++id, userId, title, createdAt",
});

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

const currentUserId = "user_123";

const userNotes = await db.notes
  .where("userId")
  .equals(currentUserId)
  .toArray();

Подобный подход формирует логическую «зону доступа» внутри одной таблицы, где границы определяются не самой базой данных, а дисциплиной запросов.

Индексация и производительность при сегментации данных

При работе с разграничением данных критически важным становится правильное проектирование индексов. Поле идентификатора пользователя должно быть индексировано, иначе фильтрация приведёт к полному сканированию таблицы.

Dexie.js позволяет комбинировать составные индексы для повышения эффективности запросов:

db.version(2).stores({
  notes: "++id, [userId+createdAt], userId, createdAt, title",
});

Использование составного индекса [userId+createdAt] позволяет эффективно выполнять выборки, ограниченные пользователем и диапазоном времени:

const recentNotes = await db.notes
  .where("[userId+createdAt]")
  .between(
    [currentUserId, Dexie.minKey],
    [currentUserId, Dexie.maxKey]
  )
  .toArray();

Такой подход формирует естественную сегментацию данных внутри одного хранилища без необходимости физического разделения таблиц.

Модель «тенантов» и мультиарендность

В приложениях с несколькими организациями или рабочими пространствами используется расширенная модель, основанная на tenantId. В этом случае каждая запись принадлежит не только пользователю, но и пространству данных.

db.version(3).stores({
  projects: "++id, tenantId, userId, name, updatedAt",
});

Запросы строятся с обязательным учетом обоих уровней изоляции:

const tenantProjects = await db.projects
  .where("[tenantId+updatedAt]")
  .between(
    [currentTenantId, Dexie.minKey],
    [currentTenantId, Dexie.maxKey]
  )
  .toArray();

Модель тенантов позволяет реализовывать сценарии совместной работы, сохраняя логическое разделение контекстов.

Абстрагирование доступа через слой репозитория

Прямое использование запросов к Dexie.js приводит к риску нарушения границ доступа. Для устранения этой проблемы вводится слой репозитория, который инкапсулирует правила фильтрации.

class NotesRepository {
  constructor(db, getUserId) {
    this.db = db;
    this.getUserId = getUserId;
  }

  async getAll() {
    return this.db.notes
      .where("userId")
      .equals(this.getUserId())
      .toArray();
  }

  async add(note) {
    return this.db.notes.add({
      ...note,
      userId: this.getUserId(),
      createdAt: Date.now(),
    });
  }
}

Такой слой формирует централизованную точку контроля, где закрепляется правило обязательного добавления идентификатора владельца.

Хуки Dexie.js как механизм контроля целостности

Dexie.js предоставляет механизм lifecycle hooks, который позволяет принудительно внедрять ограничения на уровне операций записи.

db.notes.hook("creating", (primKey, obj) => {
  obj.userId = currentUserId;
  obj.createdAt = Date.now();
});

Хуки позволяют устранить класс ошибок, связанных с пропуском поля идентификатора пользователя на уровне бизнес-логики.

Дополнительно можно реализовать проверку целостности при обновлении:

db.notes.hook("updating", (mods, primKey, obj) => {
  if (obj.userId !== currentUserId) {
    throw new Error("Недопустимое изменение зоны доступа");
  }
});

Таким образом обеспечивается защита от случайного или некорректного изменения границ данных.

Разделение данных через отдельные базы

Альтернативный подход к изоляции заключается в создании отдельной базы Dexie.js для каждого пользователя. Имя базы формируется динамически:

function createUserDb(userId) {
  const db = new Dexie(`app_${userId}`);

  db.version(1).stores({
    notes: "++id, title, createdAt",
  });

  return db;
}

Этот метод обеспечивает физическое разделение данных, однако усложняет миграции схемы и управление несколькими базами одновременно. Также увеличивается нагрузка на инфраструктуру приложения при большом количестве пользователей.

Ограничения модели безопасности на стороне клиента

Разграничение данных в Dexie.js носит исключительно прикладной характер. Любая логика, реализованная в браузере, не может считаться защитой от злоумышленника, имеющего доступ к среде выполнения.

Основные уязвимости модели:

  • возможность подмены userId в запросах;
  • прямой доступ к IndexedDB через DevTools;
  • модификация JavaScript-кода в рантайме;
  • экспорт данных через API браузера.

По этой причине зона доступа в Dexie.js рассматривается как средство организации данных, а не как механизм безопасности.

Кэширование и риск утечки данных между зонами

При реализации офлайн-режимов часто используется кэширование данных нескольких пользователей в одной базе. В таких случаях критически важно очищать состояние при смене пользователя.

async function switchUser(newUserId) {
  await db.transaction("rw", db.notes, async () => {
    await db.notes.clear();
  });

  currentUserId = newUserId;
}

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

Составные стратегии фильтрации

В сложных приложениях фильтрация данных строится на комбинации нескольких параметров: пользователь, роль, статус доступа, уровень видимости.

db.version(4).stores({
  documents: "++id, userId, visibility, role, updatedAt",
});

Пример выборки данных с учетом зоны доступа и уровня видимости:

const accessibleDocs = await db.documents
  .where("userId")
  .equals(currentUserId)
  .and(doc => doc.visibility !== "private")
  .toArray();

Такая модель формирует многоуровневую фильтрацию, где зона доступа пересекается с бизнес-логикой приложения.

Версионирование схем и влияние на разграничение данных

Изменение структуры таблиц может влиять на логику изоляции. Добавление новых индексов требует пересмотра стратегии хранения идентификаторов пользователей.

db.version(5).stores({
  notes: "++id, userId, workspaceId, createdAt, updatedAt",
}).upgrade(tx => {
  return tx.table("notes").toCollection().modify(note => {
    if (!note.workspaceId) {
      note.workspaceId = "default";
    }
  });
});

Миграции должны учитывать сохранение целостности зон доступа, иначе возможны ситуации смешивания данных разных контекстов.

Расширенная модель с ролями и правами

Для более сложных сценариев вводится система ролей, которая влияет на доступ к данным внутри одной зоны.

db.version(6).stores({
  tasks: "++id, userId, role, status, createdAt",
});

Фильтрация с учетом роли:

const tasks = await db.tasks
  .where("userId")
  .equals(currentUserId)
  .filter(task => task.role !== "guest")
  .toArray();

Ролевая модель дополняет изоляцию данных, добавляя поведенческий уровень контроля.

Объединение локальных и удалённых правил доступа

В приложениях с синхронизацией через сервер Dexie.js часто используется гибридная модель, где локальная зона доступа дублируется серверной политикой.

Локальный слой отвечает за организацию данных, серверный — за реальную безопасность и авторизацию. Несоответствие между ними приводит к рассинхронизации правил доступа, что требует строгого согласования схемы идентификаторов и политик фильтрации.

Итеративная фильтрация и оптимизация запросов

При большом объеме данных фильтрация по userId может становиться узким местом. Использование индексов и предварительной агрегации позволяет снизить нагрузку:

const stream = db.notes
  .where("[userId+createdAt]")
  .between([currentUserId, 0], [currentUserId, Date.now()])
  .reverse()
  .limit(50);

Такая конструкция обеспечивает ограничение зоны доступа уже на уровне индекса, минимизируя объем обрабатываемых данных.

Паттерны безопасного проектирования зон доступа

На практике выделяются устойчивые подходы к проектированию:

  • обязательное включение userId во все таблицы с пользовательскими данными;
  • использование составных индексов для всех частых сценариев фильтрации;
  • инкапсуляция доступа через репозитории;
  • применение hooks для принудительного контроля целостности;
  • очистка состояния при смене пользователя;
  • синхронизация клиентской и серверной модели прав.

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