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

в 16:34, , рубрики: AST, bytecode, groovy, java, Программирование, метки: , , ,

Предыстория

Мы разрабатываем небольшой портал на 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.

Ссылки

Автор: jreznot

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js