При разработке почти любого программного продукта рано или поздно перед разработчиками встает проблема ограничения доступа. Например, в веб-приложении некоторые страницы могут быть доступны только администраторам или только зарегистрированным пользователям. В проекте Campus.ru такое разграничение доступа обеспечивается библиотекой Spring Security.
Spring Security работает со статическими ролями, которые определяются через URI при каждом запросе со стороны HTTP-клиента. Но как быть с динамическими ролями, которые меняются в зависимости от отношений между текущим пользователем и запрашиваемым объектом (например, роль автора по отношении к статье)?
Для решения задачи доступа к отдельным сущностям может быть использована система безопасности доменных объектов (Domain Object Security) от Spring. Однако данная система жестко привязана к доменной модели, поддерживает ограниченное количество функций над сущностями (до 32) и достаточно сложна в реализации (7 ключевых интерфейсов).
Нами было решено разработать новый фреймворк CRMedia.Security, который обладал бы большей гибкостью, поддерживал динамические списки доступа, а также был бы более простым в использовании. Идея предлагаемого нами фреймворка заключается в том, что он позволяет защитить некоторые действия пользователя с определенным набором аргументов, смысл которых известен лишь веб-приложению, но не системе безопасности. Такая модель системы позволяет абстрагироваться от сущностей Campus.ru и использовать фреймворк в других проектах.
CRMedia.Security получает действие, после чего его аргументы из приложения ищут ограничения на эти действия через PermissionProvider и вычисляют пересечение этих ограничений со списком доступа текущего пользователя, предоставляемого ACLProvider. Если пересечение является пустым множеством, то вызывается обработчик, соответствующий случаю отказа в доступе. В случае непустого пересечения вызов конкретного события Tapestry продолжается.
Приведем конкретный сценарий, который требует проверки прав: например, просмотр в сообществе с id = 10 статьи под номером 20. В данном случае «просмотр статьи» – это действие, а 10, 20 – его аргументы. Допустим, эта статья доступна только для членов сообщества, а просмотреть ее пытается пользователь, не состоящий в сообществе. Таким образом, PermissionProvider, имплементированный в приложении, должен возвратить ограничение «член сообщества», а ACLProvider – «не состоит в сообществе». Пересечение этих множеств пусто, а, следовательно, доступ к странице поста будет закрыт.
Рассмотрим программную реализацию системы безопасности с использованием фреймворка CRMedia.Security. Вначале необходимо подключить модуль CRMedia.Security к разрабатываемому приложению. Этот модуль подключает обработчик аннотаций в цепочку преобразователя компонентов Tapestry (ComponentClassTransformWorker).
Интерфейс провайдера ограничений (PermissionProvider) имеет следующий вид:
Он может имплементироваться как DAO для хранения ограничений в БД. Здесь Action – это совокупность названия действия и списка именованных аргументов ({view_article; community = 10; article = 20} для приведенного выше примера), а PermissionEntry – это одна запись списка доступа (например, {status = member}).
ACLProvider содержит единственный метод, с помощью которого CRMedia.Security получает список доступа текущего пользователя:
Для нашего примера на действие {view_article; community = 10; article = 20} метод getACL должен возвращать множество {status = nonmember} на основании соотношений между текущим пользователем и указанным сообществом.
Теперь необходимо сообщить ядру безопасности, какие именно события следует защитить. Для этого необходимо расставить аннотации CRMedia.Security.
Пусть пост просматривается через страницу ViewArticle. При обращении к ней по правилам Tapestry вызывается метод onActivate. Чтобы ограничить к нему доступ, нам нужно расставить аннотации таким образом:
Аннотация Restricted ставится перед событием, которое имеет ограничение. Каждое событие соответствует одному действию (в данном случае это «view_article») с определенным набором аргументов («community» и «article»). Аргументы из этого сопровождаются аннотациями SecuredParam.
По умолчанию при отказе в доступе клиент получает ответ от сервера с кодом 403 (Forbidden). В случае необходимости можно использовать свой обработчик:
CRMedia.Security поддерживает вложенные свойства в проверяемых аргументах. Например, в следующем примере параметр «community» берется из свойства community сущности Article:
Если страница сохраняет свое состояние между запросами, то значения аргументов действия для защищенного события («view» в примере ниже) можно брать из самой страницы:
Помимо ограничений на события, можно выставлять ограничение на просмотр частей страницы. Для этого во фреймворке существует компонент IfCan, который полностью аналогичен по использованию в шаблоне штатному компоненту If из Tapestry Core:
Свойство viewArticleContext аналогично аннотациям SecuredProp определяет список защищенных параметров:
Таким образом, разработанный фреймворк обладает достаточной степенью абстракции, позволяет защитить от несанкционированного выполнения различные события на страницах Tapestry и настраивать отображение страницы в зависимости от имеющихся у пользователя прав.
Spring Security работает со статическими ролями, которые определяются через URI при каждом запросе со стороны HTTP-клиента. Но как быть с динамическими ролями, которые меняются в зависимости от отношений между текущим пользователем и запрашиваемым объектом (например, роль автора по отношении к статье)?
Для решения задачи доступа к отдельным сущностям может быть использована система безопасности доменных объектов (Domain Object Security) от Spring. Однако данная система жестко привязана к доменной модели, поддерживает ограниченное количество функций над сущностями (до 32) и достаточно сложна в реализации (7 ключевых интерфейсов).
Нами было решено разработать новый фреймворк CRMedia.Security, который обладал бы большей гибкостью, поддерживал динамические списки доступа, а также был бы более простым в использовании. Идея предлагаемого нами фреймворка заключается в том, что он позволяет защитить некоторые действия пользователя с определенным набором аргументов, смысл которых известен лишь веб-приложению, но не системе безопасности. Такая модель системы позволяет абстрагироваться от сущностей Campus.ru и использовать фреймворк в других проектах.
CRMedia.Security получает действие, после чего его аргументы из приложения ищут ограничения на эти действия через PermissionProvider и вычисляют пересечение этих ограничений со списком доступа текущего пользователя, предоставляемого ACLProvider. Если пересечение является пустым множеством, то вызывается обработчик, соответствующий случаю отказа в доступе. В случае непустого пересечения вызов конкретного события Tapestry продолжается.
Приведем конкретный сценарий, который требует проверки прав: например, просмотр в сообществе с id = 10 статьи под номером 20. В данном случае «просмотр статьи» – это действие, а 10, 20 – его аргументы. Допустим, эта статья доступна только для членов сообщества, а просмотреть ее пытается пользователь, не состоящий в сообществе. Таким образом, PermissionProvider, имплементированный в приложении, должен возвратить ограничение «член сообщества», а ACLProvider – «не состоит в сообществе». Пересечение этих множеств пусто, а, следовательно, доступ к странице поста будет закрыт.
Рассмотрим программную реализацию системы безопасности с использованием фреймворка CRMedia.Security. Вначале необходимо подключить модуль CRMedia.Security к разрабатываемому приложению. Этот модуль подключает обработчик аннотаций в цепочку преобразователя компонентов Tapestry (ComponentClassTransformWorker).
Интерфейс провайдера ограничений (PermissionProvider) имеет следующий вид:
public interface PermissionProvider {
/**
* Наложить ограничение на выполнение действия
* @param action действие
* @param permission ACL
*/
void restrict(Action action, List<PermissionEntry> permission);
/**
* Получить ACL для выполнения указанного действия
* @param action действие
* @return ACL
*/
List<PermissionEntry> get(Action action);
/**
* Снять все ограничения на выполнение указанного действия
* @param action действие
*/
void revoke(Action action);
/**
* Удалить ограничения на все действия хотя бы с одним аргументом из указанного списка
* @param params список именованных аргументов
*/
void revokeReferenced(Map<String, Object> params);
}
* This source code was highlighted with Source Code Highlighter.
Он может имплементироваться как DAO для хранения ограничений в БД. Здесь Action – это совокупность названия действия и списка именованных аргументов ({view_article; community = 10; article = 20} для приведенного выше примера), а PermissionEntry – это одна запись списка доступа (например, {status = member}).
ACLProvider содержит единственный метод, с помощью которого CRMedia.Security получает список доступа текущего пользователя:
public interface ACLProvider {
/**
* Получить список проверяемых значений перед выполнением действия для указанного компонента
* @param component компонент
* @param action действие
* @return список проверяемых значений; null, если доступ запрещен
*/
List<PermissionEntry> getACL(Component component, Action action);
}
* This source code was highlighted with Source Code Highlighter.
Для нашего примера на действие {view_article; community = 10; article = 20} метод getACL должен возвращать множество {status = nonmember} на основании соотношений между текущим пользователем и указанным сообществом.
Теперь необходимо сообщить ядру безопасности, какие именно события следует защитить. Для этого необходимо расставить аннотации CRMedia.Security.
Пусть пост просматривается через страницу ViewArticle. При обращении к ней по правилам Tapestry вызывается метод onActivate. Чтобы ограничить к нему доступ, нам нужно расставить аннотации таким образом:
public class ViewArticle {
@Restricted(action = "view_article")
Object onActivate(@SecuredParam("community") Community community,
@SecuredParam("article") Article article) {
...
}
}
* This source code was highlighted with Source Code Highlighter.
Аннотация Restricted ставится перед событием, которое имеет ограничение. Каждое событие соответствует одному действию (в данном случае это «view_article») с определенным набором аргументов («community» и «article»). Аргументы из этого сопровождаются аннотациями SecuredParam.
По умолчанию при отказе в доступе клиент получает ответ от сервера с кодом 403 (Forbidden). В случае необходимости можно использовать свой обработчик:
public class ViewArticle {
@Restricted(action = "view_article")
void onActivate(@SecuredParam("community") Community community,
@SecuredParam("article") Article article) {
...
}
Object onForbidForListArticles() {
return new TextStreamResponse("text/plain", "access denied");
}
}
* This source code was highlighted with Source Code Highlighter.
CRMedia.Security поддерживает вложенные свойства в проверяемых аргументах. Например, в следующем примере параметр «community» берется из свойства community сущности Article:
public class Article {
public Community getCommunity() {
…
}
}
public class ViewArticle {
@Restricted(
action = "view_article",
params = {
@SecuredProp(name = "community", paramProp = "article.community")
}
)
Object onActivate(@SecuredParam("article") Article article) {
...
}
}
* This source code was highlighted with Source Code Highlighter.
Если страница сохраняет свое состояние между запросами, то значения аргументов действия для защищенного события («view» в примере ниже) можно брать из самой страницы:
public class ViewArticle {
@Property
@Persist
private Article article;
@Restricted(
action = "view_article",
params = {
@SecuredProp(name = "article", pageProp = "article"),
@SecuredProp(name = "community", paramProp = "article.community")
}
)
Object onView() {
...
}
}
* This source code was highlighted with Source Code Highlighter.
Помимо ограничений на события, можно выставлять ограничение на просмотр частей страницы. Для этого во фреймворке существует компонент IfCan, который полностью аналогичен по использованию в шаблоне штатному компоненту If из Tapestry Core:
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<body>
<t:ifCan actionName="view_article" context="viewArticleContext">
<t:eventlink event="view">Открыть пост</t:eventlink>
<t:parameter name="else">
Просмотр запрещен
</t:parameter>
</t:ifCan>
</body>
</html>
* This source code was highlighted with Source Code Highlighter.
Свойство viewArticleContext аналогично аннотациям SecuredProp определяет список защищенных параметров:
public class ViewArticle {
@Property
@Persist
private Article article;
public Object[] getViewArticleContext() {
return new Object[]{"article", article, "community", article.getCommunity()};
}
@Restricted(
action = "view_article",
params = {
@SecuredProp(name = "article", pageProp = "article"),
@SecuredProp(name = "community", paramProp = "article.community")
}
)
Object onView() {
...
}
}
* This source code was highlighted with Source Code Highlighter.
Таким образом, разработанный фреймворк обладает достаточной степенью абстракции, позволяет защитить от несанкционированного выполнения различные события на страницах Tapestry и настраивать отображение страницы в зависимости от имеющихся у пользователя прав.