[Img_assist | нидь = 3998 | название = | убывание = | ссылка = URL | URL = HTTP: //www.amazon.com/exec/obidos/ASIN/1847193579/javalobby-20 творческий | ALIGN = право | ширина = 203 | height = 240] Теперь у нас есть некоторые идеи о базе данных, мы быстро столкнемся с другим требованием. Многие веб-сайты захотят контролировать, кто имеет к чему доступ. Оказавшись на этом маршруте, выясняется, что существует множество ситуаций, в которых управление доступом подходит, и они могут легко стать очень сложными. Поэтому в этой главе мы рассмотрим наиболее уважаемую модель управления доступом на основе ролей и найдем способы ее реализации. Цель состоит в том, чтобы добиться гибкой и эффективной реализации, которая может использоваться все более сложным программным обеспечением. Чтобы показать, что происходит, используется пример расширения хранилища файлов.
Эта проблема
Нам необходимо спроектировать и внедрить систему управления доступом на основе ролей (RBAC), продемонстрировать ее использование и обеспечить, чтобы система могла обеспечить:
- простая структура данных
- гибкий код для обеспечения удобного интерфейса RBAC
- эффективность, чтобы RBAC избегал значительных накладных расходов
Обсуждение и соображения
Компьютерные системы давно нуждаются в контроле доступа. Раннее программное обеспечение обычно попадает в категорию, которая стала известна как списки контроля доступа (ACL). Но они обычно применялись на довольно низком уровне в системах и относились к базовым компьютерным операциям. Дальнейшее развитие принесло программное обеспечение, предназначенное для решения более общих проблем, таких как контроль конфиденциальных документов. Большая работа была проделана в области дискреционного контроля доступа (DAC) и обязательного контроля доступа (MAC).
Большое количество научных исследований было посвящено всему вопросу контроля доступа. Кульминацией этой работы является то, что наиболее популярной моделью является система контроля доступа на основе ролей, такая глоточная, что в дальнейшем используется аббревиатура RBAC. Теперь, хотя академический анализ может быть заумным, нам нужно практическое решение проблемы управления доступом к услугам на веб-сайте. К счастью, как и в случае реляционной базы данных, рассмотренной в предыдущей главе, понятия RBAC достаточно просты.
RBAC включает в себя некоторые основные объекты. К сожалению, терминология не всегда согласована, поэтому давайте будем ближе к основной и определим некоторые, которые будут использоваться для реализации нашего решения:
- Субъект: Субъект — это нечто контролируемое Это может быть целая веб-страница, но вполне может быть нечто более конкретное, например, папка в файловой системе хранилища. Этот пример указывает на тот факт, что субъект часто может быть разделен на два элемента: тип и идентификатор. Таким образом, папки файлового репозитория считаются типом субъекта, и каждая отдельная папка имеет своего рода идентификатор.
- Действие: Действие возникает потому, что нам обычно нужно сделать больше, чем просто разрешить или запретить доступ к объектам RBAC. В нашем примере мы можем накладывать различные ограничения на загрузку файлов в папку и загрузку файлов из папки. Таким образом, наши действия могут включать «загрузить» и «загрузить».
- Аксессор . Простейшим примером аксессора является пользователь. Аксессор — это тот, кто хочет выполнить действие. Неоправданно ограниченно полагать, что средства доступа всегда являются пользователями. Возможно, мы захотим рассматривать другие компьютерные системы как средства доступа, или средство доступа может представлять собой определенную часть программного обеспечения. Аксессоры подобны тем, которые разбиты на две части. Первая часть — это своего рода средство доступа, при этом пользователи веб-сайта являются наиболее распространенным видом. Вторая часть представляет собой идентификатор для конкретного средства доступа, которым может быть идентификационный номер пользователя.
- Разрешение: комбинация субъекта и действия является разрешением. Так, например, возможность загрузки файлов из определенной папки в хранилище файлов будет разрешением.
- Назначение: в RBAC никогда не существует прямой связи между автором доступа и разрешением на выполнение действия над субъектом. Вместо этого методам доступа назначается одна или несколько ролей. Связывание аксессора и роли является назначением.
- Роль: роль является носителем разрешений и похожа на понятие группы. Это роли, которым предоставлено одно или несколько разрешений.
Легко видеть, что мы можем контролировать то, что можно сделать, выделяя роли пользователям, а затем проверяя, имеет ли какая-либо из ролей пользователя определенное разрешение. Более того, мы можем обобщить это за пределами пользователей на другие типы аксессоров, когда возникнет такая необходимость. Построенная модель известна в научной литературе как RBAC.
Добавление иерархии
Поскольку RBAC может работать на гораздо более общем уровне, чем ACL, часто случается, что одна роль охватывает другую. Предположим, мы думаем о примере больницы, роль консультанта может включать роль врача. Не каждый, кто имеет роль доктора, будет иметь роль консультанта. Но все консультанты — врачи.
В настоящее время Aliro реализует иерархию исключительно для обратной совместимости с Mambo и Joomla! схемы, где существует строгая иерархия ролей для ACL. Возможность расширить иерархию в более общем смысле возможна, учитывая реализацию Aliro, и может быть добавлена в некоторый момент.
Модель с добавлением ролевых иерархий называется RBAC.
Добавление ограничений
При общей обработке данных возникают ситуации, когда RBAC, как ожидается, реализует ограничения на распределение ролей. Типичным примером может быть то, что одному и тому же лицу не разрешено иметь роли как менеджера по закупкам, так и менеджера по работе с клиентами. Подобные ограничения вытекают из довольно очевидных принципов, ограничивающих возможности для мошенничества.
Хотя ограничения могут быть мощным дополнением к RBAC, они не часто возникают в веб-приложениях, поэтому Aliro в настоящее время не предоставляет никаких возможностей для ограничений. Эта опция не исключается, поскольку ограничения обычно устанавливаются поверх системы RBAC, в которой их нет.
Добавление ограничений к базовой модели RBAC создает модель RBAC2, и, если предоставляются иерархия и ограничения, модель называется RBAC.
Избегайте ненужных ограничений
Когда речь идет о разработке реализации, было бы жаль создавать препятствия, которые будут неприятны позже. Чтобы достичь максимальной гибкости, на информацию, которая хранится в системе RBAC, накладываются некоторые ограничения.
Субъекты и методы доступа имеют как типы, так и идентификаторы. Типы могут быть строками, и системе RBAC не нужно ограничивать то, что можно использовать в этом отношении. Умеренное ограничение по длине не является чрезмерно ограничительным. Например, более широкая CMS должна решить, какие виды предметов необходимы. Наш пример для этой главы — файл репозитория, а темы, в которых он нуждается, известны разработчику репозитория. Все запросы к системе RBAC из хранилища файлов будут учитывать эти знания.
Идентификаторы часто представляют собой простые числа, вероятно, полученные из первичного ключа с автоинкрементом в базе данных. Но было бы чрезмерно ограничительным утверждать, что идентификаторы должны быть числами. Может случиться так, что контроль необходим над субъектами, которые не могут быть идентифицированы по номеру. Возможно, субъект может быть идентифицирован только с помощью нечислового ключа, такого как URI, или может потребоваться более одного поля, чтобы выделить его.
По этим причинам лучше реализовать систему RBAC с идентификаторами в виде строк, возможно, с довольно щедрыми ограничениями длины. Таким образом, разработчики программного обеспечения, использующие систему RBAC, имеют максимальную возможность создавать идентификаторы, которые работают в определенном контексте. Можно представить любое количество схем, которые объединят несколько полей в строку; в конце концов, единственное, что мы будем делать с идентификатором в системе RBAC, — это проверка на равенство. При условии, что идентификаторы уникальны, их точная структура не имеет значения. Единственное, на что нужно обратить внимание — убедиться, что каким бы ни был исходный идентификатор, он последовательно преобразуется в строку.
Действия могут быть простыми строками, так как они являются просто произвольными метками. Опять же, их значение важно только в той области, где применяется RBAC, поэтому реальной системе RBAC не нужно налагать какие-либо ограничения. Длина не должна быть особенно большой.
Роли схожи, хотя системы иногда включают в себя таблицу ролей, поскольку содержится дополнительная информация, например описание роли. Но поскольку это не является обязательным требованием RBAC, система, построенная здесь, не будет требовать описания ролей и позволит роли быть любой произвольной строкой. Хотя описания могут быть полезны, их легко предоставить в качестве дополнительной опции. Исключение требований к ним делает систему максимально гибкой и значительно упрощает создание ролей, что часто будет необходимо.
Некоторые особые роли
Обработка контроля доступа может быть упрощена и более эффективна, если придумать некоторые роли, которые имеют свои особые свойства. Алиро использует три из них: посетитель, зарегистрированный, и никто.
Каждый, кто заходит на сайт, считается посетителем, и поэтому неявно получает роль посетителя. Если на эту роль предоставлено право, предполагается, что оно предоставлено каждому. В конце концов, нелогично давать права посетителю и отказывать в нем пользователю, который вошел в систему, поскольку пользователь может получить право доступа, просто выйдя из системы.
Для эффективной реализации роли посетителя сделано две вещи. Во-первых, ничего не хранится, чтобы связать определенных пользователей с ролью, так как у всех это есть автоматически. Во-вторых, так как большинство сайтов предоставляют довольно большой доступ посетителям до входа в систему, роли посетителя предоставляется доступ ко всему, что не связано с какой-либо более конкретной ролью. Это означает, опять же, что ничего не нужно хранить в связи с ролью посетителя.
Почти столь же обширна регистрируемая роль, которая автоматически применяется ко всем, кто вошел в систему, но исключает посетителей, которые не вошли в систему. Опять же, ничего не сохраняется, чтобы связать пользователей с ролью, поскольку она применяется к любому, кто идентифицирует себя как зарегистрированный пользователь. Но в этом случае права могут быть предоставлены зарегистрированной роли. Как и роль посетителя, логика диктует, что, если доступ предоставляется всем зарегистрированным пользователям, любые более конкретные права являются избыточными и могут быть проигнорированы.
Наконец, роль «никто» полезна из-за принципа, согласно которому, если не был предоставлен определенный доступ, ресурс доступен каждому. Если весь доступ должен быть заблокирован, то доступ может быть предоставлен «никому», и ни одному пользователю не разрешено быть «никем». Фактически, теперь мы можем видеть, что ни один пользователь не может быть назначен какой-либо из специальных ролей, поскольку они всегда связаны с ними автоматически или не связаны вообще.
Эффективность внедрения
Очевидно, что системе RBAC, возможно, придется обрабатывать много данных. Что еще более важно, может потребоваться обработать много запросов в короткие сроки. Страница вывода часто состоит из нескольких элементов, каждый из которых может включать решения о доступе.
К этой проблеме можно применить двунаправленный подход, использующий два разных типа кэша. Некоторые данные RBAC носят общий характер, очевидным примером является иерархия ролей. Это в равной степени относится ко всем, и это относительно небольшой объем данных. Информация такого рода может кэшироваться в файловой системе, чтобы быть доступной для каждого запроса.
Большая часть информации RBAC связана с конкретным пользователем. Если бы все такие данные были сохранены в стандартном кеше, вполне вероятно, что кэш вырастет очень большим, причем большая часть данных не имеет отношения к какому-либо конкретному запросу. Лучшим подходом является сохранение данных RBAC, определенных для пользователя, в качестве данных сеанса. Таким образом, он будет доступен для каждого запроса одним и тем же пользователем, но не будет загроможден данными для других пользователей. Поскольку Aliro гарантирует, что существует сеанс в реальном времени для каждого пользователя, включая посетителей, которые еще не вошли в систему, а также сохраняет данные сеанса при входе в систему, это осуществимый подход.
Где настоящие трудности?
Может быть, вы думаете, у нас уже есть достаточно проблем, чтобы решить, не ища других? Печальный факт в том, что мы еще даже не считали самым сложным! По моему опыту, реальные трудности возникают при попытке разработать пользовательский интерфейс для удовлетворения реальных требований к управлению.
Пример, используемый в этой главе, относительно прост. Управление тем, что пользователи могут делать в расширении репозитория файлов, не сразу представляет большую сложность. Но эта, по-видимому, простая ситуация легко усложняется с помощью запросов, которые часто направляются в более продвинутый репозиторий.
В простом случае нам нужно беспокоиться только о том, что мы можем контролировать области репозитория, указывая, кто может загружать, кто может загружать и кто может редактировать файлы. Это те требования, которые описаны в примерах ниже.
Выходя за рамки этого, рассмотрим ситуацию, которая часто обсуждается как возможное требование. Репозиторий расширен, так что некоторые пользователи имеют свою собственную область и могут делать в ней то, что им нравится. Простым следствием этого является то, что нам необходимо предоставить этим пользователям возможность создавать новые папки в репозитории файлов, а также загружать и редактировать файлы в существующих папках. Все идет нормально! Но этот сценарий также вводит идею, что мы можем захотеть, чтобы пользователь, владеющий областью репозитория, мог контролировать определенные области, к которым другие пользователи могут иметь доступ. Теперь нам нужна дополнительная возможность контролировать, какие пользователи имеют право предоставлять доступ к определенным частям репозитория. Если мы хотим пойти еще дальше,мы можем поднять вопрос о том, сможет ли пользователь, занимающий эту должность, делегировать предоставление доступа в своей области другим пользователям, чтобы достичь полной иерархии контроля.
Выполнение технических требований здесь не так уж сложно. Трудно разрабатывать пользовательские интерфейсы, чтобы использовать все возможности, не создавая при этом сложности. Для отдельного случая возможно найти решение. Попытка создать общее решение, вероятно, приведет к проблеме, которую будет чрезвычайно трудно решить.
Рамочное решение
Реализация контроля доступа делится на три класса. Одним из них является класс, который задает вопросы о том, кто что может делать. С этим тесно связан еще один класс, который кэширует общую информацию, применимую ко всем пользователям. Он сделан отдельным классом, чтобы помочь реализации разделения кэша между generalD и пользовательской спецификацией. Третий класс занимается административными операциями. Но прежде чем смотреть на классы, давайте разберемся с дизайном базы данных.
База данных для RBAC
Все, что требуется для реализации базового RBAC, — это две таблицы. Третья таблица требуется для расширения до иерархической модели. Необязательная дополнительная таблица может быть реализована для хранения описаний ролей. Возвращаясь к соображениям дизайна, первой необходим способ записи операций, которые можно выполнять над субъектами, то есть разрешения. Они являются целью нашей системы контроля доступа. Вы помните, что разрешение состоит из действия и субъекта, где субъект определяется типом и идентификатором. Для удобства обработки добавлен простой идентификационный номер с автоинкрементом. Но нам также нужна пара других вещей.
Чтобы сделать нашу систему RBAC общей, важно иметь возможность контролировать не только фактические разрешения, но также и то, кто может предоставлять эти разрешения, и могут ли они предоставлять это право другим. Таким образом, добавляется дополнительное поле управления с одним битом для каждой из этих трех возможностей. Поэтому становится возможным предоставить право доступа к чему-либо с возможностью или без возможности передать это право.
Еще один полезный дополнительный элемент данных — это системный флажок. Он используется, чтобы сделать некоторые разрешения неспособными к удалению. Хотя это и не является логическим требованием, это, безусловно, практическое требование. Мы хотим предоставить администраторам большую власть над настройкой прав доступа, но в то же время мы хотим избежать любых катастроф. Подобные вещи, которые были бы крайне нежелательными, могли бы для администратора высшего уровня удалить все свои права на систему. На практике большинство систем имеют критически важную центральную структуру прав, которая не должна изменяться даже высшим администратором.
Итак, теперь таблица разрешений выглядит так, как показано на следующем снимке экрана:
[Img_assist | NID = 3993 | название = | убывание = | ссылка = нет | Align = нет | ширина = 282 | высота = 201]
Обратите внимание, что строки символов для role, action и subject_type имеют общую длину 60, что должно быть более чем достаточно. Идентификатор субъекта часто будет довольно коротким, но во избежание ограничения общности он сделан текстовым полем, так что система RBAC может при необходимости обрабатывать очень сложные идентификаторы. Конечно, будут некоторые потери производительности, если это поле очень длинное, но лучше компромисс дизайна, чем ограничение. Если бы мы ограничивали идентификатор субъекта числом, то более сложные идентификаторы были бы особым случаем. Это разрушит общность нашей схемы и может в конечном итоге снизить общую эффективность. В дополнение к идентификатору первичного ключа с автоинкрементом создаются два индекса, как показано на следующем снимке экрана.Они связаны с накладными расходами во время операций обновления, но могут ускорить операции доступа. Так как обычно гораздо больше обращений, чем обновлений, это имеет смысл. Если по какой-то причине индекс не дает выгоды, его всегда можно отбросить. Обратите внимание, что индекс идентификатора субъекта должен быть ограничен по длине, чтобы избежать нарушения ограничений на размер ключа. Выбранное значение является компромиссом между эффективностью с помощью коротких ключей и эффективностью с помощью мелких ключей. В интенсивно используемой системе стоило бы тщательно рассмотреть выбранную фигуру и, возможно, изменить ее в свете изучения реальных данных.Обратите внимание, что индекс идентификатора субъекта должен быть ограничен по длине, чтобы избежать нарушения ограничений на размер ключа. Выбранное значение является компромиссом между эффективностью с помощью коротких ключей и эффективностью с помощью мелких ключей. В интенсивно используемой системе стоило бы тщательно рассмотреть выбранную фигуру и, возможно, изменить ее в свете изучения реальных данных.Обратите внимание, что индекс идентификатора субъекта должен быть ограничен по длине, чтобы избежать нарушения ограничений на размер ключа. Выбранное значение является компромиссом между эффективностью с помощью коротких ключей и эффективностью с помощью мелких ключей. В интенсивно используемой системе стоило бы тщательно рассмотреть выбранную фигуру и, возможно, изменить ее в свете изучения реальных данных.
[Img_assist | NID = 3994 | название = | убывание = | ссылка = нет | Align = нет | ширина = 519 | Высота = 250]
Другая основная таблица базы данных еще проще и содержит информацию о назначении средств доступа к ролям. Опять же, для удобства добавлен идентификатор автоинкремента. Помимо идентификатора, требуются только поля, роль, тип доступа и идентификатор доступа. На этот раз достаточно одного индекса, дополнительного к первичному ключу. Таблица назначений показана на следующем снимке экрана, а ее индекс показан на снимке экрана после этого:
[Img_assist | NID = 3995 | название = | убывание = | ссылка = нет | Align = нет | ширина = 277 | Высота = 128]
[Img_assist | NID = 3996 | название = | убывание = | ссылка = нет | Align = нет | ширина = 537 | высота = 151]
Для добавления иерархии в RBAC требуется только очень простая таблица, в которой каждая строка содержит два поля: роль и подразумеваемую роль. Оба поля составляют первичный ключ, и ни одно из полей не должно быть уникальным. Индекс не требуется для эффективности, поскольку предполагается, что объем информации об иерархии мал, и всякий раз, когда это необходимо, читается вся таблица. Но все еще хороший принцип — иметь первичный ключ, и он также гарантирует, что не будет избыточных записей. Для приведенного ранее примера типичная запись может иметь консультанта в качестве роли и доктора в качестве предполагаемой роли. В настоящее время Aliro реализует иерархию только для обратной совместимости, но сделать иерархические отношения в общем доступе относительно легко.
Опционально можно использовать дополнительную таблицу для описания используемых ролей. Это не имеет функционального назначения и является просто возможностью помочь администраторам системы. Таблица должна иметь роль первичного ключа. Поскольку это никак не влияет на функциональность RBAC, дальнейших подробностей здесь не приводится.
С разработанным дизайном базы данных, давайте посмотрим на классы. Самый простой класс администрирования, поэтому мы начнем с него.
Администрирование RBAC
Администрирование системы может быть выполнено путем записи непосредственно в базу данных, поскольку именно это включает в себя большинство операций. Есть веские причины не делать этого. Хотя операции просты, важно, чтобы они обрабатывались правильно. Как правило, плохой принцип — разрешать доступ к механизмам системы, а не предоставлять интерфейс через методы класса. Последний подход в идеале позволяет создать надежный интерфейс, который изменяется относительно редко, в то время как детали реализации могут быть изменены, не затрагивая остальную часть системы.
Класс администрирования отделен от классов, обрабатывающих вопросы о доступе, поскольку для большинства запросов CMS администрирование не требуется, а класс администрирования не загружается вообще. Как центральная служба, класс реализован как стандартный одноэлементный, но он не кэшируется, потому что информация обычно должна быть немедленно записана в базу данных. Фактически, класс администрирования часто запрашивает класс кэширования авторизации, чтобы очистить свой кэш, чтобы изменения в базе данных могли вступить в силу немедленно. Класс начинается с:
class aliroAuthorisationAdmin
{
private static $instance = __CLASS__;
private $handler = null;
private $authoriser = null;
private $database = null;
private function __construct()
{
$this->handler =& aliroAuthoriserCache::getInstance();
$this->authoriser =& aliroAuthoriser::getInstance();
$this->database = aliroCoreDatabase::getInstance();
}
private function __clone()
{
// Enforce singleton
}
public static function getInstance()
{
return is_object(self::$instance) ? self::$instance :
(self::$instance = new self::$instance());
}
private function doSQL($sql, $clear=false)
{
$this->database->doSQL($sql);
if ($clear) $this->clearCache();
}
private function clearCache()
{
$this->handler->clearCache();
}
Помимо свойства экземпляра, которое используется для реализации одноэлементного шаблона, другие частные свойства являются связанными объектами, которые получены в конструкторе для помощи другим методам. Получение экземпляра работает обычным образом для одиночного объекта с закрытым конструктором и методами-клонами, обеспечивающими доступ только через getInstance.
Метод doSQL также упрощает другие методы, комбинируя вызов к базе данных с необязательной очисткой кэша с помощью метода clearCache класса. Очевидно, что последнее достаточно просто, чтобы его можно было устранить. Но лучше иметь метод на месте, чтобы, если бы изменения были внесены в реализацию таким образом, чтобы при очистке любого соответствующего кэша требовались различные действия, изменения были бы изолированы от метода clearCache. Далее у нас есть пара полезных методов, которые просто ссылаются на один из других классов RBAC:
public function getAllRoles($addSpecial=false)
{
return $this->authoriser->getAllRoles($addSpecial);
}
public function getTranslatedRole($role)
{
return $this->authoriser->getTranslatedRole($role);
}
Опять же, они предоставляются для того, чтобы упростить будущую эволюцию кода, чтобы детали реализации были сосредоточены в легко идентифицируемых местах. Общая идея getAllRoles очевидна из названия, и этот параметр определяет, будут ли включены специальные роли, такие как посетитель, зарегистрирован и никто. Поскольку эти роли встроены в систему на английском языке, было бы полезно иметь возможность получать местные переводы для них. Таким образом, метод getTranslatedRole вернет перевод для любой из специальных ролей; для других ролей он вернет параметр без изменений, поскольку роли создаются динамически в виде текстовых строк и, следовательно, обычно с самого начала будут на местном языке. Теперь мы готовы взглянуть на первый мясистый метод:
public function permittedRoles ($action, $subject_type, $subject_id)
{
$nonspecific = true;
foreach ($this->permissionHolders ($subject_type, $subject_id)
as $possible)
{
if ('*' == $possible->action OR $action == $possible->action)
{
$result[$possible->role] = $this->getTranslatedRole
($possible->role);
if ('*' != $possible->subject_type AND '*' !=
$possible_subject_id) $nonspecific = false;
}
}
if (!isset($result))
{
if ($nonspecific) $result = array('Visitor' =>
$this->getTranslatedRole('Visitor'));
else return array();
}
return $result;
}
private function &permissionHolders ($subject_type, $subject_id)
{
$sql = "SELECT DISTINCT role, action, control, subject_type,
subject_id FROM #__permissions";
if ($subject_type != '*') $where[] =
"(subject_type='$subject_type' OR subject_type='*')";
if ($subject_id != '*') $where[] = "(subject_id='$subject_id' OR
subject_id='*')";
if (isset($where)) $sql .= " WHERE ".implode(' AND ', $where);
return $this->database->doSQLget($sql);
}
Любой код, который обеспечивает функцию администрирования RBAC для некоторой части CMS, вероятно, захочет узнать, какие роли уже имеют определенное разрешение, чтобы показать это администратору при подготовке к любым изменениям. Закрытый методmissionHolders использует параметры для создания оператора SQL, который получит минимальные соответствующие записи о разрешениях. Это осложняется тем, что в большинстве случаев звездочку можно использовать в качестве подстановочного знака.
Открытый метод allowRoles использует метод private для получения соответствующих строк базы данных из таблицы разрешений. Они проверяются по параметру действия, чтобы увидеть, какие из них актуальны. Если нет результатов или если ни один из результатов не относится конкретно к теме, без использования подстановочных знаков, то предполагается, что все посетители могут получить доступ к теме, поэтому к результатам добавляется особая роль посетителя. Когда фактическое разрешение должно быть предоставлено, нам нужны следующие методы:
public function permit ($role, $control, $action, $subject_type, $subject_id)
{
$sql = $this->permitSQL($role, $control, $action, $subject_type,
$subject_id);
$this->doSQL($sql, true);
}
private function permitSQL ($role, $control, $action, $subject_type, $subject_id)
{
$this->database->setQuery("SELECT id FROM #__permissions WHERE
role='$role' AND action='$action' AND
subject_type='$subject_type' AND
subject_id='$subject_id'");
$id = $this->database->loadResult();
if ($id) return "UPDATE #__permissions SET control=$control WHERE id=$id";
else return "INSERT INTO #__permissions (role, control, action,
subject_type, subject_id) VALUES ('$role', '$control',
'$action', '$subject_type', '$subject_id')";
}
Публичное разрешение метода дает разрешение на роль. Управляющие биты устанавливаются в параметре $ control. Действие является частью разрешения, а субъект действия определяется типом субъекта и параметрами идентификации. Большая часть работы выполняется закрытым методом, который генерирует SQL; он хранится отдельно, чтобы его можно было использовать другими методами. Как только SQL получен, его можно передать в базу данных, и, поскольку он обычно приводит к изменениям, устанавливается опция очистки кеша.
Сгенерированный SQL зависит от того, есть ли уже разрешение с теми же параметрами, и в этом случае обновляются только контрольные биты. В противном случае вставка происходит. Причиной необходимости выполнить SELECT вначале, а затем выбрать INSERT или UPDATE является то, что индекс соответствующих полей не гарантированно является уникальным, а также потому, что идентификатор субъекта может быть намного длиннее, чем может быть включен в пределах индекса. Поэтому невозможно использовать ON DUPLICATE KEY UPDATE.
Везде, где возможно, это помогает эффективно использовать опцию MySQL для ON DUPLICATE KEY UPDATE. Это добавляется в конец инструкции INSERT, и, если INSERT завершается неудачей из-за ключа, уже существующего в таблице, выполняются альтернативные действия, следующие за ON DUPLICATE KEY UPDATE. Они состоят из одного или нескольких назначений, разделенных запятыми, как в операторе UPDATE. ГДЕ не допускается, поскольку условие для назначений уже определяется ситуацией с дубликатом ключа.
Простой метод позволяет удалить все разрешения для определенного действия и темы:
public function dropPermissions ($action, $subject_type, $subject_id)
{
$sql = "DELETE FROM #__permissions WHERE action='$action' AND
subject_type='$subject_type'AND subject_id='$subject_id'
AND system=0";
$this->doSQL($sql, true);
}
Последний набор методов относится к назначению средств доступа к ролям. Два из них отражают очевидную необходимость иметь возможность удалить все роли из средства доступа (возможно, подготовительного к назначению новых ролей) и предоставить роль средству доступа. Там, где необходимо назначить целый набор ролей, лучше иметь метод специально для этой цели. Отчасти это удобно, но также обеспечивает дополнительную операцию, минимизацию набора ролей. Метод таков:
public function assign ($role, $access_type, $access_id, $clear=true)
{
if ($this->handler->barredRole($role)) return false;
$this->database->setQuery("SELECT id FROM #__assignments WHERE
role='$role' AND access_type='$access_type' AND
access_id='$access_id'");
if ($this->database->loadResult()) return true;
$sql = "INSERT INTO #__assignments (role, access_type, access_id)
VALUES ('$role', '$access_type', '$access_id')";
$this->doSQL($sql, $clear);
return true;
}
public function assignRoleSet ($roleset, $access_type, $access_id)
{
$this->dropAccess ($access_type, $access_id);
$roleset = $this->authoriser->minimizeRoleSet($roleset);
foreach ($roleset as $role) $this->assign ($role, $access_type,
$access_id, false);
$this->clearCache();
}
public function dropAccess ($access_type, $access_id)
{
$sql = "DELETE FROM #__assignments WHERE
access_type='$access_type' AND access_id='$access_id'";
$this->doSQL($sql, true);
}
Метод assign связывает роль с аксессором. Сначала он проверяет наличие запрещенных ролей, это просто особые роли, обсуждавшиеся ранее, которые не могут быть назначены никаким методам доступа. Как и в случае метода allowSQL, невозможно использовать ON DUPLICATE KEY UPDATE, потому что полная длина идентификатора средства доступа не является частью индекса, поэтому снова проверяется наличие назначения в первую очередь. Если назначение роли уже есть в базе данных, делать нечего. В противном случае строка вставляется, а кэш очищается.
Избавление от всех назначений ролей для средства доступа — это простое удаление базы данных, реализованное в методе dropAccess. Метод более высокого уровня assignRoleSet использует dropAccess для очистки любых существующих назначений. Призыв к объекту Authorizer для минимизации набора ролей отражает реализацию иерархической модели. При наличии иерархии одна роль может подразумевать другую в качестве консультанта, подразумеваемого врачом в нашем предыдущем примере. Это означает, что набор ролей может содержать избыточность. Например, кому-то, кому была назначена роль консультанта, не обязательно назначать роль врача. Метод minimalRoleSet отсеивает любые излишние роли. Как только это будет сделано, каждая роль обрабатывается с использованием метода assign, при этом очистка кэша сохраняется до самого конца.
Общий кэш RBAC
As outlined earlier, the information needed to deal with RBAC questions is cached in two ways. The fi le system cache is handled by the aliroAuthoriserCache singleton class, which inherits from the cachedSingleton class and is described fully in Chapter 8, on caches. This means that the data of the singleton object will be automatically stored in the fi le system whenever possible, with the usual provisions for timing out an old cache, or clearing the cache when an update has occurred. It is highly desirable to cache the data both to avoid database operations and to avoid repeating the processing needed in the constructor. So the intention is that the constructor method will run only infrequently. It contains this code:
protected function __construct()
{
// Making private enforces singleton
$database = aliroCoreDatabase::getInstance();
$database->setQuery("SELECT role, implied FROM #__role_link UNION
SELECT DISTINCT role, role AS implied FROM
#__assignments UNION SELECT DISTINCT role,
role AS implied FROM #__permissions");
$links = $database->loadObjectList();
if ($links) foreach ($links as $link)
{
$this->all_roles[$link->role] = $link->role;
$this->linked_roles[$link->role][$link->implied] = 1;
foreach ($this->linked_roles as $role=>$impliedarray)
{
foreach ($impliedarray as $implied=>$marker)
{
if ($implied == $link->role OR $implied == $link->implied)
{
$this->linked_roles[$role][$link->implied] = 1;
if (isset($this->linked_roles[$link->implied])) foreach
($this->linked_roles[$link->implied] as $more=>$marker)
{
$this->linked_roles[$role][$more] = 1;
}
}
}
}
}
$database->setQuery("SELECT role, access_id FROM #__assignments
WHERE access_type = 'aUser' AND (access_id = '*'
OR access_id = '0')");
$user_roles = $database->loadObjectList();
if ($user_roles) foreach ($user_roles as $role) $this-
>user_roles[$role->access_id][$role->role] = 1;
if (!isset($this->user_roles['0'])) $this->user_roles['0']
= array();
if (isset($this->user_roles['*'])) $this->user_roles['0'] =
array_merge($this->user_roles['0'], $this->user_roles['*']);
}
All possible roles are derived by a UNION of selections from the permissions, assignments, and linked roles database tables. The union operation has overheads, so that alone is one reason for favoring the use of a cache. The processing of linked roles is also complex, and therefore worth running as infrequently as possible. Rather than working through the code in detail, it is more useful to describe what it is doing. The concept is much simpler than the detail! If we take an example from the backwards compatibility features of Aliro, there is a role hierarchy that includes the role Publisher, which implies membership of the role Editor. The role Editor also implies membership of the role Author. In the general case, it is unreasonable to expect the administrator to fi gure out the implied relationships. In this case, it is clear that the role Publisher must also imply membership of the role Editor. But these linked relationships can plainly become quite complex. The code in the constructor therefore assumes that only the least number of connections have been entered into the database, and it fi gures out all the implications.
The other operation where the code is less than transparent is the setting of the user_roles property. The Aliro RBAC system permits the use of wild cards for specifi cation of identities within accessor, or subject types. An asterisk indicates any identity. For accessors whose accessor type is user, another wild card available is zero. This means any user who is logged in, and is not an unregistered visitor. Given the relatively small number of role assignments of this kind, it saves a good deal of processing if all of them are cached. Hence the user_roles processing is done in the constructor.
Other methods in the cache class are simple enough to be mentioned rather than given in detail. They include the actual implementation of the getTranslatedRole method, which provides local translations for the special roles. Other actual implementations are getAllRoles with the option to include the special roles, getTranslatedRole, which translates a role if it turns out to be one of the special ones and barredRole, which in turn, tests to see if the passed role is in the special group. It may therefore not be assigned to an accessor.
Asking RBAC Questions
Perhaps the most signifi cant class is the one that actually answers questions about permitted access. The aliroAuthoriser class is once again a singleton with the usual mechanisms. For convenience, it has getAllRoles and getTranslatedRole methods, but these are really implemented in the cache class described above.
The constructor does some relatively simple setting, including looking for cached data in the PHP super-global $_SESSION:
private function __construct()
{
// Make sure session started
aliroSessionFactory::getSession();
// Use session data as the source for cached user related data
foreach ($this->auth_vars as $one_var)
{
if (!isset($_SESSION['aliro_auth'][$one_var]))
$_SESSION['aliro_auth'][$one_var] = array();
$this->$one_var =& $_SESSION['aliro_auth'][$one_var];
}
$this->handler = aliroAuthoriserCache::getInstance();
$this->linked_roles = $this->handler->getLinkedRoles();
$this->database = aliroCoreDatabase::getInstance();
}
Getting the current session, even though it is not used directly for anything, ensures that a session has been started so that $_SESSION will contain data, if there is any. Since Aliro always activates a session, and much RBAC data is specifi c to the current user, it makes good sense to cache as session data. The handler and database objects are found using the usual singleton access method, getInstance, and linked roles are obtained from the authorizer cache.
Many RBAC questions involve roles, and the option of a hierarchy means that one role can imply another. This relationship is stored in the linked_roles property. Having roles implied means that a set of roles may include entries that are not really needed. The minimizeRoleSet method eliminates them:
public function minimizeRoleSet ($roleset)
{
if (0 == count($roleset)) return $roleset;
$first = array_shift($roleset);
foreach ($roleset as $key=>$role)
{
if (isset($this->linked_roles[$first][$role])) unset
($roleset[$key]);
if (isset($this->linked_roles[$role][$first])) return
$this->minimizeRoleSet ($roleset);
}
array_unshift($roleset, $first);
return $roleset;
}
There are about a score of other methods, some public, and some private. In detail, the key ones become quite complex. This is partly because of the nature of RBAC, and partly to do with attempts at effi ciency. Others are very simple, but this is because they are interfaces to the more substantial methods, but with simplifi ed parameters, so as to provide a more usable interface. Because of the complexity, a selection of the remaining classes is discussed in outline rather than being reviewed in detail. The full code is downloadable from the Aliro website.
Permissions refer to actions on subjects, and it is very likely that multiple queries will arise around similar subjects. The private method getSubjectData is used to load permissions, based on a subject and an action, that is, a specifi c permission. This method always ensures that all relevant rows from the permission table will be loaded. The number of directly relevant rows will be the number of roles that have the given permission. But the method also tries to get more data than is strictly necessary. Depending on the number of records involved, the method may load all permission data relating to the type of subject specifi ed, not merely to the specifi c subject. The precise number chosen is subject to optimization work. That is to say, all records where the subject type matches, not just those that match both subject type, and subject identifi er. This is done because it is common for a question about rights to a particular subject is often followed by a question about another subject of the same kind. The permission data that is loaded is organized into array structures to maximize the effi ciency of lookups, and it is also cached as session data.
The method getAccessorRoles is used both internally and externally. Its prototype is:
public function getAccessorRoles ($type, $id)
It also returns an array of roles. The processing is complicated by the storage of data in cache, something that is especially important for accessors since it is very likely that a number of questions will be asked about the current user. The parameters are the type of accessor (such as ‘a User’), and the identifi er (such as a user ID number).
A private method, accessorPermissionOrControl, does the basic work of fi nding out whether a particular accessor has rights to a given subject for a stated action. The type of access is passed as a bit mask. This method is then used to create a series of very simple public methods. The most frequently used has a prototype:
public function checkPermission ($a_type, $a_id, $action, $s_type='*', $s_id='*')
The result is zero or one to indicate false or true respectively. The accessor type and ID together defi ne the accessor. Action is self explanatory. Subject type and ID together defi ne the subject. There are situations where wild cards are used. For example, when the action is to manage and the subjects are all users, then the subject ID will be the asterisk wild card. Other actions may have no subject at all, in which case both subject type and ID will be asterisks.
For ease of development, an alternative to checkPermission is the method with prototype:
public function checkUserPermission ($action, $s_type='*', $s_id='*')
It assumes that the accessor is the current user, whose details can be obtained from a standard class in the CMS, so only the action and the subject need be specifi ed. Similar methods to the last two also exist to handle the granting of rights.
While the link between accessors, and subjects via roles can often be kept under the covers and handled within the authorizer class, in some cases it is needed explicitly. It is therefore possible to ask whether a particular role can access a subject for a particular action:
public function checkRolePermission ($role, $action, $s_type, $s_id)
When it comes to deciding questions of access to objects that are generally managed by another piece of software, the most effective query is to fi nd out which items are not available. Let’s return to our example of a fi le repository, where roles are given access to download from specifi c folders. A folder is identifi ed by its subject type, say remosFolder and an identifi er, which in this case, is an ID number. Because we have a rule saying that anything that does not have any specifi c permissions set is available to all, it is possible to identify a list of all the folders where there are permissions of some kind. For some of those, the user for whom we are asking may have been granted access, via their roles. So those folders are removed from the list. If any folders are left, they are the ones where access is not allowed. The method used to support these queries is:
public function getRefusedList ($a_type, $a_id, $s_type, $actionlist)
It returns an array of ID numbers, given an accessor defi ned by type, and ID along with a subject type, and an action list. The action list may be a single action, but for convenience, it is allowed to be a comma separated list of actions. The result is the ID numbers for all folders where the accessor is denied permission to carry out any of the actions.
Again to provide a more useful interface, an extended version of the method is available:
public function getRefusedListSQL ($a_type, $a_id, $s_type, $actionlist, $keyname)
It returns a fragment of SQL. Taking an example, if we call getRefusedListSQL( ‘aUser’, 47, ‘remosFolder’, ‘download’, ‘id’) we might get back a string containing CAST(id AS CHAR) NOT IN (‘5′, ’14’, ’27’). This can be used as part of a SQL statement to select folders where the user with ID 47 is allowed to download. So, supposing we want to get a list of the repository container names that are available to our sample user, the full SQL will be constructed using SELECT name FROM #__downloads_containers WHERE followed by the partial SQL provided by getRefusedListSQL. The fi nal sample SQL is then SELECT name FROM #__ downloads_containers WHERE CAST(id AS CHAR) NOT IN (‘5′, ’14’, ’27’).
Summary
We’ve now got at least the outline of a highly fl exible role-based access control system. The principles are established, using standard notions of RBAC. Specifi c details, such as the way accessors and subjects are identifi ed are adapted to the particular situation of a CMS framework.
The implementation in the database has been established in detail. We’ve studied the code for administering RBAC, and considered in outline how questions about access can be answered. Further details are available by downloading the Aliro implementation.
This article is excerpted from the book PHP5 CMS Framework Development, by Martin Brampton, and published on June 6, 2008 by Packt Publications.