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.*;
    
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.SOURCE)
    @GroovyASTTransformationClass("com.example.SuperSecuredTransformation")
    public @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;
    
    @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.
    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

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

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