Spring Security — Generate Docs for Authorization Rules
Tags: java, documentation, programming, tutorial
Published: 2023-03-08 12:15:19

That's a small addition to my [previous post "Spring Security and Non-flat Roles Inheritance Architecture"](https://dev.to/kirekov/spring-security-and-non-flat-roles-inheritance-architecture-2a7b). In this article, I'm telling you:

1. How to generate documentation for Spring Security authorization rules directly from code?
1. How to host the result HTML page on [GitHub Pages](https://pages.github.com/)?

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cftjd71n1kqcv4tmfi6p.jpg)

You can find the whole [generator's code by this link](https://github.com/SimonHarmonicMinor/spring-security-dynamic-roles-inheritance-model-example/blob/master/src/test/java/com/example/demo/controller/DocsTest.java). Look at the example of generated documentation below.

| HTTP method | API path                    | Security checking rule                                                        | Controller function name|
|------|-----------------------------------|--------------------------------------------------------------------------|-----------------|
| POST | /api/community                    | isAuthenticated()                                                        | createCommunity |
| POST | /api/community/{communityId}/post | @RoleService.hasAnyRoleByCommunityId(#communityId, @CommunityRole.ADMIN) | createPost      |
| PUT  | /api/post/{postId}                | @RoleService.hasAnyRoleByPostId(#postId, @PostRole.EDITOR)               | updatePost      |
| GET  | /api/post/{postId}                | @RoleService.hasAnyRoleByPostId(#postId, @PostRole.VIEWER)               | getPost         |

> I put the layout as Markdown table to make it easier to read. Anyway, you can check out [the rendered HTML by this link](https://simonharmonicminor.github.io/spring-security-dynamic-roles-inheritance-model-example/).

## 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](https://pages.github.com/).

## Generation steps

Take a look at the base setup below.

```java
class DocsTest extends AbstractControllerTest {
    @Autowired
    private ApplicationContext context;

    ...
}
```

> The `AbstractControllerTest` starts PostgreSQL with Testcontainers. You can find its source code [by this link](https://github.com/SimonHarmonicMinor/spring-security-dynamic-roles-inheritance-model-example/blob/master/src/test/java/com/example/demo/controller/AbstractControllerTest.java).

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
1. Details about each endpoint
    1. HTTP method
    1. API path
    1. The security SpEL expression parsed from `@PreAuthorize` annotation.
    1. The name of the Java method that maps the HTTP request.

Look at the Java records that hold the stated points:

```java
@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:

```java
@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.
1. Then I get the current controller bean by its name.
1. Afterwards, I parse the base API path.
1. And finally, I traverse each method inside the controller and parse information about it.

Look at the `getMethodInfo` declaration below.

```java
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:

```java
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](https://github.com/SimonHarmonicMinor/spring-security-dynamic-roles-inheritance-model-example/blob/master/src/test/java/com/example/demo/controller/DocsTest.java#L99).

Look at the `toHtml` function definition below.

```java
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.

```yaml
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](https://gradle.org/) build operation.
1. Then comes the `Upload artifact` that saves the directory containing the HTML documentation to the GitHub registry.
1. 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](https://simonharmonicminor.github.io/spring-security-dynamic-roles-inheritance-model-example/). 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"](https://dev.to/kirekov/spring-security-and-non-flat-roles-inheritance-architecture-2a7b)
1. [GitHub Pages](https://pages.github.com/)
1. [The entire generator code](https://github.com/SimonHarmonicMinor/spring-security-dynamic-roles-inheritance-model-example/blob/master/src/test/java/com/example/demo/controller/DocsTest.java)
1. [The rendered HTML page hosted on GitHub Pages](https://simonharmonicminor.github.io/spring-security-dynamic-roles-inheritance-model-example/)
1. [AbstractControllerTest with Testcontainers setup](https://github.com/SimonHarmonicMinor/spring-security-dynamic-roles-inheritance-model-example/blob/master/src/test/java/com/example/demo/controller/AbstractControllerTest.java)
1. [Gradle](https://gradle.org/)