Статьи

Ролевое управление доступом в PHP

Существует несколько разных подходов к управлению правами пользователей, и у каждого есть свои плюсы и минусы. Например, использование маскировки битов чрезвычайно эффективно, но также ограничивает вас 32 или 64 разрешениями (количество бит в 32- или 64-битном целом числе). Другой подход заключается в использовании списка контроля доступа (ACL), однако вы можете назначать разрешения только объектам, а не конкретным или значимым операциям.

В этой статье я расскажу о своем любимом подходе: контроле доступа на основе ролей (RBAC). RBAC — это модель, в которой создаются роли для различных рабочих функций, а затем разрешения на выполнение определенных операций привязываются к ролям. Пользователю может быть назначена одна или несколько ролей, которые ограничивают доступ его системы к разрешениям, для которых они были авторизованы.

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

Я начну с обсуждения необходимых таблиц базы данных, затем создам два файла классов: ( Role.php ), который будет выполнять несколько задач, специфичных для ролей, и ( PrivilegedUser.php ), который расширит существующий класс пользователя. Наконец, я рассмотрю несколько примеров того, как вы можете интегрировать код в свое приложение. Управление ролями и управление пользователями идут рука об руку, и поэтому в этой статье я предполагаю, что у вас уже есть какая-то система аутентификации пользователей.

База данных

Вам нужны четыре таблицы для хранения информации о роли и разрешениях: таблица roles хранит идентификатор роли и имя роли, таблица permissions хранит идентификатор и описание role_perm таблица role_perm связывает, какие разрешения принадлежат каким ролям, а таблица user_role ассоциирует, какие роли которым назначены пользователи.

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

Это операторы CREATE TABLE для базы данных:

 CREATE TABLE roles ( role_id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, role_name VARCHAR(50) NOT NULL, PRIMARY KEY (role_id) ); CREATE TABLE permissions ( perm_id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, perm_desc VARCHAR(50) NOT NULL, PRIMARY KEY (perm_id) ); CREATE TABLE role_perm ( role_id INTEGER UNSIGNED NOT NULL, perm_id INTEGER UNSIGNED NOT NULL, FOREIGN KEY (role_id) REFERENCES roles(role_id), FOREIGN KEY (perm_id) REFERENCES permissions(perm_id) ); CREATE TABLE user_role ( user_id INTEGER UNSIGNED NOT NULL, role_id INTEGER UNSIGNED NOT NULL, FOREIGN KEY (user_id) REFERENCES users(user_id), FOREIGN KEY (role_id) REFERENCES roles(role_id) ); 

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

Вам не нужно вносить какие-либо изменения в таблицу users для хранения информации о роли, поскольку эта информация хранится отдельно в этих новых таблицах. В отличие от некоторых других систем RBAC, пользователь здесь не обязан иметь роль по умолчанию; вместо этого пользователь просто не будет иметь никаких привилегий, пока роль не будет специально назначена. В качестве альтернативы, в классе PrivilegedUser бы обнаружить пустую роль и при необходимости ответить непривилегированной ролью по умолчанию, либо вы можете написать короткий сценарий SQL, чтобы скопировать идентификаторы пользователей и инициализировать их, назначив непривилегированную роль по умолчанию.

Ролевый класс

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

Используйте следующий код для создания Role.php :

 <?php class Role { protected $permissions; protected function __construct() { $this->permissions = array(); } // return a role object with associated permissions public static function getRolePerms($role_id) { $role = new Role(); $sql = "SELECT t2.perm_desc FROM role_perm as t1 JOIN permissions as t2 ON t1.perm_id = t2.perm_id WHERE t1.role_id = :role_id"; $sth = $GLOBALS["DB"]->prepare($sql); $sth->execute(array(":role_id" => $role_id)); while($row = $sth->fetch(PDO::FETCH_ASSOC)) { $role->permissions[$row["perm_desc"]] = true; } return $role; } // check if a permission is set public function hasPerm($permission) { return isset($this->permissions[$permission]); } } 

Метод getRolePerms() создает новый объект Role на основе определенного идентификатора роли, а затем использует предложение JOIN для объединения role_perm и perm_desc . Для каждого разрешения, связанного с данной ролью, описание сохраняется как ключ, и его значение устанавливается в значение true. Метод hasPerm() принимает описание разрешения и возвращает значение на основе текущего объекта.

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

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

Используйте следующий код для создания файла PrivilegedUser.php :

 <?php class PrivilegedUser extends User { private $roles; public function __construct() { parent::__construct(); } // override User method public static function getByUsername($username) { $sql = "SELECT * FROM users WHERE username = :username"; $sth = $GLOBALS["DB"]->prepare($sql); $sth->execute(array(":username" => $username)); $result = $sth->fetchAll(); if (!empty($result)) { $privUser = new PrivilegedUser(); $privUser->user_id = $result[0]["user_id"]; $privUser->username = $username; $privUser->password = $result[0]["password"]; $privUser->email_addr = $result[0]["email_addr"]; $privUser->initRoles(); return $privUser; } else { return false; } } // populate roles with their associated permissions protected function initRoles() { $this->roles = array(); $sql = "SELECT t1.role_id, t2.role_name FROM user_role as t1 JOIN roles as t2 ON t1.role_id = t2.role_id WHERE t1.user_id = :user_id"; $sth = $GLOBALS["DB"]->prepare($sql); $sth->execute(array(":user_id" => $this->user_id)); while($row = $sth->fetch(PDO::FETCH_ASSOC)) { $this->roles[$row["role_name"]] = Role::getRolePerms($row["role_id"]); } } // check if user has a specific privilege public function hasPrivilege($perm) { foreach ($this->roles as $role) { if ($role->hasPerm($perm)) { return true; } } return false; } } 

Первый метод, getByUsername() , возвращает объект, заполненный информацией о конкретном пользователе. Метод, почти идентичный этому, вероятно, уже существует в вашем пользовательском классе, но вам необходимо переопределить его здесь, чтобы методы PrivilegedUser могли быть вызваны с соответствующим объектом. Если вы попытаетесь вызвать метод PrivilegedUser для объекта User , вы получите сообщение о том, что метод не существует.

Второй метод, initRoles() , использует JOIN для объединения user_role и user_role для сбора ролей, связанных с ID текущего пользователя. Каждая роль затем заполняется соответствующими разрешениями с помощью вызова метода класса Role созданного ранее, Role::getRolePerms() .

Последний метод hasPrivilege() принимает описание разрешения и возвращает true, если у пользователя есть разрешение, или false в противном случае.

При наличии двух предыдущих классов проверка, имеет ли пользователь определенную привилегию, так же проста:

 <?php require_once "Role.php"; require_once "PrivilegedUser.php"; // connect to database... // ... session_start(); if (isset($_SESSION["loggedin"])) { $u = PrivilegedUser::getByUsername($_SESSION["loggedin"]); } if ($u->hasPrivilege("thisPermission")) { // do something } 

Здесь имя пользователя сохраняется в активном сеансе, и для этого пользователя создается новый объект PrivilegedUser для которого можно hasPrivilege() метод hasPrivilege() . В зависимости от информации в вашей базе данных выходные данные вашего объекта будут выглядеть примерно так:

  object (PrivilegedUser) # 3 (2) {
   [ "роль": "PrivilegedUser": частный] =>
   массив (1) {
     [ "Admin"] =>
     объект (роль) № 5 (1) {
       [ "Разрешение": защита] =>
       массив (4) {
         [ "AddUser"] => BOOL (истина)
         [ "EditUser"] => BOOL (истина)
         [ "DeleteUser"] => BOOL (истина)
         [ "EditRoles"] => BOOL (истина)
       }
     }
   }
   [ "Полей": "Пользователь": частный] =>
   массив (4) {
     ["user_id"] => string (1) "2"
     ["username"] => string (7) "mpsinas"
     [ "Пароль"] => BOOL (ложь)
     ["email_addr"] => string (0) ""
   }
 } 

Держать вещи организованными

Одно из многих преимуществ использования ООП-подхода с RBAC заключается в том, что он позволяет отделить логику кода и проверку от конкретных задач объекта. Например, вы можете добавить следующие методы к вашему классу Role чтобы помочь управлять специфическими для ролей операциями, такими как вставка новых ролей, удаление ролей и т. Д.

 // insert a new role public static function insertRole($role_name) { $sql = "INSERT INTO roles (role_name) VALUES (:role_name)"; $sth = $GLOBALS["DB"]->prepare($sql); return $sth->execute(array(":role_name" => $role_name)); } // insert array of roles for specified user id public static function insertUserRoles($user_id, $roles) { $sql = "INSERT INTO user_role (user_id, role_id) VALUES (:user_id, :role_id)"; $sth = $GLOBALS["DB"]->prepare($sql); $sth->bindParam(":user_id", $user_id, PDO::PARAM_STR); $sth->bindParam(":role_id", $role_id, PDO::PARAM_INT); foreach ($roles as $role_id) { $sth->execute(); } return true; } // delete array of roles, and all associations public static function deleteRoles($roles) { $sql = "DELETE t1, t2, t3 FROM roles as t1 JOIN user_role as t2 on t1.role_id = t2.role_id JOIN role_perm as t3 on t1.role_id = t3.role_id WHERE t1.role_id = :role_id"; $sth = $GLOBALS["DB"]->prepare($sql); $sth->bindParam(":role_id", $role_id, PDO::PARAM_INT); foreach ($roles as $role_id) { $sth->execute(); } return true; } // delete ALL roles for specified user id public static function deleteUserRoles($user_id) { $sql = "DELETE FROM user_role WHERE user_id = :user_id"; $sth = $GLOBALS["DB"]->prepare($sql); return $sth->execute(array(":user_id" => $user_id)); } 

Аналогично, вы можете добавить в свой класс PrivilegedUser похожие методы:

 // check if a user has a specific role public function hasRole($role_name) { return isset($this->roles[$role_name]); } // insert a new role permission association public static function insertPerm($role_id, $perm_id) { $sql = "INSERT INTO role_perm (role_id, perm_id) VALUES (:role_id, :perm_id)"; $sth = $GLOBALS["DB"]->prepare($sql); return $sth->execute(array(":role_id" => $role_id, ":perm_id" => $perm_id)); } // delete ALL role permissions public static function deletePerms() { $sql = "TRUNCATE role_perm"; $sth = $GLOBALS["DB"]->prepare($sql); return $sth->execute(); } 

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

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

Резюме

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

Изображение через PILart / Shutterstock