Spring Security — Generate Docs for Authorization Rules

Semyon Kirekov
Javarevisited
Published in
5 min readMar 10, 2023

That’s a small addition to my previous post “Spring Security and Non-flat Roles Inheritance Architecture”. In this article, I’m telling you:

  1. How to generate documentation for Spring Security authorization rules directly from code?
  2. How to host the result HTML page on GitHub Pages?

Such documentation is useful for a variety of specialists. System and business analysts want to understand the logic behind request processing. While quality engineers check that endpoints validate the access as described in the task. Both of these categories will benefit from the documentation that is always relevant.

Meme cover

You can find the whole generator’s code by this link. Look at the example of generated documentation below.

Generated docs example

You can check out the rendered HTML by this link.

The algorithm

Here is the whole idea of documentation generation:

  1. There is a separate DocsTest that starts the whole Spring application.
  2. While running the test generates the result HTML page into build/classes/test directory.
  3. Finally, during the GitHub pipeline execution, we host the output HTML page on GitHub Pages.

Generation steps

Take a look at the base setup below.

class DocsTest extends AbstractControllerTest {
@Autowired
private ApplicationContext context;
...
}

The AbstractControllerTest starts PostgreSQL with Testcontainers. You can find its source code by this link.

I use the ApplicationContext bean to resolve registered REST controllers.

What information do we need to parse from the annotations put on a REST controller? Here is the list:

  1. The name of the controller
  2. Details about each endpoint:
  3. HTTP method
  4. API path
  5. The security SpEL expression parsed from @PreAuthorize annotation.
  6. The name of the Java method that maps the HTTP request.

Look at the Java records that hold the stated points:

@With
private record ControllerInfo(
String name,
List<MethodInfo> methods
) {}

@With
private record MethodInfo(
String httpMethod,
String apiPath,
String security,
String functionName
) {}

Now it’s time for traversing the existing controllers and parsing the required data. Look at the code snippet below:

@Test
void generateDocs() throws Exception {
final var controllers = new ArrayList<ControllerInfo>();
for (String controllerName : context.getBeanNamesForAnnotation(RestController.class)) {
final var controllerBean = context.getBean(controllerName);
final var baseApiPath = getApiPath(AnnotationUtils.findAnnotation(controllerBean.getClass(), RequestMapping.class));
final var controllerSecurityInfo = new ControllerInfo(
StringUtils.capitalize(controllerName),
new ArrayList<>()
);
for (Method method : controllerBean.getClass().getMethods()) {
getMethodInfo(method)
.map(m -> m.withPrefixedApiPath(baseApiPath))
.ifPresent(m -> controllerSecurityInfo.methods().add(m));
}
controllers.add(controllerSecurityInfo);
}
...
}

Here is what happens step by step:

  1. I retrieve all bean names that marked with @RestController annotation.
  2. Then I get the current controller bean by its name.
  3. Afterwards, I parse the base API path.
  4. And finally, I traverse each method inside the controller and parse information about it.

Look at the getMethodInfo declaration below.

private static Optional<MethodInfo> getMethodInfo(Method method) {
return Optional.<Annotation>ofNullable(AnnotationUtils.findAnnotation(method, GetMapping.class))
.or(() -> ofNullable(AnnotationUtils.findAnnotation(method, PostMapping.class)))
.or(() -> ofNullable(AnnotationUtils.findAnnotation(method, DeleteMapping.class)))
.or(() -> ofNullable(AnnotationUtils.findAnnotation(method, PutMapping.class)))
.map(annotation -> AnnotationUtils.getAnnotationAttributes(method, annotation))
.map(attributes -> new MethodInfo(
attributes.annotationType()
.getSimpleName()
.replace("Mapping", "")
.toUpperCase(),
getApiPath(attributes.getStringArray("value")),
ofNullable(AnnotationUtils.findAnnotation(method, PreAuthorize.class))
.map(PreAuthorize::value)
.orElse(""),
method.getName()
));
}

In that case, I'm trying to obtain possible request mapping annotations from the method: GetMapping, PostMapping, DeleteMapping, or PutMapping. Then I get the annotation's attributes by calling AnnotationUtils.getAnnotationAttributes, and finally pass the parameters to the MethodInfo constructor.

The getApiPath method accepts String... parameter and returns its first value if it's present.

Creating HTML report

Now that we have the information about endpoints, it’s time to format it as the HTML page. Look at the template declaration below:

final var html = """
<html>
<head>
<meta charset="UTF8">
<style>
body, table {
font-family: "JetBrains Mono";
font-size: 20px;
}
table, th, td {
border: 1px solid black;
}
</style>
<link href='https://fonts.googleapis.com/css?family=JetBrains Mono' rel='stylesheet'>
</head>
<body>
<div>
<h2>Endpoints role checking</h2>
<div>{docs}</div>
</div>
</body>
</html>
""".replace("{docs}", toHtml(controllers));

writeFileToBuildFolder("index.html", html);

The controllers variable represents the List<ControllerInfo> that we built previously. The function toHtml transforms it into an HTML snippet. Then we replace the placeholder of {docs} with the content.

The writeFileToBuildFolder function writes the result content into file build/classes/java/test/index.html. You can find its declaration by this link.

Look at the toHtml function definition below.

private static String toHtml(List<ControllerInfo> controllers) {
StringBuilder docs = new StringBuilder();
for (ControllerInfo controller : controllers) {
docs.append("<b>")
.append(controller.name())
.append("</b>")
.append("<br>")
.append("<table>");

for (MethodInfo method : controller.methods()) {
docs.append("<tr>")
.append("<td>").append(method.httpMethod()).append("</td>")
.append("<td>").append(method.apiPath()).append("</td>")
.append("<td>").append(method.security()).append("</td>")
.append("<td>").append(method.functionName()).append("</td>")
.append("</tr>");
}
docs.append("</table>")
.append("----------------------------------<br>");
}
return docs.toString();
}

As you can see, I just create an HTML table for each existing controller and concatenate them into a single string.

Hosting the documentation on GitHub Pages

The whole GitHub Actions pipeline is less than 40 rows. Look at the YAML below.

name: Java CI with Gradle

on:
push:
branches: [ "master" ]

permissions:
contents: read
pages: write
id-token: write

jobs:
build-and-deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Build with Gradle
run: ./gradlew build
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: build/classes/java/test/
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1

Here is what happens:

  1. The Set up JDK 17 and Build with Gradle performs a regular Gradle build operation.
  2. Then comes the Upload artifact that saves the directory containing the HTML documentation to the GitHub registry.
  3. Finally, we deploy the previously stored artifact to the GitHub Pages.

And that’s basically it. You can check out the generated HTML page by this link. The coolest thing is that you don’t have to write documentation manually. Therefore, it’s always relevant because you generate the content directly from your code.

Conclusion

That’s all I wanted to tell you about documenting Spring Security applications and storing the HTML result on GitHub Pages. Do you generate any docs in your projects? If so, what kind of documentation it is? Tell your story in the comments. Thanks for reading!

Resources

  1. My previous post "Spring Security and Non-flat Roles Inheritance Architecture"
  2. GitHub Pages
  3. The entire generator code
  4. The rendered HTML page hosted on GitHub Pages
  5. AbstractControllerTest with Testcontainers setup
  6. Gradle

--

--

Semyon Kirekov
Javarevisited

Java Dev and Team Lead. Passionate about clean code, tea, pastila, and smooth jazz/blues. semyon@kirekov.com