В этой статье я расскажу, как использовал библиотеку react-abac, созданную Риком Хоффбауэром, для управления ролями пользователей и правилами доступности в проекте с многочисленными зависимостями между пользователями.

Управление контролем доступа в больших и сложных проектах React может быть сложной задачей, особенно при работе с множеством пользовательских ролей, правил доступа и зависимостей между пользователями. В этой статье мы рассмотрим, как я использовал библиотеку react-abac для упрощения управления доступом в таком проекте. Я приведу реальные примеры структуры проекта, а также подробно объясню, как библиотека react-abac была интегрирована в проект для повышения чистоты кода и удобства сопровождения.

Структура ABAC

  1. Хук, в котором происходит волшебство — withAbac.tsx
import React from 'react';

import { AbacProvider, AllowedTo } from 'react-abac';
import { useSelector } from 'react-redux';

import { ApplicationState } from '@redux/interfaces';

import { permissions, rules } from './abac.config';
import { users } from './users';

// Define the useAbacHook, which takes a Component, permission, and optional data as parameters
const useAbacHook =
    (Component, permission, data = {}) =>
    (props) => {
        // Define a function to get the user based on the userRole from state
        const getUser = (user: string) =>
            user
                ? users.find(({ name }) => name === user)
                : 'NoRole'
        // Get the userRole from the Redux store using useSelector
        const { userRole} = useSelector((state: ApplicationState) => state.userInfo);
        // Return the AbacProvider with the user, roles, permissions, and rules
        // Use the AllowedTo component to control access to the Component based on the permission       
        return (
            <AbacProvider
                user={getUser(userRole)}
                roles={getUser(userRole)?.roles}
                permissions={getUser(userRole)?.permissions}
                rules={rules as any}
            >
                <AllowedTo
                    perform={permissions[permission]}
                    data={props?.data || data}
                    no={() => <React.Fragment></React.Fragment>}
                >
                    <Component {...props} />
                </AllowedTo>
            </AbacProvider>
        );
    };
export default useAbacHook;

Этот код определяет функцию useAbacHook, которая является компонентом более высокого порядка, который оборачивает данный компонент React в компоненты AbacProvider и AllowedTo из библиотеки react-abac. Ловушка принимает три параметра: компонент, который необходимо обернуть, требуемое разрешение на доступ к компоненту и необязательный объект данных для предоставления дополнительной информации для оценки управления доступом.

Функция getUser определена в хуке для извлечения объекта пользователя на основе userRole, полученного из хранилища Redux. Хук возвращает новый компонент, который обертывает исходный компонент компонентами AbacProvider и AllowedTo. AbacProvider настраивается с пользователем, его ролями, разрешениями и правилами управления доступом. Компонент AllowedTo определяет, следует ли отображать исходный компонент, на основе предоставленного разрешения и данных. Если пользователю не разрешен доступ к компоненту, вместо этого отображается пустой React.Fragment.

2. Список пользователей — users.tsx

const users = [
    {
        id: 1,
        name: 'PowerUser',
        roles: ['PowerUser'],
        permissions: []
    },
    {
        id: 2,
        name: 'User',
        roles: ['User'],
        permissions: []
    },
    {
        id: 3,
        name: 'SubUser',
        roles: ['SubUser'],
        permissions: []
    },
   
    {
        id: 4,
        name: 'AuthorizedPerson',
        roles: ['AuthorizedPerson'],
        permissions: []
    },
    {
        id: 5,
        name: 'NoRole',
        roles: ['NoRole'],
        permissions: []
    },
   
];
export { users };

3. Конфигурационный файл, в котором можно включить/отключить доступ к различным компонентам — abac.config.tsx

import { SHOW_SETTINGS_USER_REQUESTS } from './constants'
import { showSettingsUserRequests } from '/rulesList'
import { PowerUser, User, SubUser, AuthorizedPerson, NoRole} from './userRolesConstants'

// Make roles and permissions readonly
const roles = { PowerUser, User, SubUser, AuthorizedPerson, NoRole } as const
const permissions = { SHOW_SETTINGS_USER_REQUESTS } as const

const rules = {
[roles.User]:
        // this access will be handled by showSettingsUserRequests
        // the result of the function should be a boolean
        // data is the minimum necessary information for establishing the access for User role
        [permissions.SHOW_SETTINGS_USER_REQUESTS]: (data) => showSettingsUserRequests(data),
        // more rules for User role            
 },
[roles.SubUser]: {
        // SubUser won't have access
        [permissions.SHOW_SETTINGS_USER_REQUESTS]:false
        // more rules for SubUser role
              },
[roles.PowerUser]: {
         // PowerUser will have access
        [permissions.SHOW_SETTINGS_USER_REQUESTS]: true
        // more rules for PowerUser role
              }
[roles.NoRole]: {
        // NoRole won't have access
        [permissions.SHOW_SETTINGS_USER_REQUESTS]: false
        // more rules for NoRole
              }
}

export { roles, permissions, rules };

4. Список правил отображения: rulesList.tsx

const showSettingsUserRequests= ({ isAuthenticated, isPrepaid, hasBillPaid, ...anyOtherData}) =>
    isAuthenticated && authType === "AUTH_TYPE" && !isPrepaid && hasBillPaid;

export { showSettingsUserRequests }

5. Файл констант — constants.tsx

export const SHOW_SETTINGS_USER_REQUESTS = 'SHOW_SETTINGS_USER_REQUESTS'

Знаю, знаю… Давайте перейдем к самому интересному! :)

Интеграция компонентов React

После определения структуры проекта библиотека react-abac была интегрирована в компоненты React. Вот пример реализации контроля доступа с помощью useAbacHook:

// Destructure the SHOW_SETTINGS_USER_REQUESTS constant from the permissions object
const { SHOW_SETTINGS_USER_REQUESTS } = permissions;

// Create the RenderChangeSIM component using the useAbacHook higher-order component
const RenderChangeSIM = useAbacHook(
    () => {
        // Call the custom useDisplayElement hook to manage the display of 'userRequestsHiddenContainer'
        useDisplayElement('userRequestsHiddenContainer');

        // Return the component wrapped in the NewRequestsWrapper component
        return (
            <NewRequestsWrapper>
                <NewRequests
                    buttonText={seeMoreButtonText}
                    text={`${totalPendingRequests} ${userRequestsDescription}`}
                    title={userRequestsHeadline}
                    handleChange={handleChange}
                />
            </NewRequestsWrapper>
        );
    },
    // Pass the required permission (SHOW_SETTINGS_USER_REQUESTS) to the useAbacHook
    SHOW_SETTINGS_USER_REQUESTS,
    { hasBillPaid, isPrepaid, subscriptionType, authType, ...anyOtherDataToPass}
);

// Render the RenderChangeSIM component that is controlled by the useAbacHook
return <RenderChangeSIM />;

Этот код создает компонент RenderChangeSIM, используя компонент более высокого порядка useAbacHook. Разрешение SHOW_SETTINGS_USER_REQUESTS деструктурируется из объекта permissions и передается в качестве аргумента в useAbacHook.

Внутри компонента вызывается пользовательский хук useDisplayElement для управления отображением файла userRequestsHiddenContainer. Затем компонент возвращает компонент NewRequests, завернутый в файл NewRequestsWrapper.

Компонент RenderChangeSIM создается путем передачи функции, которая отображает NewRequestsWrapper, необходимое разрешение SHOW_SETTINGS_USER_REQUESTS и любые дополнительные данные, необходимые для оценки управления доступом.

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

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

useEffect(() => {
    if (isAuthenticated && accessToken) {
        // ... (code removed for brevity)
    } else {
        navigate('/');
    }
}, [sessionRole, isAuthenticated])

useEffect(() => {
    if (sessionType === PREPAID) {
        return navigate('/recharge');
    }
    if (
        (userRole=== POWER_USER ||
        userRole=== USER ||
        userRole=== SUB_USER) &&
        !hybrid
    ) {
        return navigate('/pay-bill-unauthenticated');
    }
}, [userRole, sessionType])

return (<>
          {
           hasBillPaid || !isPrepaid ? 
             <NewRequestsWrapper>
                <NewRequests
                    buttonText={seeMoreButtonText}
                    text={`${totalPendingRequests} ${userRequestsDescription}`}
                    title={userRequestsHeadline}
                    handleChange={handleChange}
                />
            </NewRequestsWrapper> 
            : 
            <p> You do not have access here! </p>
</>
)

Что ты скажешь сейчас?
Я бы сказал, что это большое улучшение. Код стал намного чище, проще для понимания и обслуживания.

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

  1. Улучшенная читабельность. Чистый код и хорошо структурированная архитектура облегчают разработчикам чтение и понимание кодовой базы. Это сокращает время, необходимое для адаптации новых членов команды, и позволяет разработчикам работать более эффективно.
  2. Более простая отладка и устранение неполадок: когда код чистый и хорошо организованный, становится проще выявлять и исправлять ошибки, что приводит к более быстрому разрешению проблем и более стабильной работе приложения.
  3. Улучшение совместной работы. Чистый код и понятная архитектура облегчают совместную работу членов команды, гарантируя, что каждый может быстро понять кодовую базу и эффективно внести свой вклад.
  4. Улучшенная масштабируемость. Хорошо продуманная архитектура проекта упрощает масштабирование, поскольку обеспечивает прочную основу для добавления новых функций и возможностей без негативного влияния на существующую кодовую базу.
  5. Сокращение технического долга. Чистый код и поддерживаемые функции способствуют сокращению технического долга, поскольку разработчики могут избежать взломов или обходных путей, которые могут вызвать проблемы в долгосрочной перспективе.
  6. Повышенная ремонтопригодность. Когда код чистый, а архитектура проста, обслуживание приложения становится менее сложной задачей. Это упрощает применение обновлений, оптимизацию производительности и адаптацию приложения к изменяющимся требованиям.
  7. Более высокое качество кода. Акцент на чистом коде и хорошо спланированной архитектуре в конечном итоге приводит к более высокому общему качеству кода, что приводит к более надежному и надежному приложению.

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

Заключение

Библиотека react-abac, созданная Риком Хоффбауэром, оказалась мощным инструментом для управления контролем доступа в большом и сложном проекте React. Библиотека позволила создать более упорядоченную и удобную в сопровождении структуру кода, упростив управление ролями пользователей, правилами доступа и зависимостями между пользователями. Делясь своим опытом работы с библиотекой react-abac, я надеюсь вдохновить других на использование этой библиотеки в своих проектах, что в конечном итоге приведет к созданию более эффективного и удобного кода.