Недавно я работал над приложением, которое в значительной степени опиралось на различные источники данных. Наше приложение фактически объединяет, интерпретирует и визуализирует данные.
Не всем разрешено видеть все данные. Нам нужен был простой способ защитить доступ к нашим ресурсам, хотя и не только на основе типичных пользователей и ролей. Доступ к данным должен быть защищен на более детализированном уровне. Однако, хотя, с одной стороны, правила доступа определяются набором взаимоисключающих параметров, мы также хотели сохранить код как можно более чистым и доступным для обслуживания.
Поскольку наше приложение уже полагалось на Spring Boot, наш выбор пал на spring-безопасность, а точнее, на аннотацию @PreAuthorize.
Мы могли бы выбрать копирование/вставку/адаптацию прилагаемого выражения SPeL, хотя это быстро привело бы к проблемам с обслуживанием, например. учитывая, что у нас есть только 2 различных параметра, которые используются для предоставления доступа к части данных, мы все равно будем копировать выражения SPeL по всему коду, создавая проблемы с обслуживанием при изменении правил доступа.
@Component
public class SomeSecuredResource {
@PreAuthorize("hasAccess(#foo)")
public SensitiveData showMe(Foo foo){
return new SensitiveData();
}
@PreAuthorize("isAllowedToSee(#bar)")
public SensitiveData showMeToo(Bar bar){
return new SensitiveData();
}
}
Поэтому мы создали для этого простую аннотацию
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@PreAuthorize(Sensitive.EXPRESSION)
public @interface Sensitive {
String EXPRESSION = "#foo != null ? hasAccess(#foo) : (#bar != null ? isAllowedToSee(#bar) : false)";
}
который мы теперь можем применить:
@Component
public class SomeSecuredResource {
@Sensitivepublic SensitiveData showMe(Foo foo){
return new SensitiveData();
}
@Sensitivepublic SensitiveData showMeToo(Bar bar){
return new SensitiveData();
}
}
Хорошо. Казалось бы, проблема решена…
Это верно лишь отчасти. Хорошо, мы изолировали логику, которая предоставляет доступ к методу с учетом определенного параметра метода, но мы ввели другие менее заметные проблемы.
Предположим, к команде присоединяется новый разработчик. И он видит эти @Конфиденциальные аннотации, защищающие доступ к ресурсам. И он применяет их на:
@Component
public class NewDevsClass {
@Sensitivepublic SensitiveData doSomething(long someParameter){
return new SensitiveData();
}
@Sensitivepublic SensitiveData doOtherStuff(String foo){
return new SensitiveData();
}
}
Он нарушает здесь неявные правила. Как мы видели, реализация нашей аннотации @Sensitive зависит от параметра типа Foo или Bar.
На самом деле он нарушает наше архитектурное правило, которое заключается в:
Методы, аннотированные аннотацией @Sensitive, должны иметь параметр типа Foo или Bar.
Итак, как нам решить эту проблему? Хранить обширный список правил в вики и позволять всем просматривать его? Настроить правило сонара и собирать отчеты каждую ночь? Нет, о чем… внедрять эти правила в быстро выполняемый модульный тест и получать немедленную обратную связь?
Пожалуйста, добро пожаловать Арочный Блок
Модуль Arch: Библиотека тестов архитектуры Java, предназначенная для простого определения и утверждения правил архитектуры Ява Итак, давайте углубимся и напишем тест, который обеспечит правильное использование нашей аннотации.
public class SensitiveAnnotationUsageTest {
DescribedPredicate haveAFieldAnnotatedWithSensitive =
new DescribedPredicate("have a field annotated with @Sensitive"){
@Overridepublic boolean apply(JavaClass input) {
// note : simplified version which inspects all classesreturn true;
}
};
ArchCondition mustContainAParameterOfTypeFooOrBar =
new ArchCondition("must have parameter of type 'com.example.post.Foo' or 'com.example.post.Bar'") {
@Overridepublic void check(JavaClass item, ConditionEvents events) {
List collect = item.getMethods().stream()
.filter(method -> method.isAnnotatedWith(Sensitive.class)).collect(Collectors.toList());
for(JavaMethod method: collect){
List names = method.getParameters().getNames();
if(!names.contains("com.example.post.Foo") && !names.contains("com.example.post.Bar")) {
String message = String.format(
"Method %s bevat geen parameter met type 'Foo' of 'Bar", method.getFullName());
events.add(SimpleConditionEvent.violated(method, message));
}
}
}
};
@Testpublic void checkArchitecturalRules(){
JavaClasses importedClasses = new ClassFileImporter().importPackages("com.example.post");
ArchRule rule = ArchRuleDefinition.classes()
.that(haveAFieldAnnotatedWithSensitive)
.should(mustContainAParameterOfTypeFooOrBar);
rule.check(importedClasses);
}
}
Выполнение этого теста на наших 2 классах приводит к следующему результату:
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that have a field annotated with @Sensitive should must have parameter of type 'com.example.post.Foo' was violated (2 times): Method com.example.post.NewDevsClass.doOtherStuff(java.lang.String) bevat geen parameter met type 'Foo' of 'Bar Method com.example.post.NewDevsClass.doSomething(long) bevat geen parameter met type 'Foo' of 'Bar
И вот! У нас есть тест, который обеспечивает правильное применение наших архитектурных правил.
Любопытно узнать больше о подразделении Arch? Обязательно ознакомьтесь с их руководством пользователя и примерами .
Оригинал: “https://dev.to/davidcyp_52/guaranteeing-and-enforcing-your-architecture-with-archunit–4nf5”