DEV Community

davidcyp
davidcyp

Posted on

Guaranteeing and enforcing your architecture (with ArchUnit)

Guaranteeing and enforcing your architecture (with ArchUnit)

Recently I was working on an application which relied heavily on a variety of data sources. Our application actually combines, interpretes and visualizes the data.

Not everyone is allowed to see all the data. We needed a simple way to protect access to our resources, though not solely based on the typical users and roles. Access to data should be protected on a more granularized level. However, while on the one hand access rules are defined by a set of mutually excluding parameters, we also wanted to keep the code as clean and maintainable as possible.

Since our application already relied on Spring Boot, our choice fell on spring-security and to be more precise it's @PreAuthorize annotation.

We could have chosen to copy/paste/adapt the enclosed SPeL expression, though this would quickly introduce maintenance issues, eg. given we only have 2 distinct parameters which are used to grant access to a piece of data, we would still be copying the SPeL expressions throughout the code, introducing maintenance issues whenever the access rules change.

   @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();
    }
   }
Enter fullscreen mode Exit fullscreen mode

So we created a simple annotation for that

@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)";

}
Enter fullscreen mode Exit fullscreen mode

which we can now apply:

@Component
public class SomeSecuredResource {

    @Sensitivepublic SensitiveData showMe(Foo foo){
        return new SensitiveData();
    }

    @Sensitivepublic SensitiveData showMeToo(Bar bar){
        return new SensitiveData();
    }
}
Enter fullscreen mode Exit fullscreen mode

Fine. Problem solved, one would think...

That's only partially true. Okay, we isolated the logic which grants access to a method given a specific method parameter but we introduced other less visible issues.

Suppose a new developer joins the team. And he sees these @Sensitive annotations protecting access to resources. And he applies them on:

@Component
public class NewDevsClass {

    @Sensitivepublic SensitiveData doSomething(long someParameter){
       return new SensitiveData(); 
    }

    @Sensitivepublic SensitiveData doOtherStuff(String foo){
        return new SensitiveData();
    }
}
Enter fullscreen mode Exit fullscreen mode

He is breaking the implicit rules here. As we have seen, the implementation of our @Sensitive annotation relies on a parameter of type Foo or Bar.

Actually he is breaking our architectural rule, which is:

Methods annotated with the @Sensitive annotation must have a parameter of type Foo or Bar.

So, how do we solve this? Keep an extensive list of rules on the wiki and let everyone sift through it? Configure a Sonar rule and collect the reports every night? No, what about ... embedding those rules in a fast-executing unit test and get immediate feedback?

Please welcome ArchUnit

ArchUnit: A Java architecture test library, to specify and assert architecture rules in plain Java
So let's dig in, and write a test which ensures correct usage of our annotation.

public class SensitiveAnnotationUsageTest {

    DescribedPredicate<JavaClass> haveAFieldAnnotatedWithSensitive =
            new DescribedPredicate<JavaClass>("have a field annotated with @Sensitive"){
                @Overridepublic boolean apply(JavaClass input) {
                    // note : simplified version which inspects all classesreturn true;
                }
            };

    ArchCondition<JavaClass> mustContainAParameterOfTypeFooOrBar =
            new ArchCondition<JavaClass>("must have parameter of type 'com.example.post.Foo' or 'com.example.post.Bar'") {
                @Overridepublic void check(JavaClass item, ConditionEvents events) {
                    List<JavaMethod> collect = item.getMethods().stream()
                            .filter(method -> method.isAnnotatedWith(Sensitive.class)).collect(Collectors.toList());

                    for(JavaMethod method: collect){

                        List<String> 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Executing this test on our 2 classes results in the following result:

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
Enter fullscreen mode Exit fullscreen mode

Et voila! We have a test in place which enforces a correct application of our architectural rules.

Curious to find out more about ArchUnit? Make sure to check out their user guide and examples.

Top comments (0)