Browse Source

Merge pull request #14445 from AndriiLandiak/api-keys-swagger-usage

Added API keys auth to swagger
pull/14473/head
Viacheslav Klimov 9 months ago
committed by GitHub
parent
commit
438beaed98
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 116
      application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java

116
application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java

@ -86,6 +86,9 @@ public class SwaggerConfiguration {
public static final String LOGIN_ENDPOINT = "/api/auth/login";
public static final String REFRESH_TOKEN_ENDPOINT = "/api/auth/token";
private static final String LOGIN_PASSWORD_SCHEME = "HTTP login form";
private static final String API_KEY_SCHEME = "API key form";
private static final ApiResponses loginResponses = loginResponses();
private static final ApiResponses defaultErrorResponses = defaultErrorResponses(false);
private static final ApiResponses defaultPostErrorResponses = defaultErrorResponses(true);
@ -142,14 +145,28 @@ public class SwaggerConfiguration {
.license(license)
.version(apiVersion);
SecurityScheme securityScheme = new SecurityScheme()
SecurityScheme loginPasswordScheme = new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.description("Enter Username / Password")
.scheme("loginPassword")
.bearerFormat("/api/auth/login|X-Authorization");
SecurityScheme apiKeyScheme = new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.name("X-Authorization")
.in(SecurityScheme.In.HEADER)
.description("""
Enter the API key value with 'ApiKey' prefix in format: **ApiKey <your_api_key_value>**
Example: **ApiKey tb_5te51SkLRYpjGrujUGwqkjFvooWBlQpVe2An2Dr3w13wjfxDW**
<br>**NOTE**: Use only ONE authentication method at a time. If both are authorized, JWT auth takes the priority.<br>
""");
var openApi = new OpenAPI()
.components(new Components().addSecuritySchemes("HTTP login form", securityScheme))
.components(new Components()
.addSecuritySchemes(LOGIN_PASSWORD_SCHEME, loginPasswordScheme)
.addSecuritySchemes(API_KEY_SCHEME, apiKeyScheme))
.info(info);
addDefaultSchemas(openApi);
addLoginOperation(openApi);
@ -198,13 +215,14 @@ public class SwaggerConfiguration {
operation.summary("Login method to get user JWT token data");
operation.description("""
Login method used to authenticate user and get JWT token data.
Value of the response **token** field can be used as **X-Authorization** header value:
`X-Authorization: Bearer $JWT_TOKEN_VALUE`.""");
var requestBody = new RequestBody().description("Login request")
.content(new Content().addMediaType(APPLICATION_JSON_VALUE,
new MediaType().schema(new Schema<LoginRequest>().$ref("#/components/schemas/LoginRequest"))));
new MediaType().schema(new Schema<LoginRequest>().$ref("#/components/schemas/LoginRequest"))));
operation.requestBody(requestBody);
operation.responses(loginResponses);
@ -218,11 +236,11 @@ public class SwaggerConfiguration {
var operation = new Operation();
operation.summary("Refresh user JWT token data");
operation.description("""
Method to refresh JWT token. Provide a valid refresh token to get a new JWT token.
The response contains a new token that can be used for authorization.
`X-Authorization: Bearer $JWT_TOKEN_VALUE`""");
Method to refresh JWT token. Provide a valid refresh token to get a new JWT token.
The response contains a new token that can be used for authorization.
`X-Authorization: Bearer $JWT_TOKEN_VALUE`""");
var requestBody = new RequestBody().description("Refresh token request")
.content(new Content().addMediaType(APPLICATION_JSON_VALUE,
@ -291,8 +309,9 @@ public class SwaggerConfiguration {
return (routerOperation, handlerMethod) -> {
String[] pNames = localSpringDocParameterNameDiscoverer.getParameterNames(handlerMethod.getMethod());
String[] reflectionParametersNames = Arrays.stream(handlerMethod.getMethod().getParameters()).map(java.lang.reflect.Parameter::getName).toArray(String[]::new);
if (pNames == null || Arrays.stream(pNames).anyMatch(Objects::isNull))
if (pNames == null || Arrays.stream(pNames).anyMatch(Objects::isNull)) {
pNames = reflectionParametersNames;
}
MethodParameter[] parameters = handlerMethod.getMethodParameters();
List<String> requestParams = new ArrayList<>();
for (var i = 0; i < parameters.length; i++) {
@ -324,26 +343,25 @@ public class SwaggerConfiguration {
}
private OpenApiCustomizer customOpenApiCustomizer() {
var loginForm = new SecurityRequirement().addList("HTTP login form", Arrays.asList(
Authority.SYS_ADMIN.name(),
Authority.TENANT_ADMIN.name(),
Authority.CUSTOMER_USER.name()
));
var loginRequirement = createSecurityRequirement(LOGIN_PASSWORD_SCHEME);
var apiKeyRequirement = createSecurityRequirement(API_KEY_SCHEME);
return openAPI -> {
var paths = openAPI.getPaths();
paths.entrySet().stream().peek(entry -> {
securityCustomization(loginForm, entry);
if (!entry.getKey().equals(LOGIN_ENDPOINT)) {
defaultErrorResponsesCustomization(entry.getValue());
}
}).map(this::tagsCustomization).filter(Objects::nonNull).distinct().sorted(Comparator.comparing(Tag::getName)).forEach(openAPI::addTagsItem);
paths.entrySet().stream()
.peek(entry -> {
securityCustomization(entry, loginRequirement, apiKeyRequirement);
if (!entry.getKey().equals(LOGIN_ENDPOINT)) {
defaultErrorResponsesCustomization(entry.getValue());
}
})
.map(this::extractTagFromPath).filter(Objects::nonNull).distinct().sorted(Comparator.comparing(Tag::getName)).forEach(openAPI::addTagsItem);
var pathItemsByTags = new TreeMap<String, Map<String, PathItem>>();
paths.forEach((k, v) -> {
var tagItem = tagItemFromPathItem(v);
if (tagItem != null) {
var pathItemMap = pathItemsByTags.computeIfAbsent(tagItem, k1 -> new TreeMap<>());
pathItemMap.put(k, v);
pathItemsByTags.computeIfAbsent(tagItem, k1 -> new TreeMap<>()).put(k, v);
}
});
var sortedPaths = new Paths();
@ -357,13 +375,17 @@ public class SwaggerConfiguration {
};
}
private SecurityRequirement createSecurityRequirement(String schemeName) {
return new SecurityRequirement().addList(schemeName, Arrays.asList(
Authority.SYS_ADMIN.name(),
Authority.TENANT_ADMIN.name(),
Authority.CUSTOMER_USER.name()
));
}
private Tag tagsCustomization(Map.Entry<String, PathItem> entry) {
var tagItem = tagItemFromPathItem(entry.getValue());
if (tagItem != null) {
return tagFromTagItem(tagItem);
}
return null;
private Tag extractTagFromPath(Map.Entry<String, PathItem> entry) {
var tagName = tagItemFromPathItem(entry.getValue());
return tagName != null ? tagFromTagItem(tagName) : null;
}
private String tagItemFromPathItem(PathItem item) {
@ -383,17 +405,20 @@ public class SwaggerConfiguration {
StringBuilder sb = new StringBuilder();
for (String word : words) {
sb.append(word.substring(0, 1).toUpperCase());
sb.append(word.substring(1).toLowerCase());
sb.append(" ");
if (!word.isEmpty()) {
sb.append(word.substring(0, 1).toUpperCase());
sb.append(word.substring(1).toLowerCase());
sb.append(" ");
}
}
return new Tag().name(tagItem).description(sb.toString().trim());
}
private void defaultErrorResponsesCustomization(PathItem pathItem) {
pathItem.readOperationsMap().forEach(((httpMethod, operation) -> {
pathItem.readOperationsMap().forEach((httpMethod, operation) -> {
var errorResponses = httpMethod.equals(PathItem.HttpMethod.POST) ? defaultPostErrorResponses : defaultErrorResponses;
var responses = operation.getResponses();
if (responses == null) {
responses = errorResponses;
@ -406,16 +431,19 @@ public class SwaggerConfiguration {
});
}
operation.setResponses(responses);
}));
});
}
private void securityCustomization(SecurityRequirement loginForm, Map.Entry<String, PathItem> entry) {
private void securityCustomization(Map.Entry<String, PathItem> entry, SecurityRequirement jwtBearerRequirement, SecurityRequirement apiKeyRequirement) {
var path = entry.getKey();
if (path.matches(securityPathRegex) && !path.matches(nonSecurityPathRegex) && !path.equals(LOGIN_ENDPOINT)) {
if (path.matches(securityPathRegex) && !path.matches(nonSecurityPathRegex) && !path.equals(LOGIN_ENDPOINT) && !path.equals(REFRESH_TOKEN_ENDPOINT)) {
entry.getValue()
.readOperationsMap()
.values()
.forEach(operation -> operation.addSecurityItem(loginForm));
.forEach(operation -> {
operation.addSecurityItem(jwtBearerRequirement);
operation.addSecurityItem(apiKeyRequirement);
});
}
}
@ -430,6 +458,7 @@ public class SwaggerConfiguration {
private static ApiResponses defaultErrorResponses(boolean isPost) {
ApiResponses apiResponses = new ApiResponses();
apiResponses.addApiResponse("400", errorResponse("400", "Bad Request",
ThingsboardErrorResponse.of(isPost ? "Invalid request body" : "Invalid UUID string: 123", ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST)));
@ -465,8 +494,7 @@ public class SwaggerConfiguration {
ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED))
)
));
var credentialsExpiredSchema = new Schema<ThingsboardCredentialsExpiredResponse>();
credentialsExpiredSchema.$ref("#/components/schemas/ThingsboardCredentialsExpiredResponse");
var credentialsExpiredSchema = new Schema<ThingsboardCredentialsExpiredResponse>().$ref("#/components/schemas/ThingsboardCredentialsExpiredResponse");
apiResponses.addApiResponse("401 ", errorResponse("Unauthorized (**Expired credentials**)",
Map.of(
"credentials-expired", errorExample("Expired credentials",
@ -482,15 +510,13 @@ public class SwaggerConfiguration {
}
private static ApiResponse errorResponse(String description, Map<String, Example> examples) {
var schema = new Schema<ThingsboardErrorResponse>();
schema.$ref("#/components/schemas/ThingsboardErrorResponse");
var schema = new Schema<ThingsboardErrorResponse>().$ref("#/components/schemas/ThingsboardErrorResponse");
return errorResponse(description, examples, schema);
}
private static ApiResponse errorResponse(String description, Map<String, Example> examples, Schema<? extends ThingsboardErrorResponse> errorResponseSchema) {
MediaType mediaType = new MediaType().schema(errorResponseSchema);
mediaType.setExamples(examples);
Content content = new Content().addMediaType(org.springframework.http.MediaType.APPLICATION_JSON_VALUE, mediaType);
MediaType mediaType = new MediaType().schema(errorResponseSchema).examples(examples);
Content content = new Content().addMediaType(org.springframework.http.MediaType.APPLICATION_JSON_VALUE, mediaType);
return new ApiResponse().description(description).content(content);
}

Loading…
Cancel
Save