Делегирование доступа в библиотеке 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 проходит по структуре сверху вниз:
Пример:
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 включает:
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-4user-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;
}
Итеративный обход вместо рекурсии
Основные риски:
Методы защиты:
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-токены
3. Микросервисы
Для повышения эффективности:
Пример:
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.