loading...

Using Micronaut Annotation Mapping to Automatically Add Security Info to Swagger Docs

philhardwick profile image Phil Hardwick Originally published at blog.wick.technology on ・4 min read

In my last post I introduced how to create a custom security rule in Mirconaut which allows you to secure an endpoint by requiring a particular permission for a resource ID. In Mettle, we wanted to automatically add some documentation to Swagger based on the custom security annotation so any person consuming the API documentation would know what permissions are necessary. This is a good example of documentation generated from code, so that it is always correct and up to date.

The OpenAPI Specification allows you to add extra information via “Specification Extensions”. In Java annotations these are specified like so:

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;

@Operation(
            extensions = {
                    @Extension(name = "extensionName",
                    properties = {
                            @ExtensionProperty(name = "name", value = "value")
                    })
            }
    )

To add these annotations automatically however, my colleague Nejc and I, used an annotation mapper to hook into the Micronaut compilation process. This is how we did it:

Dependencies

Start a new gradle project and add to your build.gradle:

implementation platform("io.micronaut:micronaut-bom:$micronautVersion")
implementation "io.micronaut:micronaut-inject"
implementation "io.micronaut:micronaut-runtime"
implementation("io.swagger.core.v3:swagger-annotations") 
implementation project(":custom-security-rule-lib")

This will add the necessary Micronaut and Swagger annotations to your classpath.

Annotation Mapper

Extend the TypedAnnotationMapper<RequiredPermission> interface and implement like:

import custom.security.rule.RequiredPermission;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.inject.annotation.TypedAnnotationMapper;
import io.micronaut.inject.visitor.VisitorContext;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;

import java.util.Collections;
import java.util.List;

public class OpenApiAnnotationMapper implements TypedAnnotationMapper<RequiredPermission> {
    @Override
    public Class<RequiredPermission> annotationType() {
        return RequiredPermission.class;
    }

    @Override
    public List<AnnotationValue<?>> map(AnnotationValue<RequiredPermission> annotation, 
            VisitorContext visitorContext) {
        String resourceIdName = annotation.stringValue("resourceIdName").orElse(null);
        String permission = annotation.stringValue("permission").orElse(null);

        AnnotationValue<ExtensionProperty> extensionProp = AnnotationValue.builder(ExtensionProperty.class)
                .member("name", "AuthorisationDescription")
                .member("value", "Your JWT needs to have " + permission + " permissions for " + resourceIdName)
                .build();

        AnnotationValue<Extension> extension = AnnotationValue.builder(Extension.class)
                .member("name", "Security")
                .member("properties", new AnnotationValue[]{extensionProp})
                .build();

        AnnotationValue<Operation> operation = AnnotationValue.builder(Operation.class)
                .member("extensions", new AnnotationValue[]{extension})
                .build();
        return List.of(operation);
    }
}

Here I’ve decided to use TypedAnnotationMapper and have the jar which provides the custom security rule annotation on the annotation processor classpath, but it’s also possible to use NamedAnnotationMapper. NamedAnnotationMapper allows you to map the annotation without having it on the classpath - this is useful if you want to keep you annotation processor classpath small, as suggested in the Micronaut docs.

Implementing the mapping is a case of:

  1. getting the information from the custom security rule
  2. creating an extension property
  3. creating an extension which holds a list of that extension property
  4. creating an operation which holds a list of that extension
  5. return that operation

Micronaut won’t remove the annotation you’re mapping, so your endpoint will still be secured by the security rule. It also won’t remove any existing Operation annotations, it will merge the new Operation annotation with the existing one.

Making the mapper discoverable

To make the mapper discoverable via the java service loader create a file in the resources folder called META-INF/services/io.micronaut.inject.annotation.AnnotationMapper and put the fully qualified name of the mapper inside:

custom.security.rule.openapi.OpenApiAnnotationMapper

Using your mapper in another project

Add the jar as an annotation processor, for example, in gradle add the following to your dependencies:

//to generate the openapi docsc
annotationProcessor("io.micronaut.configuration:micronaut-openapi:1.5.1")
//This shows using the jar from a multi-module project 
//if you're uploading this to a maven repo use the artefact coordinates
annotationProcessor project(":custom-security-rule-openapi") 

What gets generated

When the above class and service descriptor is added to the annotation processor classpath, the annotation mapper will run before the Micronaut openapi annotation processor. This means the OpenAPI processor will parse the mapped @Extension annotation and generate the following:

openapi: 3.0.1
info:
  title: Example API
  version: v1
paths:
  /tenant/{tenantId}:
    get:
      operationId: index
      parameters:
      - name: tenantId
        in: path
        required: true
        schema:
          type: string
      responses:
        default:
          description: index default response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HttpStatus'
      x-Security:
        AuthorisationDescription: Your JWT needs to have READ_ONLY permissions for tenantId

Notice the x-Security key which holds the info specified in our AnnotationMapper.

Conclusion

Micronaut’s compilation process is really powerful and allows a lot of custom functionality to be created - this is just scratching the surface. Another way to do the above is to use a TypeElementVisitor to modify the @Operation annotation when a @RequiredPermission is present. I’ll aim put a blog post up about that to follow this one.

The code from above, including unit tests, is all in a public github repo: https://github.com/PhilHardwick/micronaut-custom-security-rule.

Discussion

pic
Editor guide