That's a small addition to my previous post "Spring Security and Non-flat Roles Inheritance Architecture". In this article, I'm telling you:
- How to generate documentation for Spring Security authorization rules directly from code?
- 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.
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:
- There is a separate
DocsTest
that starts the whole Spring application. - While running the test generates the result HTML page into
build/classes/test
directory. - 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:
- The name of the controller
- Details about each endpoint
- HTTP method
- API path
- The security SpEL expression parsed from
@PreAuthorize
annotation. - 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:
- I retrieve all bean names that marked with
@RestController
annotation. - Then I get the current controller bean by its name.
- Afterwards, I parse the base API path.
- 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 acceptsString...
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 filebuild/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:
- The
Set up JDK 17
andBuild with Gradle
performs a regular Gradle build operation. - Then comes the
Upload artifact
that saves the directory containing the HTML documentation to the GitHub registry. - 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!
Top comments (5)
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.
@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@dashaun Thank you! I've known about Spring REST Docs. But as far as I understood it only works for
MockMvc
but not forTestRestTemplate
. 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
?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.
@dashaun Thank you! I'll be glad, if you tell the results of your investigations :)