Привет, Хабр! Меня зовут Макс Баюров, я PHP-разработчик в компании AGIMA. Мне хотелось бы поделиться с вами опытом расширения уровней доступа к функционалу сайта. Если вам еще не приходилось с этим сталкиваться или этот процесс вызывает трудности, сейчас всё расскажу. В этой статье я отвечу на следующие вопросы: Для чего и когда нужно добавлять свои уровни доступа к функциональности сайта?  Нужно ли вообще что-то дорабатывать и как это понять? Что нужно для того, чтобы процесс разработки подобной доработки был более быстрым и как к ней подготовиться? Также рассмотрим предложенную мной реализацию, подводные камни и небольшой пример. В статье буду оперировать термином ТЭО. На всякий случай напомню, что это технико-экономическое обоснование — документ с информацией, из которой выводится целесообразность создания продукта или услуги, в нашем случае доработки ПО. Ответ зависит от нужд бизнеса. Но всегда может возникнуть соблазн применить быстрое, но негибкое решение, отрезав часть функционала какой-нибудь группе пользователей. На длинной дистанции такое решение будет проигрывать в высоконагруженном проекте. Помимо этого с ростом групп пользователей кодовая база будет расти, а ее поддержка затруднится. Также могут возникнуть проблемы с доступами при сочетании групп. В связи с этим советую использовать поставляемые возможности вашего фреймворка для разграничения доступа. В таком случае ответственность за реализацию ложится на вендора, и риск того, что что-то будет работать не так после обновления, будет значительно ниже. Но что делать, если поставляемые разграничения слишком обширны? Возможно, у бизнеса есть необходимость более тонко настраивать доступы к функциональности? В таком случае подобная доработка будет иметь смысл. Также может возникнуть ситуация, когда в проекте уже используется хардкод, который мешает гибко управлять доступами. Это значит, что при изменении доступа какой-либо группы пользователей нужна будет доработка. Конечно, хардкод имеет место, если проект небольшой, управление доступами не подразумевается, а сам сайт — маленький промолендинг. Также подобное решение применимо, когда мы точно уверены в том, что в следующем обновлении фреймворка появится возможность разграничить тот или иной функционал его средствами. Но не стоит сильно на это надеяться, так как один подобный компромисс может привести к другому и в конечном итоге сильно ухудшить качество кодовой базы. Итак, предположим, что у нас есть ТЭО, оно весомое и мы беремся за доработку пользовательских уровней доступа к функционалу. Нельзя не рассмотреть этот вопрос, так как если есть возможность грамотно разграничить доступ, используя встроенный функционал системы, лучше воспользоваться им и избавить себя от лишней головной боли. Если в вашем проекте уже используется хардкод, который мешает гибко управлять доступами, начните с анализа кодовой базы. Это поможет понять, как именно нужно разделить доступы к функционалу, а также выявить сами функции. Для выявления функций, доступы к которым в дальнейшем будем ограничивать, можно использовать следующий шаблон таблицы: В этой таблице и в следующих многоточие («...») означает, что в таблице может быть больше строк и столбцов, в зависимости от исследуемой вами функциональности. С помощью такой таблицы можно выявить пересекающиеся функции и исключить их. А еще на этапе формализации задачи понять, как ограничивать доступ к ним. Например, если нужно отдельно управлять доступом к изменению активности, то в таблице наряду с функцией «Редактирование» должна быть функция «Изменение активности». И соответственно, при проверке доступа к редактированию сущности мы не учитываем поля, связанные с активностью. Таблица может быть большой, если делать ее для всей функциональности сайта. Поэтому можно провести такой анализ в рамках отдельных сущностей системы. Также необходимо выявить уровни доступа к выделенным функциям. Рекомендую использовать следующие уровни доступа: «Нет доступа»; «Чтение»; «Полный доступ». Но нужно условиться, что «Полный доступ» позволяет группе выполнять такие функции, как «Удаление» и «Создание». То есть мы не вводим уровень доступа «Исполнение», как в UNIX-подобных системах.  Для дальнейшего использования этих уровней доступа можно использовать обозначение, предложенное в таблице, или же задать его по-другому. Нет доступа Чтение Полный доступ - R RW Обозначение уровней доступа После выявления функций и уровней доступа можно приступить к составлению основной матрицы доступов, представленной в таблице ниже. Сущность Функция Группа 1 … Группа n Сущность 1 (например, заказ) Функция 1 (например, редактирование) - … RW Функция 2 - … R Функция 3 - … - ... … … … … Сущность n Функция n-1 - … R Функция n - … RW Финальная матрица доступов Пояснения к таблице: функции в сущностях могут называться одинаково, например редактирование. Но это не значит, что доступ на редактирование к одной сущности даст доступ на редактирование к другой. Такая матрица будет полезна в случае рефакторинга, чтобы при ликвидации хардкода из кодовой базы уже существующие группы пользователей имели аналогичные доступы в системе. Если есть необходимость изменить какие-то доступы, можно создать две таблицы: «Как есть» («AS_IS») и «Как будет» («TO_BE»). Вторая — это, по сути, копия таблицы «Как есть», но с необходимыми изменениями. Для подобных изменений можно ввести цветовые обозначения для наглядности. Также матрица будет полезна, если необходимо настроить существующие группы пользователей. Имея подобную матрицу под рукой, будет проще реализовать необходимые миграции. Конечно, с точки зрения разработки наибольший интерес представляют сами функции. Ведь в конечном итоге необходимо создать надстройку, позволяющую гибко управлять уровнями доступа. В первую очередь реализуйте хранение доступов к функциям. В базе необходимо хранить поля, представленные в таблице: Функционал Тип: строка Кодовый идентификатор функционала Рекомендуется использовать префикс сущности в коде идентификатора. Например, код для функционала редактирования заказа — order_edit Группа пользователей Тип: целое числоИдентификатор группы пользователей (FK) Уровень доступа Тип: целое число 0 — Нет доступа 1 — Чтение 3 — Полный доступ Список полей сущности доступов к функциям, общие сведения Также необходимо продумать возможность управлять записями в данной сущности. В дальнейшем мы рассмотрим пример для Bitrix. При этом стоит учитывать, что все эти поля имеют ограниченные варианты значений, поддерживаемые системой. Затем реализуйте сервис для определения, какой уровень доступа имеет пользователь к конкретному функционалу.  Примерный интерфейс сервиса представлен на картинке слева. Возможно, для ограничения доступа вам могут потребоваться и другие публичные методы, например проверка наличия доступа хотя бы к одной из переданных функций. Такой метод может понадобиться для работы с меню. В любом случае облик сервиса стоит продумать заранее на этапе формирования ТЭО.  Давайте разберём этот одинокий метод. По переданному коду функции метод проверяет уровень доступа пользователя к заданному функционалу сайта, возвращая число, при этом учитывая все назначенные пользователю группы. Для вычисления уровня доступа к функции можно использовать побитовое «Или». Такой подход позволяет грамотно сочетать уровни доступа. После того как сервис реализован, его можно использовать в коде проекта для разграничения доступов (в обработчиках и на страницах), опираясь на выделенные ранее функции. В конце концов можно реализовывать миграции на добавление соответствующих доступов в соответствующую таблицу. Подводные камни можно разделить на организационные и технические. К организационным относятся: Много доступов к малой функциональности.Функционал может быть очень сильно раздроблен, из-за чего обработка доступов может стать очень сложной, как и их настройка. Данная проблема частично решается на этапе выявления функций и их пересечений. Какую-то функциональность можно объединить. Если это возможно, то лучше это сделать, Это упростит как настройку доступов, так и разработку. Функциональность, зависящая друг от друга.Сущности в системе могут зависеть друг от друга, и это нормально. Например, какие-то группы пользователей имеют ограниченный уровень доступа к просмотру списка пользователей системы, но имеют доступ к редактированию сущности, которая ссылается на какого-либо пользователя.В таком случае пользователи данной группы не смогут корректно работать с данной сущностью. Такую кросс-функциональность стоит учитывать и выявлять при определении функциональности. Вариантом решения подобной проблемы может стать выбор пользователя без просмотра полного списка пользователей, например по почте. «Тупняк» и коммуникация. Непрерывная интеграция, новые функции, старые добрые роли. Процесс разработки может затянуться не на один релиз, в результате чего появятся новые фичи, не отраженные в матрице доступов. Частичным решением данной проблемы может послужить быстрая реализация сервиса проверки доступа и его интеграция в новых фичах. К техническим трудностям относятся: Разнообразие проверок доступа.Данная проблема более актуальна в случае, когда в проекте уже существует хардкод, проект живет долго, и некоторые подходы менялись. Это может затруднить анализ кодовой базы на этапе выявления функциональности. Что говорит о необходимости составления матрицы доступов и ее согласования с бизнесом. Излишние проверки доступа к функциям и оптимизация сервиса.Проверка доступов может повлиять на производительность. В случае скрытия пунктов из меню административной части сайта, проверка доступов будет необходима на каждом хите, что говорит о необходимой оптимизации соответствующего сервиса. Кэшируйте запросы к базе на получение уровней доступа пользователя, также можно сделать упрощенную проверку уровня доступа для админа, которому доступен доступ ко всему функционалу. Ограничение доступа на стороне клиента.Проверка доступа на стороне клиента небезопасна, дизейблить какие-либо поля нужно лишь для удобства использования, а основные проверки делать на стороне сервера. Так как у Битрикса обширные возможности по разграничению прав, стоит их учитывать при реализации доработки. Если мы, например, хотим ограничить доступ к сущности «Купоны правил корзины», то стоит понимать, что без доступа к модулю «Интернет-магазин» пользователь не сможет редактировать и просматривать купоны. Таким образом, эта доработка не что иное, как надстройка над битриксовыми правами, которая дает гибкости в настройке, но делает ее немного сложнее. Для реализации хранения доступов к функциям был использован Highload-блок. Он отлично подходит для решения данной задачи, ведь помимо хранения предоставляет возможность управлять записями, используя фильтрацию и прочие удобства. Кроме того, позволяет реализовать пользовательские типы полей, необходимые для ввода корректных значений, поддерживаемых сервисом. Для реализации пользовательских типов полей можно расширить базовый тип пользовательского свойства (\Bitrix\Main\UserField\Types\BaseType) и реализовать компонент рендеринга поля в системе. Пользовательский тип для «Групп пользователей» лучше реализовать, расширив тип «Список» (\Bitrix\Main\UserField\Types\EnumType), так как группы пользователей могут меняться, в отличие от функций и уровней доступа. Реализовав HL-блок и пользовательские типы полей, получаем следующее: Список с фильтрацией. Возможность настраивать доступ (добавлять/удалять/редактировать записи). Все необходимые ограничения для ввода значений в поля (пример с полем функционала). Упрощенную реализацию метода интерфейса для сервиса можно представить в виде следующей диаграммы деятельности. Как вы можете видеть, сервис возвращает уровни доступа, так что детали об этих значениях можно инкапсулировать в него с помощью публичных констант. Также хочется отметить, что не всегда нужно, чтобы админ имел полный доступ к функционалу. В таком случае следует параметризировать проверку принадлежности пользователя к группе админов, а также хранить в сервисе информацию о подобной группе (например ID). Кроме того, если есть необходимость проверять уровень доступа какого-либо пользователя (не текущего авторизованного), то это также следует учесть при выборке групп пользователя. Не буду говорить о том, что все выборки необходимо кэшировать. Определять максимальный уровень доступа можно с помощью побитового «Или», подробнее почитать о котором можно в официальной документации PHP. Это позволит корректно сочетать уровни доступа при наличии нескольких групп у пользователя. Давайте определимся с событиями, в обработчиках которых нужно проверять уровни доступа. В первую очередь это события перед изменением сущностей, такие как создание, удаление и редактирование. Это позволяет проверить уровень доступа пользователя и не изменять записи в базе данных в случае его отсутствия, а также уведомить пользователя об этом, выведя ошибку. А также следующие события административной части сайта: Событие Что ограничиваем OnBuildGlobalMenu Вывод пунктов меню в админке. OnEpilog Дизейблим кнопки и поля при необходимости. Добавляем JS. OnProlog Доступ по прямой ссылке при отсутствии прав на чтение.  OnAdminContextMenuShow Кнопки, выводимые в админке, такие как «Добавить», «Удалить», «Скопировать» и т. д. OnAdminListDisplay Действия над элементами списка, а также пункты контекстного меню записей.  OnAdminSubListDisplay Аналогично OnAdminListDisplay только для подсписка, например, при работе с доступами с сущностью «Купоны правил корзины». Подробнее об этих событиях в официальной документации Битрикс. Возможно, для настройки доступов в вашем проекте пригодятся еще какие-либо события. В заключение хочется отметить, что внедрение данной доработки требует тщательной аналитики, что, на мой взгляд, наиболее трудозатратно. Ведь это в свою очередь поможет корректно формализовать задачу и упростить не только процесс разработки, но и приемки. Хочу поблагодарить свою команду, которая работала со мной над этой задачей, и человека, который вдохновил на написание статьи, сам бы не решился. Пока, Хабр, надеюсь, статья была полезной! P. S. А еще подписывайтесь на наш телеграм-канал про разработку. Там мы публикуем ссылки на статьи, исследования, сами делимся полезным контентом. Эту статью тоже можем обсудить там.  Список литературы: Справочник PHP. Документация для разработчиков Bitrix.