DEV Community

Semyon Kirekov
Semyon Kirekov

Posted on

Spring Security — Generate Docs for Authorization Rules

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.

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.

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;

    ...
}
Enter fullscreen mode Exit fullscreen mode

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
    1. HTTP method
    2. API path
    3. The security SpEL expression parsed from @PreAuthorize annotation.
    4. 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
) {}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

Enter fullscreen mode Exit fullscreen mode

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

Top comments (5)

Collapse
 
dashaun profile image
DaShaun

Semyon, this is super interesting!

You should definitely take a look at spring.io/projects/spring-restdocs

I like to call it "Test Driven Documentation" because my tests results, get put directly into the documentation. Very similar to what you are doing here.

I imagine there is a way for us to integrate what you are doing, into a Spring RESTdocs workflow too.

Collapse
 
kirekov profile image
Semyon Kirekov

@dashaun Sorry, I misinterpreted my idea a bit. We don't use TestRestTemplate directly. Instead, we generate Java REST API client on OpenAPI specification and call it inside tests. The client itself uses OKHttp3 library behind the scenes

Collapse
 
kirekov profile image
Semyon Kirekov

@dashaun Thank you! I've known about Spring REST Docs. But as far as I understood it only works for MockMvc but not for TestRestTemplate. Though we test all of our REST API with @SpringBootTest(webEnvironment = RANDOM_PART) (i.e. integration testing).

Is there an approach to mix Spring REST Docs with TestRestTemplate?

Collapse
 
dashaun profile image
DaShaun

I don't think TestRestTemplate is an option, but WebTestClient is.

I have some other TODO items, but I plan on looking at this later today!

I think I have a way to combine your output, with my RESTDocs output, in a meaningful way, as a good starting point.

Thread Thread
 
kirekov profile image
Semyon Kirekov

@dashaun Thank you! I'll be glad, if you tell the results of your investigations :)