@ -18,27 +18,60 @@ package org.thingsboard.server.config;
import com.fasterxml.classmate.ResolvedType ;
import com.fasterxml.classmate.TypeResolver ;
import com.fasterxml.jackson.databind.JsonNode ;
import com.google.common.base.Predicate ;
import org.springframework.beans.factory.annotation.Autowired ;
import org.springframework.beans.factory.annotation.Value ;
import org.springframework.context.annotation.Bean ;
import org.springframework.context.annotation.Configuration ;
import org.springframework.core.annotation.Order ;
import org.springframework.http.HttpMethod ;
import org.springframework.http.MediaType ;
import org.thingsboard.server.common.data.security.Authority ;
import org.thingsboard.server.exception.ThingsboardErrorResponse ;
import org.thingsboard.server.service.security.auth.rest.LoginRequest ;
import org.thingsboard.server.service.security.auth.rest.LoginResponse ;
import springfox.documentation.builders.ApiInfoBuilder ;
import springfox.documentation.schema.AlternateTypeRule ;
import springfox.documentation.builders.OperationBuilder ;
import springfox.documentation.builders.RepresentationBuilder ;
import springfox.documentation.builders.RequestParameterBuilder ;
import springfox.documentation.builders.ResponseBuilder ;
import springfox.documentation.service.ApiDescription ;
import springfox.documentation.service.ApiInfo ;
import springfox.documentation.service.ApiKey ;
import springfox.documentation.service.ApiListing ;
import springfox.documentation.service.AuthorizationScope ;
import springfox.documentation.service.Contact ;
import springfox.documentation.service.HttpLoginPasswordScheme ;
import springfox.documentation.service.ParameterType ;
import springfox.documentation.service.Response ;
import springfox.documentation.service.SecurityReference ;
import springfox.documentation.service.SecurityScheme ;
import springfox.documentation.service.Tag ;
import springfox.documentation.spi.DocumentationType ;
import springfox.documentation.spi.service.ApiListingBuilderPlugin ;
import springfox.documentation.spi.service.ApiListingScannerPlugin ;
import springfox.documentation.spi.service.contexts.ApiListingContext ;
import springfox.documentation.spi.service.contexts.DocumentationContext ;
import springfox.documentation.spi.service.contexts.OperationContext ;
import springfox.documentation.spi.service.contexts.SecurityContext ;
import springfox.documentation.spring.web.plugins.Docket ;
import springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator ;
import springfox.documentation.swagger.common.SwaggerPluginSupport ;
import springfox.documentation.swagger.web.DocExpansion ;
import springfox.documentation.swagger.web.ModelRendering ;
import springfox.documentation.swagger.web.OperationsSorter ;
import springfox.documentation.swagger.web.UiConfiguration ;
import springfox.documentation.swagger.web.UiConfigurationBuilder ;
import java.util.ArrayList ;
import java.util.Collection ;
import java.util.Collections ;
import java.util.List ;
import java.util.Set ;
import java.util.function.Consumer ;
import java.util.function.Predicate ;
import static com.google.common.base.Predicates.and ;
import static com.google.common.base.Predicates.not ;
import static com.google.common.collect.Lists.newArrayList ;
import static java.util.function.Predicate.not ;
import static springfox.documentation.builders.PathSelectors.any ;
import static springfox.documentation.builders.PathSelectors.regex ;
@Configuration
@ -67,6 +100,9 @@ public class SwaggerConfiguration {
@Value ( "${swagger.version}" )
private String version ;
@Autowired
private CachingOperationNameGenerator operationNames ;
@Bean
public Docket thingsboardApi ( ) {
TypeResolver typeResolver = new TypeResolver ( ) ;
@ -77,29 +113,110 @@ public class SwaggerConfiguration {
typeResolver . resolve (
String . class ) ;
return new Docket ( DocumentationType . SWAGGER_2 )
return new Docket ( DocumentationType . OAS_30 )
. groupName ( "thingsboard" )
. apiInfo ( apiInfo ( ) )
. alternateTypeRules (
. additionalModels (
typeResolver . resolve ( ThingsboardErrorResponse . class ) ,
typeResolver . resolve ( LoginRequest . class ) ,
typeResolver . resolve ( LoginResponse . class )
)
/ * . alternateTypeRules (
new AlternateTypeRule (
jsonNodeType ,
stringType ) )
stringType ) ) * /
. select ( )
. paths ( apiPaths ( ) )
. paths ( any ( ) )
. build ( )
. securitySchemes ( newArrayList ( jwtTokenKey ( ) ) )
. globalResponses ( HttpMethod . GET ,
List . of (
new ResponseBuilder ( )
. code ( "401" )
. description ( "Unauthorized" )
. representation ( MediaType . APPLICATION_JSON )
. apply ( classRepresentation ( ThingsboardErrorResponse . class , true ) )
. build ( )
)
)
. securitySchemes ( newArrayList ( httpLogin ( ) ) )
. securityContexts ( newArrayList ( securityContext ( ) ) )
. enableUrlTemplating ( true ) ;
}
private ApiKey jwtTokenKey ( ) {
return new ApiKey ( "X-Authorization" , "JWT token" , "header" ) ;
@Bean
@Order ( SwaggerPluginSupport . SWAGGER_PLUGIN_ORDER )
ApiListingScannerPlugin loginEndpointListingScanner ( ) {
return new ApiListingScannerPlugin ( ) {
@Override
public List < ApiDescription > apply ( DocumentationContext context ) {
return List . of ( loginEndpointApiDescription ( ) ) ;
}
@Override
public boolean supports ( DocumentationType delimiter ) {
return DocumentationType . SWAGGER_2 . equals ( delimiter ) | | DocumentationType . OAS_30 . equals ( delimiter ) ;
}
} ;
}
@Bean
@Order ( SwaggerPluginSupport . SWAGGER_PLUGIN_ORDER )
ApiListingBuilderPlugin loginEndpointListingBuilder ( ) {
return new ApiListingBuilderPlugin ( ) {
@Override
public void apply ( ApiListingContext apiListingContext ) {
if ( apiListingContext . getResourceGroup ( ) . getGroupName ( ) . equals ( "default" ) ) {
ApiListing apiListing = apiListingContext . apiListingBuilder ( ) . build ( ) ;
if ( apiListing . getResourcePath ( ) . equals ( "/api/auth/login" ) ) {
apiListingContext . apiListingBuilder ( ) . tags ( Set . of ( new Tag ( "login-endpoint" , "Login Endpoint" ) ) ) ;
apiListingContext . apiListingBuilder ( ) . description ( "Login Endpoint" ) ;
}
}
}
@Override
public boolean supports ( DocumentationType delimiter ) {
return DocumentationType . SWAGGER_2 . equals ( delimiter ) | | DocumentationType . OAS_30 . equals ( delimiter ) ;
}
} ;
}
@Bean
UiConfiguration uiConfig ( ) {
return UiConfigurationBuilder . builder ( )
. deepLinking ( true )
. displayOperationId ( false )
. defaultModelsExpandDepth ( 1 )
. defaultModelExpandDepth ( 1 )
. defaultModelRendering ( ModelRendering . EXAMPLE )
. displayRequestDuration ( false )
. docExpansion ( DocExpansion . NONE )
. filter ( false )
. maxDisplayedTags ( null )
. operationsSorter ( OperationsSorter . ALPHA )
. showExtensions ( false )
. showCommonExtensions ( false )
. supportedSubmitMethods ( UiConfiguration . Constants . DEFAULT_SUBMIT_METHODS )
. validatorUrl ( null )
. syntaxHighlightActivate ( true )
. syntaxHighlightTheme ( "agate" )
. build ( ) ;
}
private SecurityScheme httpLogin ( ) {
return HttpLoginPasswordScheme
. X_AUTHORIZATION_BUILDER
. loginEndpoint ( "/api/auth/login" )
. name ( "HTTP login form" )
. description ( "Enter Username / Password" )
. build ( ) ;
}
private SecurityContext securityContext ( ) {
return SecurityContext . builder ( )
. securityReferences ( defaultAuth ( ) )
. forPaths ( securityPaths ( ) )
. operationSelector ( securityPathOperationSelector ( ) )
. build ( ) ;
}
@ -107,11 +224,8 @@ public class SwaggerConfiguration {
return regex ( apiPathRegex ) ;
}
private Predicate < String > securityPaths ( ) {
return and (
regex ( securityPathRegex ) ,
not ( regex ( nonSecurityPathRegex ) )
) ;
private Predicate < OperationContext > securityPathOperationSelector ( ) {
return new SecurityPathOperationSelector ( securityPathRegex , nonSecurityPathRegex ) ;
}
List < SecurityReference > defaultAuth ( ) {
@ -120,7 +234,7 @@ public class SwaggerConfiguration {
authorizationScopes [ 1 ] = new AuthorizationScope ( Authority . TENANT_ADMIN . name ( ) , "Tenant administrator" ) ;
authorizationScopes [ 2 ] = new AuthorizationScope ( Authority . CUSTOMER_USER . name ( ) , "Customer" ) ;
return newArrayList (
new SecurityReference ( "X-Authorization " , authorizationScopes ) ) ;
new SecurityReference ( "HTTP login form " , authorizationScopes ) ) ;
}
private ApiInfo apiInfo ( ) {
@ -134,4 +248,75 @@ public class SwaggerConfiguration {
. build ( ) ;
}
private ApiDescription loginEndpointApiDescription ( ) {
return new ApiDescription ( null , "/api/auth/login" , "Login method to get user JWT token data" , "Login endpoint" , Collections . singletonList (
new OperationBuilder ( operationNames )
. summary ( "Login method to get user JWT token data" )
. tags ( Set . of ( "login-endpoint" ) )
. authorizations ( new ArrayList < > ( ) )
. position ( 0 )
. codegenMethodNameStem ( "loginPost" )
. method ( HttpMethod . POST )
. notes ( "Login method to get user JWT token data.\n\nValue of the response **token** field can be used as JWT token value for authorization." )
. requestParameters (
List . of (
new RequestParameterBuilder ( )
. in ( ParameterType . BODY )
. required ( true )
. description ( "Login request" )
. content ( c - >
c . requestBody ( true )
. representation ( MediaType . APPLICATION_JSON )
. apply ( classRepresentation ( LoginRequest . class , false ) )
)
. build ( )
)
)
. responses ( loginResponses ( ) )
. build ( )
) , false ) ;
}
private Collection < Response > loginResponses ( ) {
return List . of (
new ResponseBuilder ( )
. code ( "200" )
. description ( "OK" )
. representation ( MediaType . APPLICATION_JSON )
. apply ( classRepresentation ( LoginResponse . class , true ) ) .
build ( )
) ;
}
/** Helper methods **/
private Consumer < RepresentationBuilder > classRepresentation ( Class clazz , boolean isResponse ) {
return r - > r . model (
m - >
m . referenceModel ( ref - >
ref . key ( k - >
k . qualifiedModelName ( q - >
q . namespace ( clazz . getPackageName ( ) )
. name ( clazz . getSimpleName ( ) ) ) . isResponse ( isResponse ) ) )
) ;
}
private static class SecurityPathOperationSelector implements Predicate < OperationContext > {
private final Predicate < String > securityPathSelector ;
SecurityPathOperationSelector ( String securityPathRegex , String nonSecurityPathRegex ) {
this . securityPathSelector = regex ( securityPathRegex ) . and (
not (
regex ( nonSecurityPathRegex )
) ) ;
}
@Override
public boolean test ( OperationContext operationContext ) {
return this . securityPathSelector . test ( operationContext . requestMappingPattern ( ) ) ;
}
}
}