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:
- getting the information from the custom security rule
- creating an extension property
- creating an extension which holds a list of that extension property
- creating an operation which holds a list of that extension
- 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.
Top comments (1)
Nice post