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

Loading…
Cancel
Save