Итак, вы работали над этим новым интересным веб-приложением. Будь то приложение с рецептами, менеджер документов или даже ваше частное облако, теперь вы достигли точки работы с пользователями и разрешениями. Возьмем, к примеру, диспетчер документов: вам нужны не только администраторы; возможно, вы хотите пригласить гостей с доступом только для чтения или людей, которые могут редактировать, но не удалять ваши файлы. Как вы справляетесь с этой логикой во внешнем интерфейсе, не загромождая код слишком большим количеством сложных условий и проверок?
В этой статье мы рассмотрим пример реализации того, как можно элегантно и четко справиться с подобными ситуациями. Отнеситесь к этому с недоверием — ваши потребности могут отличаться, но я надеюсь, что вы сможете почерпнуть из него некоторые идеи.
Предположим, что вы уже создали серверную часть, добавили таблицу для всех пользователей в своей базе данных и, возможно, предоставили специальный столбец или свойство для ролей. Детали реализации полностью зависят от вас (в зависимости от вашего стека и предпочтений). Для этой демонстрации давайте использовать следующие роли:
- Администратор : может делать что угодно, например создавать, удалять и редактировать собственные или иностранные документы.
- Редактор : может создавать, просматривать и редактировать файлы, но не удалять их.
- Гость : может просматривать файлы, просто так.
Как и большинство современных веб-приложений, ваше приложение может использовать RESTful API для связи с серверной частью, поэтому давайте воспользуемся этим сценарием для демонстрации. Даже если вы выберете что-то другое, например GraphQL или рендеринг на стороне сервера, вы все равно можете применить тот же шаблон, который мы собираемся рассмотреть.
Ключ должен вернуть роль (или разрешение, если вы предпочитаете это имя) текущего пользователя, вошедшего в систему, при извлечении некоторых данных.
{
id: 1,
title: "Мой первый документ",
authorId: 742,
accessLevel: "АДМИНИСТРАТОР",
содержание: {...}
}
Здесь мы получаем документ с некоторыми свойствами, включая свойство с именем accessLevel
для роли пользователя. Таким образом мы узнаем, что разрешено или запрещено делать зарегистрированному пользователю. Наша следующая задача — добавить логику во внешний интерфейс, чтобы гости не видели того, чего не должны видеть, и наоборот.
В идеале, вы не должны полагаться только на интерфейс для проверки разрешений. Кто-то, имеющий опыт работы с веб-технологиями, все еще может отправить запрос без пользовательского интерфейса на сервер с намерением манипулировать данными, следовательно, ваш бэкэнд тоже должен это проверять.
Между прочим, этот шаблон не зависит от структуры; неважно, работаете ли вы с React, Vue или даже с каким-то диким ванильным JavaScript.
Содержание статьи
Определение констант
Самым первым (необязательным, но настоятельно рекомендуемым) шагом является создание некоторых констант. Это будут простые объекты, содержащие все действия, роли и другие важные части, из которых может состоять приложение. Мне нравится помещать их в специальный файл, возможно, называть его constants.js
:
const actions = {
MODIFY_FILE: "MODIFY_FILE",
VIEW_FILE: "VIEW_FILE",
DELETE_FILE: «DELETE_FILE»,
CREATE_FILE: «CREATE_FILE»
};
const роли = {
АДМИНИСТРАТОР: «АДМИНИСТРАТОР»,
РЕДАКТОР: «РЕДАКТОР»,
ГОСТЬ: «ГОСТЬ»
};
экспорт {действия, роли};
Если у вас есть преимущество использования TypeScript, вы можете использовать перечисления, чтобы получить немного более чистый синтаксис.
Создание коллекции констант для ваших действий и ролей имеет ряд преимуществ:
- Единый источник истины. Вместо того, чтобы просматривать всю кодовую базу, вы просто открываете
constants.js
чтобы увидеть, что возможно внутри вашего приложения. Этот подход также очень расширяемый, например, когда вы добавляете или удаляете действия. - Никаких опечаток . Вместо того, чтобы каждый раз вводить роль или действие вручную, что делает его подверженным опечаткам и неприятным сеансам отладки, вы импортируете объект и, благодаря волшебству вашего любимого редактора, получаете бесплатные предложения и автозаполнение. Если вы по-прежнему неправильно набираете имя, ESLint или другой инструмент, скорее всего, будет кричать на вас, пока вы его не исправите.
- Документация. Вы работаете в команде? Новые члены команды оценят простоту того, что им не нужно просматривать тонны файлов, чтобы понять, какие разрешения или действия существуют. Это также может быть легко задокументировано с помощью JSDoc.
Использование этих констант довольно просто; импортируйте и используйте их так:
импорт {действий} из "./constants.js";
console.log (actions.CREATE_FILE);
Определение разрешений
Переходим к захватывающей части: моделированию структуры данных для сопоставления наших действий с ролями. Есть много способов решить эту проблему, но больше всего мне нравится следующий. Давайте создадим новый файл, назовем его permissions.js
и поместим внутрь код:
импортировать {действия, роли} из "./constants.js";
const mappings = новая карта ();
mappings.set (actions.MODIFY_FILE, [roles.ADMIN, roles.EDITOR]);
mappings.set (actions.VIEW_FILE, [roles.ADMIN, roles.EDITOR, roles.GUEST]);
mappings.set (actions.DELETE_FILE, [roles.ADMIN]);
mappings.set (actions.CREATE_FILE, [roles.ADMIN, roles.EDITOR]);
Давайте рассмотрим это, шаг за шагом:
- Сначала нам нужно импортировать наши константы.
- Затем мы создаем новую карту JavaScript, называемую
сопоставлениями
. Мы могли бы использовать любую другую структуру данных, например, объекты, массивы, что угодно. Мне нравится использовать Карты, поскольку они предлагают несколько удобных методов, например.has ()
.get ()
и т. Д. - Затем мы добавляем (или скорее установите) новую запись для каждого действия нашего приложения. Действие служит ключом, с помощью которого мы затем получаем роли, необходимые для выполнения указанного действия. Что касается ценности, мы определяем набор необходимых ролей.
Этот подход сначала может показаться странным (мне так показалось), но со временем я научился ценить его. Преимущества очевидны, особенно в больших приложениях с множеством действий и ролей:
- Опять только один источник истины. Вам нужно знать, какие роли требуются для редактирования файла? Нет проблем, перейдите в
permissions.js
и найдите запись. - Изменить бизнес-логику на удивление просто. Допустим, ваш менеджер по продукту решает, что с завтрашнего дня редакторам разрешено удалять файлы; просто добавьте их роль в запись
DELETE_FILE
и завершите работу. То же самое касается добавления новых ролей: добавьте больше записей в переменную mappings, и все готово. - Testable. Вы можете использовать тесты моментальных снимков, чтобы убедиться, что внутри этих сопоставлений ничего неожиданно не изменится. Это также становится понятнее при проверке кода.
Приведенный выше пример довольно прост и может быть расширен для охвата более сложных случаев. Например, если у вас разные типы файлов с разным доступом к ролям. Подробнее об этом в конце статьи.
Проверка разрешений в пользовательском интерфейсе
Мы определили все наши действия и роли и создали карту, которая объясняет, кому и что разрешено. Пришло время реализовать функцию, которую мы будем использовать в нашем пользовательском интерфейсе для проверки этих ролей.
Создавая такое новое поведение, мне всегда нравится начинать с того, как должен выглядеть API. После этого я реализую реальную логику этого API.
Допустим, у нас есть компонент React, отображающий раскрывающееся меню:
function Dropdown () {
возвращаться (
-
-
-
-
-
-
-
-
);
}
Очевидно, мы не хотим, чтобы гости видели и не нажимали кнопку «Удалить» или «Переименовать», но мы хотим, чтобы они видели «Обновить». С другой стороны, редакторы должны видеть все, кроме «Удалить». Я представляю себе API вроде этого:
hasPermission (файл, действия.DELETE_FILE);
Первый аргумент — это сам файл, полученный нашим REST API. Он должен содержать свойство accessLevel
из более ранней версии, которое может иметь значение ADMIN
EDITOR
или GUEST
. Поскольку один и тот же пользователь может иметь разные разрешения в разных файлах, нам всегда нужно указывать этот аргумент.
Что касается второго аргумента, мы передаем действие, например, удаление файла. Затем функция должна вернуть логическое значение истина
если текущий пользователь, вошедший в систему, имеет разрешения на это действие, или ложь
в противном случае.
импорт hasPermission из "./permissions.js";
импортировать {действия} из "./constants.js";
function Dropdown () {
возвращаться (
{hasPermission (файл, действия.VIEW_FILE) && (
-
)}
{hasPermission (файл, actions.MODIFY_FILE) && (
-
)}
{hasPermission (файл, actions.CREATE_FILE) && (
-
)}
{hasPermission (файл, действия.DELETE_FILE) && (
-
)}
);
}
Возможно, вы захотите найти менее подробное имя функции или, может быть, даже другой способ реализации всей логики (на ум приходит каррирование), но для меня это неплохо поработало, даже в приложениях со сверхсложными разрешениями. . Конечно, JSX выглядит более загроможденным, но это небольшая цена. Постоянное использование этого шаблона во всем приложении делает разрешения более понятными и интуитивно понятными.
Если вы все еще не уверены, давайте посмотрим, как это будет выглядеть без помощника hasPermission
:
возврат (
{['ADMIN', 'EDITOR', 'GUEST'] .includes (file.accessLevel) && (
-
)}
{['ADMIN', 'EDITOR'] .includes (file.accessLevel) && (
-
)}
{['ADMIN', 'EDITOR'] .includes (file.accessLevel) && (
-
)}
{file.accessLevel == "ADMIN" && (
-
)}
);
Вы можете сказать, что это не так уж плохо, но подумайте, что произойдет, если будет добавлена дополнительная логика, например проверка лицензий или более детальные разрешения. В нашей профессии дела быстро выходят из-под контроля.
Вам интересно, зачем нам нужна первая проверка прав доступа, когда все в любом случае могут увидеть кнопку «Обновить»? Мне нравится это там, потому что никогда не знаешь, что может измениться в будущем. Может появиться новая роль, которая может даже не видеть кнопку. В этом случае вам нужно только обновить свой permissions.js
и оставить компонент в покое, что приведет к более чистому фиксации Git и меньшим шансам на ошибку.
Реализация проверки разрешений
Наконец, пришло время реализовать функцию, которая объединяет все воедино: действия, роли и пользовательский интерфейс. Реализация довольно проста:
импортировать сопоставления из "./permissions.js";
function hasPermission (файл, действие) {
if (! file? .accessLevel) {
вернуть ложь;
}
if (mappings.has (действие)) {
вернуть mappings.get (действие) .includes (file.accessLevel);
}
вернуть ложь;
}
экспорт по умолчанию hasPermission;
экспорт {действия, роли};
Вы можете поместить приведенный выше код в отдельный файл или даже в permissions.js
. Я лично храню их в одном файле, но, эй, я не говорю вам, как жить своей жизнью. : -)
Давайте разберемся, что здесь происходит:
- Мы определяем новую функцию
hasPermission
используя ту же сигнатуру API, которую мы выбрали ранее. Он принимает файл (который поступает из серверной части) и действие, которое мы хотим выполнить. - В качестве отказоустойчивого, если по какой-то причине файл
null
или не соответствует не содержит свойстваaccessLevel
мы возвращаемfalse
. Лучше быть особенно осторожными, чтобы не раскрыть пользователю «секретную» информацию, вызванную сбоями или ошибкой в коде. - Переходя к основному, мы проверяем, содержит ли
сопоставления
действие, которое мы ищем. Если это так, мы можем безопасно получить его значение (помните, что это массив ролей) и проверить, имеет ли наш вошедший в систему пользователь роль, необходимую для этого действия. Это либо возвращаетtrue
либоfalse
. - Наконец, если
сопоставления
не содержат действие, которое мы ищем (это могло быть ошибкой в коде или снова сбой), мы возвращаемfalse
для большей безопасности. - В последних двух строках мы не только экспортируем функцию
hasPermission
но также повторно экспортируем наши константы для удобства разработчика. Таким образом, мы можем импортировать все утилиты в одной строке.
import hasPermission, {actions} from "./permissions.js";
Другие варианты использования
Показанный код довольно прост для демонстрационных целей. Тем не менее, вы можете взять его за основу своего приложения и соответствующим образом придать ему форму. Я думаю, что это хорошая отправная точка для любого приложения на основе JavaScript для реализации пользовательских ролей и разрешений.
После небольшого рефакторинга вы даже можете повторно использовать этот шаблон, чтобы проверить что-то другое, например лицензии:
импорт {действий, лицензий} из "./constants.js";
const mappings = новая карта ();
mappings.set (actions.MODIFY_FILE, [licenses.PAID]);
mappings.set (actions.VIEW_FILE, [licenses.FREE, licenses.PAID]);
mappings.set (actions.DELETE_FILE, [licenses.FREE, licenses.PAID]);
mappings.set (actions.CREATE_FILE, [licenses.PAID]);
function hasLicense (пользователь, действие) {
if (mappings.has (действие)) {
вернуть mappings.get (действие) .includes (user.license);
}
вернуть ложь;
}
Вместо роли пользователя мы утверждаем его свойство лицензии
: тот же ввод, тот же вывод, совершенно другой контекст.
В моей команде нам нужно было проверить как роли пользователей, так и лицензии, вместе или по отдельности. Выбирая этот шаблон, мы создали разные функции для разных проверок и объединили их в оболочку. В итоге мы использовали hasAccess
util:
функция hasAccess (файл, пользователь, действие) {
return hasPermission (файл, действие) && hasLicense (пользователь, действие);
}
Не идеально передавать три аргумента каждый раз, когда вы вызываете hasAccess
и вы можете найти способ обойти это в своем приложении (например, каррирование или глобальное состояние). В нашем приложении мы используем глобальные хранилища, которые содержат информацию о пользователе, поэтому мы можем просто удалить второй аргумент и вместо этого получить его из хранилища.
Вы также можете углубиться в структуру разрешений. У вас есть разные типы файлов (или сущностей, если быть более общим)? Вы хотите включить определенные типы файлов на основе лицензии пользователя? Давайте возьмем приведенный выше пример и сделаем его немного более мощным:
const mappings = new Map ();
mappings.set (
actions.EXPORT_FILE,
новая карта ([
[typesPDF[licenses.FREE, licenses.PAID]],
[typesDOCX[licenses.PAID]],
[typesXLSX[licenses.PAID]],
[typesPPTX[licenses.PAID]]
])
);
Это добавляет совершенно новый уровень нашей проверке прав доступа. Теперь у нас могут быть разные типы сущностей для одного действия. Предположим, вы хотите предоставить экспортера для своих файлов, но хотите, чтобы ваши пользователи платили за этот супер-модный конвертер Microsoft Office, который вы создали (и кто может вас винить?). Вместо того, чтобы напрямую предоставлять массив, мы вкладываем вторую карту в действие и передаем все типы файлов, которые хотим охватить. Вы спросите, зачем использовать карту? По той же причине, о которой я упоминал ранее: он предоставляет несколько дружественных методов, таких как .has ()
. Но вы можете использовать что-нибудь другое.
С недавним изменением наша функция hasLicense
больше не сокращает ее, поэтому пришло время ее немного обновить:
функция hasLicense (пользователь, файл, действие) {
if (! user ||! file) {
вернуть ложь;
}
if (mappings.has (действие)) {
const mapping = mappings.get (действие);
if (mapping.has (file.type)) {
вернуть mapping.get (file.type) .includes (user.license);
}
}
вернуть ложь;
}
Не знаю, только ли я, но разве это не выглядит супер-читаемым, даже несмотря на то, что сложность увеличилась?
Тестирование
Если вы хотите убедиться, что ваше приложение работает должным образом, даже после рефакторинга кода или введения новых функций, вам лучше подготовить некоторое тестовое покрытие. Что касается тестирования разрешений пользователей, вы можете использовать разные подходы:
- Создавайте тесты моментальных снимков для сопоставлений, действий, типов и т. Д. Этого можно легко достичь в Jest или других средствах выполнения тестов и гарантировать, что при проверке кода ничего неожиданно не проскочит. Однако обновление этих снимков может оказаться утомительным, если разрешения все время меняются.
- Добавьте модульные тесты для
hasLicense
илиhasPermission
и убедитесь, что функция работает как ожидается путем жесткого кодирования некоторых реальных тестовых случаев. Функции модульного тестирования в большинстве случаев, если не всегда, являются хорошей идеей, поскольку вы хотите убедиться, что возвращается правильное значение. - Помимо проверки работы внутренней логики, вы можете использовать дополнительные тесты моментальных снимков в сочетании с вашим константы для каждого сценария. Моя команда использует что-то похожее на это:
Object.values (actions) .forEach ((action) => {
описать (действие.toLowerCase (), функция () {
Object.values (лицензии) .forEach ((лицензия) => {
it (license.toLowerCase (), function () {
ожидать (hasLicense ({тип: 'PDF'}, {лицензия}, действие)). toMatchSnapshot ();
ожидать (hasLicense ({тип: 'DOCX'}, {лицензия}, действие)). toMatchSnapshot ();
ожидать (hasLicense ({тип: 'XLSX'}, {лицензия}, действие)). toMatchSnapshot ();
ожидать (hasLicense ({тип: 'PPTX'}, {лицензия}, действие)). toMatchSnapshot ();
});
});
});
});
Но опять же, есть много разных личных предпочтений и способов проверить это.
Заключение
Вот и все! Я надеюсь, что вы смогли почерпнуть некоторые идеи или вдохновение для своего следующего проекта, и что этот шаблон может быть тем, чего вы хотите достичь. Напомним некоторые из его преимуществ:
- Больше нет необходимости в сложных условиях или логике в пользовательском интерфейсе (компонентах). Вы можете положиться на значение
return
функцииhasPermission
и удобно отображать и скрывать элементы на основе этого. Возможность отделить бизнес-логику от пользовательского интерфейса помогает сделать кодовую базу более чистой и удобной в обслуживании. - Единый источник достоверной информации о ваших разрешениях. Вместо того, чтобы просматривать множество файлов, чтобы выяснить, что пользователь может или не может видеть, зайдите в сопоставления разрешений и посмотрите там. Это упрощает расширение и изменение разрешений пользователей, поскольку вам, возможно, даже не придется трогать какую-либо разметку.
- Очень хорошо тестируемый. Независимо от того, выбираете ли вы тесты моментальных снимков, тесты интеграции с другими компонентами или что-то еще, централизованные разрешения позволяют безболезненно писать тесты для.
- Документация. Вам не нужно писать приложение на TypeScript, чтобы воспользоваться автозаполнением или проверкой кода; использование предопределенных констант для действий, ролей, лицензий и т. д. может упростить вашу жизнь и уменьшить досадные опечатки. Кроме того, другие члены команды могут легко определить, какие действия, роли или что-то еще доступны и где они используются.
Предположим, вы хотите увидеть полную демонстрацию этого шаблона, перейдите в этот CodeSandbox. это обыгрывает идею использования React. Он включает в себя различные проверки разрешений и даже некоторое тестовое покрытие.
Как вы думаете? Есть ли у вас аналогичный подход к подобным вещам и считаете ли вы, что оно того стоит? Мне всегда интересно, что придумали другие люди, не стесняйтесь оставлять отзывы в разделе комментариев. Береги себя!