17 июня 2012 в 20:34

Groovy и трансформации AST на службе безопасности приложения

Предыстория


Мы разрабатываем небольшой портал на Grails и используем Spring Security для управления безопасностью. Плагин spring-security для Grails достаточно удобен и до последнего момента от него не требовалось сложной функциональности.

Недавно был обнаружен неприятный момент в использовании аннотаций @Secured для методов контроллеров Grails. Проблема заключается в том, что аннотации обрабатываются во время исполнения и преобразуются в набор правил для адресов «Адрес -> Набор требуемых ролей». Такой подход порождает ряд проблем в Grails-контроллерах в действиях сохранения/удаления данных, поскольку они отправляют данные на основной URL контроллера, то приходиться во-первых аннотировать контроллер, во вторых — невозможно задать различные ограничения для таких запросов.

Речь пойдёт о том, как решить проблему и приобрести хороший инструмент для управления правилами безопасности.

Возможные решения


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

Альтернативные решения:
  • AOP для контроллеров (сложно конфигурировать большое количество правил)
  • Генерация байт-кода по аннотациям во время компиляции

Второй подход неприятно реализовывать в Java, поскольку требуется организовывать этапы сборки. Но только не в Groovy. В Groovy для таких целей принято использовать Мета-программирование или Трансформации AST (Abastract Syntax Tree).

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

Трансформации


Трансформации широко применяются в Grails, например для добавления полей id и version в классы модели. Мы будем использовать их для фильтрации обращений к методам контроллеров.

Трансформация представляет собой простой Java класс реализующий интерфейс ASTTransformation и аннотированный @GroovyASTTransformation, который содержит всего один метод visit — и по сути является типичным представителем паттерна Посетитель. Для трансформации можно задать фазу компиляции на которой она применяется. А для выбора узлов, поступающих посетителю необходим класс аннотации, аннотированный GroovyASTTransformationClass. В итоге трансформация изменяет синтаксическое дерево, может добавлять/изменять/удалять узлы, влияя на получающийся байт-код.

Итого нам потребуется некоторая аннотация и класс транформации. Для простоты назовём их @SuperSecured и SuperSecuredTransformation.

Аннотация содержит в значении массив строк — необходимых ролей для доступа к методу.
@Retention(RetentionPolicy.SOURCE) — указывает на то, что в итоговом байт-коде аннотация будет отсутствовать.
package com.example;

import org.codehaus.groovy.transform.GroovyASTTransformationClass;
import java.lang.annotation.*;

<hh user=Target>({ElementType.METHOD})
<hh user=Retention>(RetentionPolicy.SOURCE)
<hh user=GroovyASTTransformationClass>("com.example.SuperSecuredTransformation")
public <hh user=interface> SuperSecured {

    String[] value() default {};
}


Трансформация:
package com.example;

import org.codehaus.groovy.ast.*;
import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.stmt.*;
import org.codehaus.groovy.control.*;
import org.codehaus.groovy.transform.*;
import java.util.List;

<hh user=GroovyASTTransformation>(phase = CompilePhase.SEMANTIC_ANALYSIS)
public class SuperSecuredTransformation implements ASTTransformation {

    @Override
    public void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {
        if (astNodes != null) {
            for (ASTNode node : astNodes) {
                if (node instanceof MethodNode) {
                    MethodNode methodNode = (MethodNode) node;

                    List<AnnotationNode> annotations = methodNode.getAnnotations(new ClassNode(SuperSecured.class));

                    if (annotations != null && !annotations.isEmpty()) {
                        injectRolesCheck(methodNode, annotations);
                    }
                }
            }
        }
    }

    private void injectRolesCheck(MethodNode method, List<AnnotationNode> annotations) {
        for (AnnotationNode annotationNode : annotations) {
            BlockStatement code = (BlockStatement) method.getCode();

            Expression rolesValue = annotationNode.getMember("value");

            Expression checkRolesExpression = new StaticMethodCallExpression(
                    new ClassNode(SuperSecuredInspector.class),
                    "rejectByRoles",
                    new ArgumentListExpression(
                            rolesValue
                    )
            );

            code.getStatements().add(0, new ExpressionStatement(checkRolesExpression));
        }
    }
}

Получилось следующее: трансформация принимает на вход узлы синтаксического дерева, аннотированные как @SuperSecured и, если это метод, добавляет в начало вызов статического метода SuperSecuredInspector.rejectByRoles со списком ролей в значении аннотации. Этот метод выбрасывает исключение AccessDeniedException, если текущий пользователь не удовлетворяет условиям безопасности.

Пользоваться такими аннотациями в итоге — одно удовольствие.

Заключение


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

Трансформации — достойная альтернатива AOP в Groovy.

Ссылки




P.S. Хабра-девелоперы, почините пожалуйста отображения знака @ в теге source.
+6
3
jreznot 1,5

комментарии (4)

0
splix, #
Раз 10 уже прочитал первый раздел, но так и не понял предысторию, и чем отличается существующее решение от предлагаемого. Может приведете конкретный пример?
0
jreznot, #
В первом случае формируется набор правил для адресов. Во втором — в код методов добавляется проверка.
Проблема правил в том, что формы Grails по умолчанию отправляют POST данные на URL контроллера, то есть работает только правило для контроллера.
И ещё важный момент: если используется переопределение адресов контроллеров, то правила Secured не работают совсем.
0
splix, #
Опять ничего не понял, честно говоря.
Проблема в том что правила работают для контроллера, но вам надо чтобы они работали гдето еще (в сервисе видимо?). Но разве запрос пройдет както мимо контроллера напрямую в сервис?
0
jreznot, #
Правила просто не фильтруют пользователей по ролям. Просто не работают.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

HabraPack возвращается
Захабренный договор на разработку сайта, дизайна, софта. Версия 1.1
Новый троян с валидной цифровой подписью LLC Mail.Ru маскируется под обновления популярных программ