Делегирование доступа через вложенные данные

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

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

Ключевые свойства:

  • Локализация прав — доступ описывается максимально близко к данным
  • Контекстность — права зависят от текущего состояния вложенных объектов
  • Изолированность — вложенные структуры не влияют на глобальное состояние

Структура вложенных разрешений

В Iron вложенные данные часто представляются в виде JSON-подобных объектов:

const resource = {
  id: "doc-1",
  content: "Текст документа",
  access: {
    owner: "user-1",
    permissions: {
      read: ["user-2", "user-3"],
      write: ["user-2"]
    },
    delegated: {
      "user-2": {
        canDelegate: true,
        scope: {
          read: true,
          write: false
        }
      }
    }
  }
};

Здесь:

  • permissions определяет базовый уровень доступа
  • delegated описывает делегированные права с дополнительными ограничениями
  • scope ограничивает действия делегированного пользователя

Механизм наследования прав

Вложенные данные формируют цепочку наследования. При проверке доступа Iron проходит по структуре сверху вниз:

  1. Проверяется глобальный доступ (если есть)
  2. Проверяется уровень ресурса
  3. Проверяются вложенные делегированные правила
  4. Применяются ограничения scope

Пример:

function canWrite(userId, resource) {
  const base = resource.access.permissions.write.includes(userId);

  const delegated = resource.access.delegated[userId];
  if (delegated) {
    return delegated.scope.write === true;
  }

  return base;
}

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


Ограничение области действия (Scope)

Scope — ключевой элемент делегирования. Он определяет границы, в которых действуют переданные права.

Типичный scope включает:

  • операции (read, write, delete)
  • временные ограничения
  • фильтры данных

Пример:

delegated: {
  "user-2": {
    scope: {
      read: true,
      write: false,
      expiresAt: 1710000000
    }
  }
}

Проверка времени:

function isValidScope(scope) {
  if (scope.expiresAt && Date.now() > scope.expiresAt) {
    return false;
  }
  return true;
}

Делегирование с передачей прав дальше

Iron допускает каскадное делегирование, если это явно разрешено:

delegated: {
  "user-2": {
    canDelegate: true,
    scope: { read: true },
    delegated: {
      "user-4": {
        scope: { read: true }
      }
    }
  }
}

В этом случае:

  • user-2 получил право читать
  • он может передать это право user-4
  • user-4 не обязательно может делегировать дальше

Рекурсивная проверка:

function hasReadAccess(userId, node) {
  if (node.permissions?.read?.includes(userId)) {
    return true;
  }

  if (node.delegated) {
    for (const delegator in node.delegated) {
      const delegate = node.delegated[delegator];

      if (delegator === userId && delegate.scope.read) {
        return true;
      }

      if (delegate.delegated && hasReadAccess(userId, delegate)) {
        return true;
      }
    }
  }

  return false;
}

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

Каждый уровень вложенности работает как отдельный контекст. Это предотвращает утечку прав:

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

Пример изоляции:

const parent = {
  scope: { read: true, write: true }
};

const child = {
  scope: { read: true, write: false }
};

Даже если родитель имеет write, дочерний уровень его теряет.


Динамическое формирование прав

Iron поддерживает вычисляемые права на основе вложенных данных:

const resource = {
  access: {
    dynamic: (user, context) => {
      if (context.time < 18) {
        return { read: true };
      }
      return { read: false };
    }
  }
};

Использование:

function getAccess(user, resource, context) {
  if (typeof resource.access.dynamic === "function") {
    return resource.access.dynamic(user, context);
  }
  return resource.access;
}

Комбинирование статических и делегированных правил

Часто используется гибридный подход:

access: {
  permissions: { read: ["user-1"] },
  delegated: {
    "user-2": { scope: { read: true } }
  },
  dynamic: (user) => ({ read: user.role === "admin" })
}

Проверка должна учитывать все источники:

function canRead(user, resource) {
  const staticAccess = resource.access.permissions.read.includes(user.id);

  const delegated = resource.access.delegated[user.id]?.scope.read;

  const dynamic = resource.access.dynamic?.(user)?.read;

  return staticAccess || delegated || dynamic;
}

Управление сложностью вложенных структур

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

Нормализация структуры

  • ограничение глубины вложенности
  • выделение делегирования в отдельные узлы

Кэширование результатов

const cache = new Map();

function cachedAccessCheck(userId, resourceId) {
  const key = `${userId}:${resourceId}`;

  if (cache.has(key)) {
    return cache.get(key);
  }

  const result = computeAccess(userId, resourceId);
  cache.set(key, result);

  return result;
}

Итеративный обход вместо рекурсии

  • снижает нагрузку на стек
  • повышает предсказуемость

Безопасность делегирования

Основные риски:

  • эскалация привилегий через вложенные структуры
  • бесконтрольное каскадное делегирование
  • устаревшие scope (например, истёкшие права)

Методы защиты:

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

Пример ограничения глубины:

function hasAccess(userId, node, depth = 0) {
  if (depth > 3) return false;

  if (node.delegated) {
    for (const key in node.delegated) {
      if (hasAccess(userId, node.delegated[key], depth + 1)) {
        return true;
      }
    }
  }

  return false;
}

Практические сценарии применения

1. Документооборот

  • владелец делегирует просмотр редактору
  • редактор делегирует просмотр наблюдателям

2. API-токены

  • основной токен создаёт вложенные с ограниченным scope
  • вложенные токены не могут расширять права

3. Микросервисы

  • сервис делегирует доступ другому сервису через вложенные контексты
  • каждый уровень добавляет ограничения

Оптимизация доступа через вложенные данные

Для повышения эффективности:

  • минимизация количества проверок
  • ранний выход при успешной авторизации
  • использование структур с быстрым доступом (Map, Set)

Пример:

const readSet = new Set(resource.access.permissions.read);

if (readSet.has(userId)) {
  return true;
}

Архитектурные паттерны

Capability-based access

  • права представлены как объекты
  • передаются через вложенные структуры

Policy objects

  • каждая вложенная структура — отдельная политика

Immutable access trees

  • вложенные данные не изменяются, а пересоздаются
  • повышает безопасность и предсказуемость

Работа с изменениями вложенных прав

Обновление делегированных прав требует аккуратности:

function updateDelegate(resource, userId, newScope) {
  return {
    ...resource,
    access: {
      ...resource.access,
      delegated: {
        ...resource.access.delegated,
        [userId]: {
          ...resource.access.delegated[userId],
          scope: newScope
        }
      }
    }
  };
}

Использование иммутабельности предотвращает побочные эффекты.


Валидация вложенных структур

Перед использованием данные проверяются:

function validateAccess(access) {
  if (!access.permissions) return false;
  if (typeof access.delegated !== "object") return false;

  for (const key in access.delegated) {
    const d = access.delegated[key];
    if (!d.scope) return false;
  }

  return true;
}

Роль вложенных данных в масштабировании

При росте системы:

  • вложенные структуры позволяют распределять ответственность
  • уменьшается необходимость централизованного контроля
  • повышается гибкость настройки доступа

Это делает модель делегирования через вложенные данные одним из ключевых инструментов при построении сложных систем управления доступом в Iron.