Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in / Register
Toggle navigation
S
spring-boot
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
DEMO
spring-boot
Commits
d50fe887
Commit
d50fe887
authored
Nov 03, 2017
by
Madhura Bhave
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add CF support for reactive actuators
Closes gh-10780
parent
7c5d2fad
Changes
32
Show whitespace changes
Inline
Side-by-side
Showing
32 changed files
with
2474 additions
and
273 deletions
+2474
-273
pom.xml
...g-boot-project/spring-boot-actuator-autoconfigure/pom.xml
+10
-0
AccessLevel.java
.../boot/actuate/autoconfigure/cloudfoundry/AccessLevel.java
+2
-12
CloudFoundryAuthorizationException.java
...gure/cloudfoundry/CloudFoundryAuthorizationException.java
+4
-4
SecurityResponse.java
.../actuate/autoconfigure/cloudfoundry/SecurityResponse.java
+53
-0
Token.java
...mework/boot/actuate/autoconfigure/cloudfoundry/Token.java
+8
-7
CloudFoundryWebFluxEndpointHandlerMapping.java
...y/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java
+233
-0
ReactiveCloudFoundryActuatorAutoConfiguration.java
...active/ReactiveCloudFoundryActuatorAutoConfiguration.java
+140
-0
ReactiveCloudFoundrySecurityInterceptor.java
...dry/reactive/ReactiveCloudFoundrySecurityInterceptor.java
+117
-0
ReactiveCloudFoundrySecurityService.java
...foundry/reactive/ReactiveCloudFoundrySecurityService.java
+136
-0
ReactiveTokenValidator.java
...nfigure/cloudfoundry/reactive/ReactiveTokenValidator.java
+141
-0
CloudFoundryActuatorAutoConfiguration.java
...oundry/servlet/CloudFoundryActuatorAutoConfiguration.java
+47
-61
CloudFoundrySecurityInterceptor.java
...cloudfoundry/servlet/CloudFoundrySecurityInterceptor.java
+11
-38
CloudFoundrySecurityService.java
...ure/cloudfoundry/servlet/CloudFoundrySecurityService.java
+3
-1
CloudFoundryWebEndpointServletHandlerMapping.java
...servlet/CloudFoundryWebEndpointServletHandlerMapping.java
+8
-6
SkipSslVerificationHttpRequestFactory.java
...oundry/servlet/SkipSslVerificationHttpRequestFactory.java
+1
-1
TokenValidator.java
...te/autoconfigure/cloudfoundry/servlet/TokenValidator.java
+5
-3
spring.factories
...utoconfigure/src/main/resources/META-INF/spring.factories
+2
-1
AuthorizationExceptionMatcher.java
...configure/cloudfoundry/AuthorizationExceptionMatcher.java
+2
-2
CloudFoundryWebFluxEndpointIntegrationTests.java
...reactive/CloudFoundryWebFluxEndpointIntegrationTests.java
+338
-0
ReactiveCloudFoundryActuatorAutoConfigurationTests.java
...e/ReactiveCloudFoundryActuatorAutoConfigurationTests.java
+268
-0
ReactiveCloudFoundrySecurityInterceptorTests.java
...eactive/ReactiveCloudFoundrySecurityInterceptorTests.java
+187
-0
ReactiveCloudFoundrySecurityServiceTests.java
...ry/reactive/ReactiveCloudFoundrySecurityServiceTests.java
+263
-0
ReactiveTokenValidatorTests.java
...re/cloudfoundry/reactive/ReactiveTokenValidatorTests.java
+260
-0
CloudFoundryActuatorAutoConfigurationTests.java
...y/servlet/CloudFoundryActuatorAutoConfigurationTests.java
+1
-1
CloudFoundryMvcWebEndpointIntegrationTests.java
...y/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java
+5
-2
CloudFoundrySecurityInterceptorTests.java
...foundry/servlet/CloudFoundrySecurityInterceptorTests.java
+12
-9
CloudFoundrySecurityServiceTests.java
...loudfoundry/servlet/CloudFoundrySecurityServiceTests.java
+3
-1
SkipSslVerificationHttpRequestFactoryTests.java
...y/servlet/SkipSslVerificationHttpRequestFactoryTests.java
+1
-1
TokenValidatorTests.java
...toconfigure/cloudfoundry/servlet/TokenValidatorTests.java
+3
-1
AbstractWebFluxEndpointHandlerMapping.java
...t/web/reactive/AbstractWebFluxEndpointHandlerMapping.java
+196
-0
WebFluxEndpointHandlerMapping.java
.../endpoint/web/reactive/WebFluxEndpointHandlerMapping.java
+8
-121
pom.xml
spring-boot-project/spring-boot-parent/pom.xml
+6
-1
No files found.
spring-boot-project/spring-boot-actuator-autoconfigure/pom.xml
View file @
d50fe887
...
...
@@ -348,6 +348,16 @@
<artifactId>
logback-classic
</artifactId>
<scope>
test
</scope>
</dependency>
<dependency>
<groupId>
io.projectreactor
</groupId>
<artifactId>
reactor-test
</artifactId>
<scope>
test
</scope>
</dependency>
<dependency>
<groupId>
com.squareup.okhttp3
</groupId>
<artifactId>
mockwebserver
</artifactId>
<scope>
test
</scope>
</dependency>
<dependency>
<groupId>
com.jayway.jsonpath
</groupId>
<artifactId>
json-path
</artifactId>
...
...
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/AccessLevel.java
View file @
d50fe887
...
...
@@ -19,15 +19,13 @@ package org.springframework.boot.actuate.autoconfigure.cloudfoundry;
import
java.util.Arrays
;
import
java.util.List
;
import
javax.servlet.http.HttpServletRequest
;
/**
* The specific access level granted to the cloud foundry user that's calling the
* endpoints.
*
* @author Madhura Bhave
*/
enum
AccessLevel
{
public
enum
AccessLevel
{
/**
* Restricted access to a limited set of endpoints.
...
...
@@ -39,7 +37,7 @@ enum AccessLevel {
*/
FULL
;
p
rivate
static
final
String
REQUEST_ATTRIBUTE
=
"cloudFoundryAccessLevel"
;
p
ublic
static
final
String
REQUEST_ATTRIBUTE
=
"cloudFoundryAccessLevel"
;
private
final
List
<
String
>
endpointPaths
;
...
...
@@ -56,12 +54,4 @@ enum AccessLevel {
return
this
.
endpointPaths
.
isEmpty
()
||
this
.
endpointPaths
.
contains
(
endpointPath
);
}
public
void
put
(
HttpServletRequest
request
)
{
request
.
setAttribute
(
REQUEST_ATTRIBUTE
,
this
);
}
public
static
AccessLevel
get
(
HttpServletRequest
request
)
{
return
(
AccessLevel
)
request
.
getAttribute
(
REQUEST_ATTRIBUTE
);
}
}
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryAuthorizationException.java
View file @
d50fe887
...
...
@@ -23,15 +23,15 @@ import org.springframework.http.HttpStatus;
*
* @author Madhura Bhave
*/
class
CloudFoundryAuthorizationException
extends
RuntimeException
{
public
class
CloudFoundryAuthorizationException
extends
RuntimeException
{
private
final
Reason
reason
;
CloudFoundryAuthorizationException
(
Reason
reason
,
String
message
)
{
public
CloudFoundryAuthorizationException
(
Reason
reason
,
String
message
)
{
this
(
reason
,
message
,
null
);
}
CloudFoundryAuthorizationException
(
Reason
reason
,
String
message
,
Throwable
cause
)
{
public
CloudFoundryAuthorizationException
(
Reason
reason
,
String
message
,
Throwable
cause
)
{
super
(
message
);
this
.
reason
=
reason
;
}
...
...
@@ -55,7 +55,7 @@ class CloudFoundryAuthorizationException extends RuntimeException {
/**
* Reasons why the exception can be thrown.
*/
enum
Reason
{
public
enum
Reason
{
ACCESS_DENIED
(
HttpStatus
.
FORBIDDEN
),
...
...
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/SecurityResponse.java
0 → 100644
View file @
d50fe887
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
;
import
org.springframework.http.HttpStatus
;
/**
* Response from the Cloud Foundry security interceptors.
*
* @author Madhura Bhave
*/
public
class
SecurityResponse
{
private
final
HttpStatus
status
;
private
final
String
message
;
public
SecurityResponse
(
HttpStatus
status
)
{
this
(
status
,
null
);
}
public
SecurityResponse
(
HttpStatus
status
,
String
message
)
{
this
.
status
=
status
;
this
.
message
=
message
;
}
public
HttpStatus
getStatus
()
{
return
this
.
status
;
}
public
String
getMessage
()
{
return
this
.
message
;
}
public
static
SecurityResponse
success
()
{
return
new
SecurityResponse
(
HttpStatus
.
OK
);
}
}
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/Token.java
View file @
d50fe887
...
...
@@ -20,6 +20,7 @@ import java.nio.charset.Charset;
import
java.util.List
;
import
java.util.Map
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason
;
import
org.springframework.boot.json.JsonParserFactory
;
import
org.springframework.util.Base64Utils
;
import
org.springframework.util.StringUtils
;
...
...
@@ -29,7 +30,7 @@ import org.springframework.util.StringUtils;
*
* @author Madhura Bhave
*/
class
Token
{
public
class
Token
{
private
static
final
Charset
UTF_8
=
Charset
.
forName
(
"UTF-8"
);
...
...
@@ -41,13 +42,13 @@ class Token {
private
final
Map
<
String
,
Object
>
claims
;
Token
(
String
encoded
)
{
public
Token
(
String
encoded
)
{
this
.
encoded
=
encoded
;
int
firstPeriod
=
encoded
.
indexOf
(
'.'
);
int
lastPeriod
=
encoded
.
lastIndexOf
(
'.'
);
if
(
firstPeriod
<=
0
||
lastPeriod
<=
firstPeriod
)
{
throw
new
CloudFoundryAuthorizationException
(
CloudFoundryAuthorizationException
.
Reason
.
INVALID_TOKEN
,
Reason
.
INVALID_TOKEN
,
"JWT must have header, body and signature"
);
}
this
.
header
=
parseJson
(
encoded
.
substring
(
0
,
firstPeriod
));
...
...
@@ -55,7 +56,7 @@ class Token {
this
.
signature
=
encoded
.
substring
(
lastPeriod
+
1
);
if
(!
StringUtils
.
hasLength
(
this
.
signature
))
{
throw
new
CloudFoundryAuthorizationException
(
CloudFoundryAuthorizationException
.
Reason
.
INVALID_TOKEN
,
Reason
.
INVALID_TOKEN
,
"Token must have non-empty crypto segment"
);
}
}
...
...
@@ -67,7 +68,7 @@ class Token {
}
catch
(
RuntimeException
ex
)
{
throw
new
CloudFoundryAuthorizationException
(
CloudFoundryAuthorizationException
.
Reason
.
INVALID_TOKEN
,
Reason
.
INVALID_TOKEN
,
"Token could not be parsed"
,
ex
);
}
}
...
...
@@ -106,12 +107,12 @@ class Token {
Object
value
=
map
.
get
(
key
);
if
(
value
==
null
)
{
throw
new
CloudFoundryAuthorizationException
(
CloudFoundryAuthorizationException
.
Reason
.
INVALID_TOKEN
,
Reason
.
INVALID_TOKEN
,
"Unable to get value from key "
+
key
);
}
if
(!
type
.
isInstance
(
value
))
{
throw
new
CloudFoundryAuthorizationException
(
CloudFoundryAuthorizationException
.
Reason
.
INVALID_TOKEN
,
Reason
.
INVALID_TOKEN
,
"Unexpected value type from key "
+
key
+
" value "
+
value
);
}
return
(
T
)
value
;
...
...
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointHandlerMapping.java
0 → 100644
View file @
d50fe887
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
reactive
;
import
java.lang.reflect.Method
;
import
java.util.Collection
;
import
java.util.Collections
;
import
java.util.HashMap
;
import
java.util.LinkedHashMap
;
import
java.util.Map
;
import
java.util.stream.Collectors
;
import
org.reactivestreams.Publisher
;
import
reactor.core.publisher.Mono
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel
;
import
org.springframework.boot.actuate.endpoint.EndpointInfo
;
import
org.springframework.boot.actuate.endpoint.OperationInvoker
;
import
org.springframework.boot.actuate.endpoint.OperationType
;
import
org.springframework.boot.actuate.endpoint.ParameterMappingException
;
import
org.springframework.boot.actuate.endpoint.ParametersMissingException
;
import
org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver
;
import
org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes
;
import
org.springframework.boot.actuate.endpoint.web.Link
;
import
org.springframework.boot.actuate.endpoint.web.WebEndpointOperation
;
import
org.springframework.boot.actuate.endpoint.web.WebEndpointResponse
;
import
org.springframework.boot.actuate.endpoint.web.reactive.AbstractWebFluxEndpointHandlerMapping
;
import
org.springframework.boot.endpoint.web.EndpointMapping
;
import
org.springframework.http.HttpMethod
;
import
org.springframework.http.HttpStatus
;
import
org.springframework.http.ResponseEntity
;
import
org.springframework.http.server.reactive.ServerHttpRequest
;
import
org.springframework.util.ReflectionUtils
;
import
org.springframework.web.bind.annotation.RequestBody
;
import
org.springframework.web.bind.annotation.ResponseBody
;
import
org.springframework.web.cors.CorsConfiguration
;
import
org.springframework.web.reactive.HandlerMapping
;
import
org.springframework.web.server.ServerWebExchange
;
import
org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping
;
/**
* A custom {@link RequestMappingInfoHandlerMapping} that makes web endpoints available on
* Cloud Foundry specific URLs over HTTP using Spring WebFlux.
*
* @author Madhura Bhave
*/
public
class
CloudFoundryWebFluxEndpointHandlerMapping
extends
AbstractWebFluxEndpointHandlerMapping
{
private
final
Method
handleRead
=
ReflectionUtils
.
findMethod
(
ReadOperationHandler
.
class
,
"handle"
,
ServerWebExchange
.
class
);
private
final
Method
handleWrite
=
ReflectionUtils
.
findMethod
(
WriteOperationHandler
.
class
,
"handle"
,
ServerWebExchange
.
class
,
Map
.
class
);
private
final
Method
links
=
ReflectionUtils
.
findMethod
(
getClass
(),
"links"
,
ServerWebExchange
.
class
);
private
final
EndpointLinksResolver
endpointLinksResolver
=
new
EndpointLinksResolver
();
private
final
ReactiveCloudFoundrySecurityInterceptor
securityInterceptor
;
@Override
protected
Method
getLinks
()
{
return
this
.
links
;
}
@Override
protected
void
registerMappingForOperation
(
WebEndpointOperation
operation
)
{
OperationType
operationType
=
operation
.
getType
();
OperationInvoker
operationInvoker
=
operation
.
getInvoker
();
if
(
operation
.
isBlocking
())
{
operationInvoker
=
new
ElasticSchedulerOperationInvoker
(
operationInvoker
);
}
registerMapping
(
createRequestMappingInfo
(
operation
),
operationType
==
OperationType
.
WRITE
?
new
WriteOperationHandler
(
operationInvoker
,
operation
.
getId
())
:
new
ReadOperationHandler
(
operationInvoker
,
operation
.
getId
()),
operationType
==
OperationType
.
WRITE
?
this
.
handleWrite
:
this
.
handleRead
);
}
@ResponseBody
private
Publisher
<
ResponseEntity
<
Object
>>
links
(
ServerWebExchange
exchange
)
{
ServerHttpRequest
request
=
exchange
.
getRequest
();
return
this
.
securityInterceptor
.
preHandle
(
exchange
,
""
)
.
map
(
securityResponse
->
{
if
(!
securityResponse
.
getStatus
().
equals
(
HttpStatus
.
OK
))
{
return
new
ResponseEntity
<>(
securityResponse
.
getStatus
());
}
AccessLevel
accessLevel
=
exchange
.
getAttribute
(
AccessLevel
.
REQUEST_ATTRIBUTE
);
Map
<
String
,
Link
>
links
=
this
.
endpointLinksResolver
.
resolveLinks
(
getEndpoints
(),
request
.
getURI
().
toString
());
return
new
ResponseEntity
<>(
Collections
.
singletonMap
(
"_links"
,
getAccessibleLinks
(
accessLevel
,
links
)),
HttpStatus
.
OK
);
});
}
private
Map
<
String
,
Link
>
getAccessibleLinks
(
AccessLevel
accessLevel
,
Map
<
String
,
Link
>
links
)
{
if
(
accessLevel
==
null
)
{
return
new
LinkedHashMap
<>();
}
return
links
.
entrySet
().
stream
()
.
filter
((
e
)
->
e
.
getKey
().
equals
(
"self"
)
||
accessLevel
.
isAccessAllowed
(
e
.
getKey
()))
.
collect
(
Collectors
.
toMap
(
Map
.
Entry
::
getKey
,
Map
.
Entry
::
getValue
));
}
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointMapping the base mapping for all endpoints
* @param webEndpoints the web endpoints
* @param endpointMediaTypes media types consumed and produced by the endpoints
* @param corsConfiguration the CORS configuration for the endpoints
* @param securityInterceptor the Security Interceptor
*/
public
CloudFoundryWebFluxEndpointHandlerMapping
(
EndpointMapping
endpointMapping
,
Collection
<
EndpointInfo
<
WebEndpointOperation
>>
webEndpoints
,
EndpointMediaTypes
endpointMediaTypes
,
CorsConfiguration
corsConfiguration
,
ReactiveCloudFoundrySecurityInterceptor
securityInterceptor
)
{
super
(
endpointMapping
,
webEndpoints
,
endpointMediaTypes
,
corsConfiguration
);
this
.
securityInterceptor
=
securityInterceptor
;
}
/**
* Base class for handlers for endpoint operations.
*/
abstract
class
AbstractOperationHandler
{
private
final
OperationInvoker
operationInvoker
;
private
final
String
endpointId
;
private
final
ReactiveCloudFoundrySecurityInterceptor
securityInterceptor
;
AbstractOperationHandler
(
OperationInvoker
operationInvoker
,
String
endpointId
,
ReactiveCloudFoundrySecurityInterceptor
securityInterceptor
)
{
this
.
operationInvoker
=
operationInvoker
;
this
.
endpointId
=
endpointId
;
this
.
securityInterceptor
=
securityInterceptor
;
}
@SuppressWarnings
({
"unchecked"
})
Publisher
<
ResponseEntity
<
Object
>>
doHandle
(
ServerWebExchange
exchange
,
Map
<
String
,
String
>
body
)
{
return
this
.
securityInterceptor
.
preHandle
(
exchange
,
this
.
endpointId
)
.
flatMap
(
securityResponse
->
{
if
(!
securityResponse
.
getStatus
().
equals
(
HttpStatus
.
OK
))
{
return
Mono
.
just
(
new
ResponseEntity
<>(
securityResponse
.
getStatus
()));
}
Map
<
String
,
Object
>
arguments
=
new
HashMap
<>(
exchange
.
getAttribute
(
HandlerMapping
.
URI_TEMPLATE_VARIABLES_ATTRIBUTE
));
if
(
body
!=
null
)
{
arguments
.
putAll
(
body
);
}
exchange
.
getRequest
().
getQueryParams
().
forEach
((
name
,
values
)
->
arguments
.
put
(
name
,
values
.
size
()
==
1
?
values
.
get
(
0
)
:
values
));
return
handleResult
((
Publisher
<?>)
this
.
operationInvoker
.
invoke
(
arguments
),
exchange
.
getRequest
().
getMethod
());
});
}
private
Mono
<
ResponseEntity
<
Object
>>
handleResult
(
Publisher
<?>
result
,
HttpMethod
httpMethod
)
{
return
Mono
.
from
(
result
).
map
(
this
::
toResponseEntity
)
.
onErrorReturn
(
ParametersMissingException
.
class
,
new
ResponseEntity
<>(
HttpStatus
.
BAD_REQUEST
))
.
onErrorReturn
(
ParameterMappingException
.
class
,
new
ResponseEntity
<>(
HttpStatus
.
BAD_REQUEST
))
.
defaultIfEmpty
(
new
ResponseEntity
<>(
httpMethod
==
HttpMethod
.
GET
?
HttpStatus
.
NOT_FOUND
:
HttpStatus
.
NO_CONTENT
));
}
private
ResponseEntity
<
Object
>
toResponseEntity
(
Object
response
)
{
if
(!(
response
instanceof
WebEndpointResponse
))
{
return
new
ResponseEntity
<>(
response
,
HttpStatus
.
OK
);
}
WebEndpointResponse
<?>
webEndpointResponse
=
(
WebEndpointResponse
<?>)
response
;
return
new
ResponseEntity
<>(
webEndpointResponse
.
getBody
(),
HttpStatus
.
valueOf
(
webEndpointResponse
.
getStatus
()));
}
}
/**
* A handler for an endpoint write operation.
*/
final
class
WriteOperationHandler
extends
AbstractOperationHandler
{
WriteOperationHandler
(
OperationInvoker
operationInvoker
,
String
endpointId
)
{
super
(
operationInvoker
,
endpointId
,
CloudFoundryWebFluxEndpointHandlerMapping
.
this
.
securityInterceptor
);
}
@ResponseBody
public
Publisher
<
ResponseEntity
<
Object
>>
handle
(
ServerWebExchange
exchange
,
@RequestBody
(
required
=
false
)
Map
<
String
,
String
>
body
)
{
return
doHandle
(
exchange
,
body
);
}
}
/**
* A handler for an endpoint write operation.
*/
final
class
ReadOperationHandler
extends
AbstractOperationHandler
{
ReadOperationHandler
(
OperationInvoker
operationInvoker
,
String
endpointId
)
{
super
(
operationInvoker
,
endpointId
,
CloudFoundryWebFluxEndpointHandlerMapping
.
this
.
securityInterceptor
);
}
@ResponseBody
public
Publisher
<
ResponseEntity
<
Object
>>
handle
(
ServerWebExchange
exchange
)
{
return
doHandle
(
exchange
,
null
);
}
}
}
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration.java
0 → 100644
View file @
d50fe887
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
reactive
;
import
java.util.Arrays
;
import
java.util.Collections
;
import
org.springframework.beans.BeansException
;
import
org.springframework.beans.factory.config.BeanPostProcessor
;
import
org.springframework.boot.actuate.autoconfigure.endpoint.DefaultCachingConfigurationFactory
;
import
org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties
;
import
org.springframework.boot.actuate.endpoint.ParameterMapper
;
import
org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes
;
import
org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer
;
import
org.springframework.boot.autoconfigure.EnableAutoConfiguration
;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnClass
;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform
;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication
;
import
org.springframework.boot.cloud.CloudPlatform
;
import
org.springframework.boot.endpoint.web.EndpointMapping
;
import
org.springframework.context.ApplicationContext
;
import
org.springframework.context.annotation.Bean
;
import
org.springframework.context.annotation.Configuration
;
import
org.springframework.core.env.Environment
;
import
org.springframework.http.HttpMethod
;
import
org.springframework.security.web.server.MatcherSecurityWebFilterChain
;
import
org.springframework.security.web.server.WebFilterChainProxy
;
import
org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher
;
import
org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers
;
import
org.springframework.web.cors.CorsConfiguration
;
import
org.springframework.web.reactive.function.client.WebClient
;
import
org.springframework.web.server.WebFilter
;
/**
* {@link EnableAutoConfiguration Auto-configuration} to expose actuator endpoints for
* cloud foundry to use in a reactive environment.
*
* @author Madhura Bhave
* @since 2.0.0
*/
@Configuration
@ConditionalOnProperty
(
prefix
=
"management.cloudfoundry"
,
name
=
"enabled"
,
matchIfMissing
=
true
)
@ConditionalOnWebApplication
(
type
=
ConditionalOnWebApplication
.
Type
.
REACTIVE
)
@ConditionalOnCloudPlatform
(
CloudPlatform
.
CLOUD_FOUNDRY
)
public
class
ReactiveCloudFoundryActuatorAutoConfiguration
{
private
final
ApplicationContext
applicationContext
;
ReactiveCloudFoundryActuatorAutoConfiguration
(
ApplicationContext
applicationContext
)
{
this
.
applicationContext
=
applicationContext
;
}
@Bean
public
CloudFoundryWebFluxEndpointHandlerMapping
cloudFoundryWebFluxEndpointHandlerMapping
(
ParameterMapper
parameterMapper
,
EndpointMediaTypes
endpointMediaTypes
,
WebClient
.
Builder
webClientBuilder
,
Environment
environment
,
DefaultCachingConfigurationFactory
cachingConfigurationFactory
,
WebEndpointProperties
webEndpointProperties
)
{
WebAnnotationEndpointDiscoverer
endpointDiscoverer
=
new
WebAnnotationEndpointDiscoverer
(
this
.
applicationContext
,
parameterMapper
,
cachingConfigurationFactory
,
endpointMediaTypes
,
(
id
)
->
id
);
return
new
CloudFoundryWebFluxEndpointHandlerMapping
(
new
EndpointMapping
(
"/cloudfoundryapplication"
),
endpointDiscoverer
.
discoverEndpoints
(),
endpointMediaTypes
,
getCorsConfiguration
(),
getSecurityInterceptor
(
webClientBuilder
,
environment
));
}
private
ReactiveCloudFoundrySecurityInterceptor
getSecurityInterceptor
(
WebClient
.
Builder
restTemplateBuilder
,
Environment
environment
)
{
ReactiveCloudFoundrySecurityService
cloudfoundrySecurityService
=
getCloudFoundrySecurityService
(
restTemplateBuilder
,
environment
);
ReactiveTokenValidator
tokenValidator
=
new
ReactiveTokenValidator
(
cloudfoundrySecurityService
);
return
new
ReactiveCloudFoundrySecurityInterceptor
(
tokenValidator
,
cloudfoundrySecurityService
,
environment
.
getProperty
(
"vcap.application.application_id"
));
}
private
ReactiveCloudFoundrySecurityService
getCloudFoundrySecurityService
(
WebClient
.
Builder
webClientBuilder
,
Environment
environment
)
{
String
cloudControllerUrl
=
environment
.
getProperty
(
"vcap.application.cf_api"
);
return
(
cloudControllerUrl
==
null
?
null
:
new
ReactiveCloudFoundrySecurityService
(
webClientBuilder
,
cloudControllerUrl
));
}
private
CorsConfiguration
getCorsConfiguration
()
{
CorsConfiguration
corsConfiguration
=
new
CorsConfiguration
();
corsConfiguration
.
addAllowedOrigin
(
CorsConfiguration
.
ALL
);
corsConfiguration
.
setAllowedMethods
(
Arrays
.
asList
(
HttpMethod
.
GET
.
name
(),
HttpMethod
.
POST
.
name
()));
corsConfiguration
.
setAllowedHeaders
(
Arrays
.
asList
(
"Authorization"
,
"X-Cf-App-Instance"
,
"Content-Type"
));
return
corsConfiguration
;
}
@Configuration
@ConditionalOnClass
(
MatcherSecurityWebFilterChain
.
class
)
static
class
IgnoredPathsSecurityConfiguration
{
@Bean
public
BeanPostProcessor
webFilterChainPostProcessor
()
{
return
new
BeanPostProcessor
()
{
@Override
public
Object
postProcessAfterInitialization
(
Object
bean
,
String
beanName
)
throws
BeansException
{
if
(
bean
instanceof
WebFilterChainProxy
)
{
return
postProcess
((
WebFilterChainProxy
)
bean
);
}
return
bean
;
}
};
}
WebFilterChainProxy
postProcess
(
WebFilterChainProxy
existing
)
{
ServerWebExchangeMatcher
cloudFoundryRequestMatcher
=
ServerWebExchangeMatchers
.
pathMatchers
(
"/cloudfoundryapplication/**"
);
WebFilter
noOpFilter
=
(
exchange
,
chain
)
->
chain
.
filter
(
exchange
);
MatcherSecurityWebFilterChain
ignoredRequestFilterChain
=
new
MatcherSecurityWebFilterChain
(
cloudFoundryRequestMatcher
,
Collections
.
singletonList
(
noOpFilter
));
MatcherSecurityWebFilterChain
allRequestsFilterChain
=
new
MatcherSecurityWebFilterChain
(
ServerWebExchangeMatchers
.
anyExchange
(),
Collections
.
singletonList
(
existing
));
return
new
WebFilterChainProxy
(
ignoredRequestFilterChain
,
allRequestsFilterChain
);
}
}
}
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptor.java
0 → 100644
View file @
d50fe887
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
reactive
;
import
org.apache.commons.logging.Log
;
import
org.apache.commons.logging.LogFactory
;
import
reactor.core.publisher.Mono
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.SecurityResponse
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.Token
;
import
org.springframework.http.HttpStatus
;
import
org.springframework.http.server.reactive.ServerHttpRequest
;
import
org.springframework.util.StringUtils
;
import
org.springframework.web.cors.reactive.CorsUtils
;
import
org.springframework.web.server.ServerWebExchange
;
/**
* Security interceptor to validate the cloud foundry token.
*
* @author Madhura Bhave
*/
class
ReactiveCloudFoundrySecurityInterceptor
{
private
static
final
Log
logger
=
LogFactory
.
getLog
(
ReactiveCloudFoundrySecurityInterceptor
.
class
);
private
final
ReactiveTokenValidator
tokenValidator
;
private
final
ReactiveCloudFoundrySecurityService
cloudFoundrySecurityService
;
private
final
String
applicationId
;
private
static
Mono
<
SecurityResponse
>
SUCCESS
=
Mono
.
just
(
SecurityResponse
.
success
());
ReactiveCloudFoundrySecurityInterceptor
(
ReactiveTokenValidator
tokenValidator
,
ReactiveCloudFoundrySecurityService
cloudFoundrySecurityService
,
String
applicationId
)
{
this
.
tokenValidator
=
tokenValidator
;
this
.
cloudFoundrySecurityService
=
cloudFoundrySecurityService
;
this
.
applicationId
=
applicationId
;
}
Mono
<
SecurityResponse
>
preHandle
(
ServerWebExchange
exchange
,
String
endpointId
)
{
ServerHttpRequest
request
=
exchange
.
getRequest
();
if
(
CorsUtils
.
isPreFlightRequest
(
request
))
{
return
SUCCESS
;
}
if
(!
StringUtils
.
hasText
(
this
.
applicationId
))
{
return
Mono
.
error
(
new
CloudFoundryAuthorizationException
(
Reason
.
SERVICE_UNAVAILABLE
,
"Application id is not available"
));
}
if
(
this
.
cloudFoundrySecurityService
==
null
)
{
return
Mono
.
error
(
new
CloudFoundryAuthorizationException
(
Reason
.
SERVICE_UNAVAILABLE
,
"Cloud controller URL is not available"
));
}
return
check
(
exchange
,
endpointId
)
.
then
(
SUCCESS
)
.
doOnError
(
throwable
->
logger
.
error
(
throwable
.
getMessage
(),
throwable
))
.
onErrorResume
(
this
::
getErrorResponse
);
}
private
Mono
<
Void
>
check
(
ServerWebExchange
exchange
,
String
path
)
{
try
{
Token
token
=
getToken
(
exchange
.
getRequest
());
return
this
.
tokenValidator
.
validate
(
token
).
then
(
this
.
cloudFoundrySecurityService
.
getAccessLevel
(
token
.
toString
(),
this
.
applicationId
))
.
filter
(
accessLevel
->
accessLevel
.
isAccessAllowed
(
path
))
.
switchIfEmpty
(
Mono
.
error
(
new
CloudFoundryAuthorizationException
(
Reason
.
ACCESS_DENIED
,
"Access denied"
)))
.
doOnSuccess
(
accessLevel
->
exchange
.
getAttributes
().
put
(
"cloudFoundryAccessLevel"
,
accessLevel
))
.
then
();
}
catch
(
CloudFoundryAuthorizationException
ex
)
{
return
Mono
.
error
(
ex
);
}
}
private
Mono
<
SecurityResponse
>
getErrorResponse
(
Throwable
throwable
)
{
if
(
throwable
instanceof
CloudFoundryAuthorizationException
)
{
CloudFoundryAuthorizationException
cfException
=
(
CloudFoundryAuthorizationException
)
throwable
;
return
Mono
.
just
(
new
SecurityResponse
(
cfException
.
getStatusCode
(),
"{\"security_error\":\""
+
cfException
.
getMessage
()
+
"\"}"
));
}
return
Mono
.
just
(
new
SecurityResponse
(
HttpStatus
.
INTERNAL_SERVER_ERROR
,
throwable
.
getMessage
()));
}
private
Token
getToken
(
ServerHttpRequest
request
)
{
String
authorization
=
request
.
getHeaders
().
getFirst
(
"Authorization"
);
String
bearerPrefix
=
"bearer "
;
if
(
authorization
==
null
||
!
authorization
.
toLowerCase
().
startsWith
(
bearerPrefix
))
{
throw
new
CloudFoundryAuthorizationException
(
Reason
.
MISSING_AUTHORIZATION
,
"Authorization header is missing or invalid"
);
}
return
new
Token
(
authorization
.
substring
(
bearerPrefix
.
length
()));
}
}
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityService.java
0 → 100644
View file @
d50fe887
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
reactive
;
import
java.util.HashMap
;
import
java.util.List
;
import
java.util.Map
;
import
reactor.core.publisher.Mono
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason
;
import
org.springframework.core.ParameterizedTypeReference
;
import
org.springframework.http.HttpStatus
;
import
org.springframework.util.Assert
;
import
org.springframework.web.reactive.function.client.WebClient
;
import
org.springframework.web.reactive.function.client.WebClientResponseException
;
/**
* Reactive Cloud Foundry security service to handle REST calls to the cloud controller and UAA.
*
* @author Madhura Bhave
*/
public
class
ReactiveCloudFoundrySecurityService
{
private
final
WebClient
webClient
;
private
final
String
cloudControllerUrl
;
private
Mono
<
String
>
uaaUrl
;
ReactiveCloudFoundrySecurityService
(
WebClient
.
Builder
webClientBuilder
,
String
cloudControllerUrl
)
{
Assert
.
notNull
(
webClientBuilder
,
"Webclient must not be null"
);
Assert
.
notNull
(
cloudControllerUrl
,
"CloudControllerUrl must not be null"
);
this
.
webClient
=
webClientBuilder
.
build
();
this
.
cloudControllerUrl
=
cloudControllerUrl
;
}
/**
* Return a Mono of the access level that should be granted to the given token.
* @param token the token
* @param applicationId the cloud foundry application ID
* @return a Mono of the access level that should be granted
* @throws CloudFoundryAuthorizationException if the token is not authorized
*/
public
Mono
<
AccessLevel
>
getAccessLevel
(
String
token
,
String
applicationId
)
throws
CloudFoundryAuthorizationException
{
String
uri
=
getPermissionsUri
(
applicationId
);
return
this
.
webClient
.
get
().
uri
(
uri
)
.
header
(
"Authorization"
,
"bearer "
+
token
)
.
retrieve
().
bodyToMono
(
Map
.
class
)
.
map
(
this
::
getAccessLevel
)
.
onErrorMap
(
throwable
->
{
if
(
throwable
instanceof
WebClientResponseException
)
{
HttpStatus
statusCode
=
((
WebClientResponseException
)
throwable
).
getStatusCode
();
if
(
statusCode
.
equals
(
HttpStatus
.
FORBIDDEN
))
{
return
new
CloudFoundryAuthorizationException
(
Reason
.
ACCESS_DENIED
,
"Access denied"
);
}
if
(
statusCode
.
is4xxClientError
())
{
return
new
CloudFoundryAuthorizationException
(
Reason
.
INVALID_TOKEN
,
"Invalid token"
,
throwable
);
}
}
return
new
CloudFoundryAuthorizationException
(
Reason
.
SERVICE_UNAVAILABLE
,
"Cloud controller not reachable"
);
});
}
private
AccessLevel
getAccessLevel
(
Map
body
)
{
if
(
Boolean
.
TRUE
.
equals
(
body
.
get
(
"read_sensitive_data"
)))
{
return
AccessLevel
.
FULL
;
}
return
AccessLevel
.
RESTRICTED
;
}
private
String
getPermissionsUri
(
String
applicationId
)
{
return
this
.
cloudControllerUrl
+
"/v2/apps/"
+
applicationId
+
"/permissions"
;
}
/**
* Return a Mono of all token keys known by the UAA.
* @return a Mono of token keys
*/
public
Mono
<
Map
<
String
,
String
>>
fetchTokenKeys
()
{
return
getUaaUrl
()
.
flatMap
(
url
->
this
.
webClient
.
get
()
.
uri
(
url
+
"/token_keys"
)
.
retrieve
().
bodyToMono
(
new
ParameterizedTypeReference
<
Map
<
String
,
Object
>>()
{
})
.
map
(
this
::
extractTokenKeys
)
.
onErrorMap
((
throwable
->
new
CloudFoundryAuthorizationException
(
Reason
.
SERVICE_UNAVAILABLE
,
throwable
.
getMessage
()))));
}
private
Map
<
String
,
String
>
extractTokenKeys
(
Map
<
String
,
Object
>
response
)
{
Map
<
String
,
String
>
tokenKeys
=
new
HashMap
<>();
for
(
Object
key
:
(
List
<?>)
response
.
get
(
"keys"
))
{
Map
<?,
?>
tokenKey
=
(
Map
<?,
?>)
key
;
tokenKeys
.
put
((
String
)
tokenKey
.
get
(
"kid"
),
(
String
)
tokenKey
.
get
(
"value"
));
}
return
tokenKeys
;
}
/**
* Return a Mono of URL of the UAA.
* @return the UAA url Mono
*/
public
Mono
<
String
>
getUaaUrl
()
{
this
.
uaaUrl
=
this
.
webClient
.
get
().
uri
(
this
.
cloudControllerUrl
+
"/info"
)
.
retrieve
().
bodyToMono
(
Map
.
class
)
.
map
(
response
->
(
String
)
response
.
get
(
"token_endpoint"
)).
cache
()
.
onErrorMap
(
throwable
->
new
CloudFoundryAuthorizationException
(
Reason
.
SERVICE_UNAVAILABLE
,
"Unable to fetch token keys from UAA."
));
return
this
.
uaaUrl
;
}
}
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveTokenValidator.java
0 → 100644
View file @
d50fe887
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
reactive
;
import
java.security.GeneralSecurityException
;
import
java.security.KeyFactory
;
import
java.security.NoSuchAlgorithmException
;
import
java.security.PublicKey
;
import
java.security.Signature
;
import
java.security.spec.InvalidKeySpecException
;
import
java.security.spec.X509EncodedKeySpec
;
import
java.util.Map
;
import
java.util.concurrent.TimeUnit
;
import
reactor.core.publisher.Mono
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.Token
;
import
org.springframework.util.Base64Utils
;
/**
* Validator used to ensure that a signed {@link Token} has not been tampered with.
*
* @author Madhura Bhave
*/
public
class
ReactiveTokenValidator
{
private
final
ReactiveCloudFoundrySecurityService
securityService
;
public
ReactiveTokenValidator
(
ReactiveCloudFoundrySecurityService
securityService
)
{
this
.
securityService
=
securityService
;
}
public
Mono
<
Void
>
validate
(
Token
token
)
{
return
validateAlgorithm
(
token
)
.
then
(
validateKeyIdAndSignature
(
token
))
.
then
(
validateExpiry
(
token
))
.
then
(
validateIssuer
(
token
))
.
then
(
validateAudience
(
token
));
}
private
Mono
<
Void
>
validateAlgorithm
(
Token
token
)
{
String
algorithm
=
token
.
getSignatureAlgorithm
();
if
(
algorithm
==
null
)
{
return
Mono
.
error
(
new
CloudFoundryAuthorizationException
(
Reason
.
INVALID_SIGNATURE
,
"Signing algorithm cannot be null"
));
}
if
(!
algorithm
.
equals
(
"RS256"
))
{
return
Mono
.
error
(
new
CloudFoundryAuthorizationException
(
Reason
.
UNSUPPORTED_TOKEN_SIGNING_ALGORITHM
,
"Signing algorithm "
+
algorithm
+
" not supported"
));
}
return
Mono
.
empty
();
}
private
Mono
<
Void
>
validateKeyIdAndSignature
(
Token
token
)
{
String
keyId
=
token
.
getKeyId
();
return
this
.
securityService
.
fetchTokenKeys
()
.
filter
(
tokenKeys
->
hasValidKeyId
(
keyId
,
tokenKeys
))
.
switchIfEmpty
(
Mono
.
error
(
new
CloudFoundryAuthorizationException
(
Reason
.
INVALID_KEY_ID
,
"Key Id present in token header does not match"
)))
.
filter
(
tokenKeys
->
hasValidSignature
(
token
,
tokenKeys
.
get
(
keyId
)))
.
switchIfEmpty
(
Mono
.
error
(
new
CloudFoundryAuthorizationException
(
Reason
.
INVALID_SIGNATURE
,
"RSA Signature did not match content"
)))
.
then
();
}
private
boolean
hasValidKeyId
(
String
keyId
,
Map
<
String
,
String
>
tokenKeys
)
{
for
(
String
candidate
:
tokenKeys
.
keySet
())
{
if
(
keyId
.
equals
(
candidate
))
{
return
true
;
}
}
return
false
;
}
private
boolean
hasValidSignature
(
Token
token
,
String
key
)
{
try
{
PublicKey
publicKey
=
getPublicKey
(
key
);
Signature
signature
=
Signature
.
getInstance
(
"SHA256withRSA"
);
signature
.
initVerify
(
publicKey
);
signature
.
update
(
token
.
getContent
());
return
signature
.
verify
(
token
.
getSignature
());
}
catch
(
GeneralSecurityException
ex
)
{
return
false
;
}
}
private
PublicKey
getPublicKey
(
String
key
)
throws
NoSuchAlgorithmException
,
InvalidKeySpecException
{
key
=
key
.
replace
(
"-----BEGIN PUBLIC KEY-----\n"
,
""
);
key
=
key
.
replace
(
"-----END PUBLIC KEY-----"
,
""
);
key
=
key
.
trim
().
replace
(
"\n"
,
""
);
byte
[]
bytes
=
Base64Utils
.
decodeFromString
(
key
);
X509EncodedKeySpec
keySpec
=
new
X509EncodedKeySpec
(
bytes
);
return
KeyFactory
.
getInstance
(
"RSA"
).
generatePublic
(
keySpec
);
}
private
Mono
<
Void
>
validateExpiry
(
Token
token
)
{
long
currentTime
=
TimeUnit
.
MILLISECONDS
.
toSeconds
(
System
.
currentTimeMillis
());
if
(
currentTime
>
token
.
getExpiry
())
{
return
Mono
.
error
(
new
CloudFoundryAuthorizationException
(
Reason
.
TOKEN_EXPIRED
,
"Token expired"
));
}
return
Mono
.
empty
();
}
private
Mono
<
Void
>
validateIssuer
(
Token
token
)
{
return
this
.
securityService
.
getUaaUrl
()
.
map
(
uaaUrl
->
String
.
format
(
"%s/oauth/token"
,
uaaUrl
))
.
filter
(
issuerUri
->
issuerUri
.
equals
(
token
.
getIssuer
()))
.
switchIfEmpty
(
Mono
.
error
(
new
CloudFoundryAuthorizationException
(
Reason
.
INVALID_ISSUER
,
"Token issuer does not match"
)))
.
then
();
}
private
Mono
<
Void
>
validateAudience
(
Token
token
)
{
if
(!
token
.
getScope
().
contains
(
"actuator.read"
))
{
return
Mono
.
error
(
new
CloudFoundryAuthorizationException
(
Reason
.
INVALID_AUDIENCE
,
"Token does not have audience actuator"
));
}
return
Mono
.
empty
();
}
}
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryActuatorAutoConfiguration.java
→
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/
servlet/
CloudFoundryActuatorAutoConfiguration.java
View file @
d50fe887
...
...
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
;
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
servlet
;
import
java.util.Arrays
;
...
...
@@ -26,11 +26,9 @@ import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver;
import
org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer
;
import
org.springframework.boot.autoconfigure.AutoConfigureAfter
;
import
org.springframework.boot.autoconfigure.EnableAutoConfiguration
;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnBean
;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnClass
;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform
;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication
;
import
org.springframework.boot.autoconfigure.security.SecurityProperties
;
import
org.springframework.boot.cloud.CloudPlatform
;
import
org.springframework.boot.endpoint.web.EndpointMapping
;
...
...
@@ -45,7 +43,6 @@ import org.springframework.security.config.annotation.web.WebSecurityConfigurer;
import
org.springframework.security.config.annotation.web.builders.WebSecurity
;
import
org.springframework.security.web.util.matcher.AntPathRequestMatcher
;
import
org.springframework.web.cors.CorsConfiguration
;
import
org.springframework.web.servlet.DispatcherServlet
;
/**
* {@link EnableAutoConfiguration Auto-configuration} to expose actuator endpoints for
...
...
@@ -60,18 +57,9 @@ import org.springframework.web.servlet.DispatcherServlet;
@ConditionalOnCloudPlatform
(
CloudPlatform
.
CLOUD_FOUNDRY
)
public
class
CloudFoundryActuatorAutoConfiguration
{
/**
* Configuration for MVC endpoints on Cloud Foundry.
*/
@Configuration
@ConditionalOnWebApplication
(
type
=
ConditionalOnWebApplication
.
Type
.
SERVLET
)
@ConditionalOnClass
(
DispatcherServlet
.
class
)
@ConditionalOnBean
(
DispatcherServlet
.
class
)
static
class
MvcWebEndpointConfiguration
{
private
final
ApplicationContext
applicationContext
;
MvcWebEndpoint
Configuration
(
ApplicationContext
applicationContext
)
{
CloudFoundryActuatorAuto
Configuration
(
ApplicationContext
applicationContext
)
{
this
.
applicationContext
=
applicationContext
;
}
...
...
@@ -122,8 +110,6 @@ public class CloudFoundryActuatorAutoConfiguration {
return
corsConfiguration
;
}
}
/**
* {@link WebSecurityConfigurer} to tell Spring Security to ignore cloudfoundry
* specific paths. The Cloud foundry endpoints are protected by their own security
...
...
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundrySecurityInterceptor.java
→
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/
servlet/
CloudFoundrySecurityInterceptor.java
View file @
d50fe887
...
...
@@ -14,13 +14,18 @@
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
;
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
servlet
;
import
javax.servlet.http.HttpServletRequest
;
import
org.apache.commons.logging.Log
;
import
org.apache.commons.logging.LogFactory
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.SecurityResponse
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.Token
;
import
org.springframework.http.HttpMethod
;
import
org.springframework.http.HttpStatus
;
import
org.springframework.util.StringUtils
;
...
...
@@ -59,12 +64,12 @@ class CloudFoundrySecurityInterceptor {
try
{
if
(!
StringUtils
.
hasText
(
this
.
applicationId
))
{
throw
new
CloudFoundryAuthorizationException
(
CloudFoundryAuthorizationException
.
Reason
.
SERVICE_UNAVAILABLE
,
Reason
.
SERVICE_UNAVAILABLE
,
"Application id is not available"
);
}
if
(
this
.
cloudFoundrySecurityService
==
null
)
{
throw
new
CloudFoundryAuthorizationException
(
CloudFoundryAuthorizationException
.
Reason
.
SERVICE_UNAVAILABLE
,
Reason
.
SERVICE_UNAVAILABLE
,
"Cloud controller URL is not available"
);
}
if
(
HttpMethod
.
OPTIONS
.
matches
(
request
.
getMethod
()))
{
...
...
@@ -92,10 +97,10 @@ class CloudFoundrySecurityInterceptor {
.
getAccessLevel
(
token
.
toString
(),
this
.
applicationId
);
if
(!
accessLevel
.
isAccessAllowed
(
path
))
{
throw
new
CloudFoundryAuthorizationException
(
CloudFoundryAuthorizationException
.
Reason
.
ACCESS_DENIED
,
Reason
.
ACCESS_DENIED
,
"Access denied"
);
}
accessLevel
.
put
(
request
);
request
.
setAttribute
(
AccessLevel
.
REQUEST_ATTRIBUTE
,
accessLevel
);
}
private
Token
getToken
(
HttpServletRequest
request
)
{
...
...
@@ -104,42 +109,10 @@ class CloudFoundrySecurityInterceptor {
if
(
authorization
==
null
||
!
authorization
.
toLowerCase
().
startsWith
(
bearerPrefix
))
{
throw
new
CloudFoundryAuthorizationException
(
CloudFoundryAuthorizationException
.
Reason
.
MISSING_AUTHORIZATION
,
Reason
.
MISSING_AUTHORIZATION
,
"Authorization header is missing or invalid"
);
}
return
new
Token
(
authorization
.
substring
(
bearerPrefix
.
length
()));
}
/**
* Response from the security interceptor.
*/
static
class
SecurityResponse
{
private
final
HttpStatus
status
;
private
final
String
message
;
SecurityResponse
(
HttpStatus
status
)
{
this
(
status
,
null
);
}
SecurityResponse
(
HttpStatus
status
,
String
message
)
{
this
.
status
=
status
;
this
.
message
=
message
;
}
public
HttpStatus
getStatus
()
{
return
this
.
status
;
}
public
String
getMessage
()
{
return
this
.
message
;
}
static
SecurityResponse
success
()
{
return
new
SecurityResponse
(
HttpStatus
.
OK
);
}
}
}
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundrySecurityService.java
→
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/
servlet/
CloudFoundrySecurityService.java
View file @
d50fe887
...
...
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
;
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
servlet
;
import
java.net.URI
;
import
java.net.URISyntaxException
;
...
...
@@ -22,6 +22,8 @@ import java.util.HashMap;
import
java.util.List
;
import
java.util.Map
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason
;
import
org.springframework.boot.web.client.RestTemplateBuilder
;
import
org.springframework.http.HttpStatus
;
...
...
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointServletHandlerMapping.java
→
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/
servlet/
CloudFoundryWebEndpointServletHandlerMapping.java
View file @
d50fe887
...
...
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
;
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
servlet
;
import
java.lang.reflect.Method
;
import
java.util.Arrays
;
...
...
@@ -31,6 +31,8 @@ import javax.servlet.http.HttpServletResponse;
import
org.apache.commons.logging.Log
;
import
org.apache.commons.logging.LogFactory
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.SecurityResponse
;
import
org.springframework.boot.actuate.endpoint.EndpointInfo
;
import
org.springframework.boot.actuate.endpoint.OperationInvoker
;
import
org.springframework.boot.actuate.endpoint.ParameterMappingException
;
...
...
@@ -91,12 +93,12 @@ class CloudFoundryWebEndpointServletHandlerMapping
@ResponseBody
private
Map
<
String
,
Map
<
String
,
Link
>>
links
(
HttpServletRequest
request
,
HttpServletResponse
response
)
{
CloudFoundrySecurityInterceptor
.
SecurityResponse
securityResponse
=
this
.
securityInterceptor
SecurityResponse
securityResponse
=
this
.
securityInterceptor
.
preHandle
(
request
,
""
);
if
(!
securityResponse
.
getStatus
().
equals
(
HttpStatus
.
OK
))
{
sendFailureResponse
(
response
,
securityResponse
);
}
AccessLevel
accessLevel
=
AccessLevel
.
get
(
request
);
AccessLevel
accessLevel
=
(
AccessLevel
)
request
.
getAttribute
(
AccessLevel
.
REQUEST_ATTRIBUTE
);
Map
<
String
,
Link
>
links
=
this
.
endpointLinksResolver
.
resolveLinks
(
getEndpoints
(),
request
.
getRequestURL
().
toString
());
Map
<
String
,
Link
>
filteredLinks
=
new
LinkedHashMap
<>();
...
...
@@ -111,7 +113,7 @@ class CloudFoundryWebEndpointServletHandlerMapping
}
private
void
sendFailureResponse
(
HttpServletResponse
response
,
CloudFoundrySecurityInterceptor
.
SecurityResponse
securityResponse
)
{
SecurityResponse
securityResponse
)
{
try
{
response
.
sendError
(
securityResponse
.
getStatus
().
value
(),
securityResponse
.
getMessage
());
...
...
@@ -151,7 +153,7 @@ class CloudFoundryWebEndpointServletHandlerMapping
@ResponseBody
public
Object
handle
(
HttpServletRequest
request
,
@RequestBody
(
required
=
false
)
Map
<
String
,
String
>
body
)
{
CloudFoundrySecurityInterceptor
.
SecurityResponse
securityResponse
=
this
.
securityInterceptor
SecurityResponse
securityResponse
=
this
.
securityInterceptor
.
preHandle
(
request
,
this
.
endpointId
);
if
(!
securityResponse
.
getStatus
().
equals
(
HttpStatus
.
OK
))
{
return
failureResponse
(
securityResponse
);
...
...
@@ -173,7 +175,7 @@ class CloudFoundryWebEndpointServletHandlerMapping
}
private
Object
failureResponse
(
CloudFoundrySecurityInterceptor
.
SecurityResponse
response
)
{
SecurityResponse
response
)
{
return
handleResult
(
new
WebEndpointResponse
<>(
response
.
getMessage
(),
response
.
getStatus
().
value
()));
}
...
...
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/SkipSslVerificationHttpRequestFactory.java
→
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/
servlet/
SkipSslVerificationHttpRequestFactory.java
View file @
d50fe887
...
...
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
;
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
servlet
;
import
java.io.IOException
;
import
java.net.HttpURLConnection
;
...
...
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/TokenValidator.java
→
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/
servlet/
TokenValidator.java
View file @
d50fe887
...
...
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
;
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
servlet
;
import
java.security.GeneralSecurityException
;
import
java.security.KeyFactory
;
...
...
@@ -26,7 +26,9 @@ import java.security.spec.X509EncodedKeySpec;
import
java.util.Map
;
import
java.util.concurrent.TimeUnit
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.Token
;
import
org.springframework.util.Base64Utils
;
/**
...
...
@@ -34,13 +36,13 @@ import org.springframework.util.Base64Utils;
*
* @author Madhura Bhave
*/
class
TokenValidator
{
public
class
TokenValidator
{
private
final
CloudFoundrySecurityService
securityService
;
private
Map
<
String
,
String
>
tokenKeys
;
TokenValidator
(
CloudFoundrySecurityService
cloudFoundrySecurityService
)
{
public
TokenValidator
(
CloudFoundrySecurityService
cloudFoundrySecurityService
)
{
this
.
securityService
=
cloudFoundrySecurityService
;
}
...
...
spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories
View file @
d50fe887
...
...
@@ -4,7 +4,8 @@ org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.audit.AuditEventsEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.cassandra.CassandraHealthIndicatorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryActuatorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet.CloudFoundryActuatorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.cloudfoundry.reactive.ReactiveCloudFoundryActuatorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.condition.AutoConfigurationReportEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.context.ShutdownEndpointAutoConfiguration,\
...
...
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/AuthorizationExceptionMatcher.java
View file @
d50fe887
...
...
@@ -26,12 +26,12 @@ import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryA
*
* @author Madhura Bhave
*/
final
class
AuthorizationExceptionMatcher
{
public
final
class
AuthorizationExceptionMatcher
{
private
AuthorizationExceptionMatcher
()
{
}
static
Matcher
<?>
withReason
(
final
Reason
reason
)
{
public
static
Matcher
<?>
withReason
(
final
Reason
reason
)
{
return
new
CustomMatcher
<
Object
>(
"CloudFoundryAuthorizationException with "
+
reason
+
" reason"
)
{
...
...
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java
0 → 100644
View file @
d50fe887
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
reactive
;
import
java.util.Arrays
;
import
java.util.Collections
;
import
java.util.Map
;
import
java.util.function.BiConsumer
;
import
java.util.function.Consumer
;
import
org.junit.Test
;
import
org.mockito.BDDMockito
;
import
reactor.core.publisher.Mono
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason
;
import
org.springframework.boot.actuate.endpoint.ParameterMapper
;
import
org.springframework.boot.actuate.endpoint.annotation.Endpoint
;
import
org.springframework.boot.actuate.endpoint.annotation.ReadOperation
;
import
org.springframework.boot.actuate.endpoint.annotation.Selector
;
import
org.springframework.boot.actuate.endpoint.annotation.WriteOperation
;
import
org.springframework.boot.actuate.endpoint.cache.CachingConfiguration
;
import
org.springframework.boot.actuate.endpoint.convert.ConversionServiceParameterMapper
;
import
org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes
;
import
org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer
;
import
org.springframework.boot.endpoint.web.EndpointMapping
;
import
org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory
;
import
org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext
;
import
org.springframework.boot.web.reactive.context.ReactiveWebServerInitializedEvent
;
import
org.springframework.context.ApplicationContext
;
import
org.springframework.context.ApplicationListener
;
import
org.springframework.context.annotation.Bean
;
import
org.springframework.context.annotation.Configuration
;
import
org.springframework.context.annotation.Import
;
import
org.springframework.core.convert.support.DefaultConversionService
;
import
org.springframework.http.HttpStatus
;
import
org.springframework.http.MediaType
;
import
org.springframework.http.server.reactive.HttpHandler
;
import
org.springframework.test.web.reactive.server.WebTestClient
;
import
org.springframework.util.Base64Utils
;
import
org.springframework.web.cors.CorsConfiguration
;
import
org.springframework.web.reactive.config.EnableWebFlux
;
import
org.springframework.web.server.adapter.WebHttpHandlerBuilder
;
import
static
org
.
mockito
.
ArgumentMatchers
.
any
;
import
static
org
.
mockito
.
ArgumentMatchers
.
eq
;
import
static
org
.
mockito
.
BDDMockito
.
given
;
import
static
org
.
mockito
.
Mockito
.
mock
;
/**
* Tests for {@link CloudFoundryWebFluxEndpointHandlerMapping}.
*
* @author Madhura Bhave
*/
public
class
CloudFoundryWebFluxEndpointIntegrationTests
{
private
static
ReactiveTokenValidator
tokenValidator
=
mock
(
ReactiveTokenValidator
.
class
);
private
static
ReactiveCloudFoundrySecurityService
securityService
=
mock
(
ReactiveCloudFoundrySecurityService
.
class
);
@Test
public
void
operationWithSecurityInterceptorForbidden
()
throws
Exception
{
given
(
tokenValidator
.
validate
(
any
())).
willReturn
(
Mono
.
empty
());
given
(
securityService
.
getAccessLevel
(
any
(),
eq
(
"app-id"
)))
.
willReturn
(
Mono
.
just
(
AccessLevel
.
RESTRICTED
));
load
(
TestEndpointConfiguration
.
class
,
(
client
)
->
client
.
get
().
uri
(
"/cfApplication/test"
)
.
accept
(
MediaType
.
APPLICATION_JSON
)
.
header
(
"Authorization"
,
"bearer "
+
mockAccessToken
()).
exchange
()
.
expectStatus
().
isEqualTo
(
HttpStatus
.
FORBIDDEN
));
}
@Test
public
void
operationWithSecurityInterceptorSuccess
()
throws
Exception
{
given
(
tokenValidator
.
validate
(
any
())).
willReturn
(
Mono
.
empty
());
given
(
securityService
.
getAccessLevel
(
any
(),
eq
(
"app-id"
)))
.
willReturn
(
Mono
.
just
(
AccessLevel
.
FULL
));
load
(
TestEndpointConfiguration
.
class
,
(
client
)
->
client
.
get
().
uri
(
"/cfApplication/test"
)
.
accept
(
MediaType
.
APPLICATION_JSON
)
.
header
(
"Authorization"
,
"bearer "
+
mockAccessToken
()).
exchange
()
.
expectStatus
().
isEqualTo
(
HttpStatus
.
OK
));
}
@Test
public
void
responseToOptionsRequestIncludesCorsHeaders
()
{
load
(
TestEndpointConfiguration
.
class
,
(
client
)
->
client
.
options
().
uri
(
"/cfApplication/test"
)
.
accept
(
MediaType
.
APPLICATION_JSON
)
.
header
(
"Access-Control-Request-Method"
,
"POST"
)
.
header
(
"Origin"
,
"http://example.com"
).
exchange
().
expectStatus
()
.
isOk
().
expectHeader
()
.
valueEquals
(
"Access-Control-Allow-Origin"
,
"http://example.com"
)
.
expectHeader
()
.
valueEquals
(
"Access-Control-Allow-Methods"
,
"GET,POST"
));
}
@Test
public
void
linksToOtherEndpointsWithFullAccess
()
{
given
(
tokenValidator
.
validate
(
any
())).
willReturn
(
Mono
.
empty
());
given
(
securityService
.
getAccessLevel
(
any
(),
eq
(
"app-id"
)))
.
willReturn
(
Mono
.
just
(
AccessLevel
.
FULL
));
load
(
TestEndpointConfiguration
.
class
,
(
client
)
->
client
.
get
().
uri
(
"/cfApplication"
)
.
accept
(
MediaType
.
APPLICATION_JSON
)
.
header
(
"Authorization"
,
"bearer "
+
mockAccessToken
()).
exchange
()
.
expectStatus
().
isOk
().
expectBody
().
jsonPath
(
"_links.length()"
)
.
isEqualTo
(
5
).
jsonPath
(
"_links.self.href"
).
isNotEmpty
()
.
jsonPath
(
"_links.self.templated"
).
isEqualTo
(
false
)
.
jsonPath
(
"_links.info.href"
).
isNotEmpty
()
.
jsonPath
(
"_links.info.templated"
).
isEqualTo
(
false
)
.
jsonPath
(
"_links.env.href"
).
isNotEmpty
()
.
jsonPath
(
"_links.env.templated"
).
isEqualTo
(
false
)
.
jsonPath
(
"_links.test.href"
).
isNotEmpty
()
.
jsonPath
(
"_links.test.templated"
).
isEqualTo
(
false
)
.
jsonPath
(
"_links.test-part.href"
).
isNotEmpty
()
.
jsonPath
(
"_links.test-part.templated"
).
isEqualTo
(
true
));
}
@Test
public
void
linksToOtherEndpointsForbidden
()
{
CloudFoundryAuthorizationException
exception
=
new
CloudFoundryAuthorizationException
(
Reason
.
INVALID_TOKEN
,
"invalid-token"
);
BDDMockito
.
willThrow
(
exception
).
given
(
tokenValidator
).
validate
(
any
());
load
(
TestEndpointConfiguration
.
class
,
(
client
)
->
client
.
get
().
uri
(
"/cfApplication"
)
.
accept
(
MediaType
.
APPLICATION_JSON
)
.
header
(
"Authorization"
,
"bearer "
+
mockAccessToken
()).
exchange
()
.
expectStatus
().
isUnauthorized
());
}
@Test
public
void
linksToOtherEndpointsWithRestrictedAccess
()
{
given
(
tokenValidator
.
validate
(
any
())).
willReturn
(
Mono
.
empty
());
given
(
securityService
.
getAccessLevel
(
any
(),
eq
(
"app-id"
)))
.
willReturn
(
Mono
.
just
(
AccessLevel
.
RESTRICTED
));
load
(
TestEndpointConfiguration
.
class
,
(
client
)
->
client
.
get
().
uri
(
"/cfApplication"
)
.
accept
(
MediaType
.
APPLICATION_JSON
)
.
header
(
"Authorization"
,
"bearer "
+
mockAccessToken
()).
exchange
()
.
expectStatus
().
isOk
().
expectBody
().
jsonPath
(
"_links.length()"
)
.
isEqualTo
(
2
).
jsonPath
(
"_links.self.href"
).
isNotEmpty
()
.
jsonPath
(
"_links.self.templated"
).
isEqualTo
(
false
)
.
jsonPath
(
"_links.info.href"
).
isNotEmpty
()
.
jsonPath
(
"_links.info.templated"
).
isEqualTo
(
false
)
.
jsonPath
(
"_links.env"
).
doesNotExist
().
jsonPath
(
"_links.test"
)
.
doesNotExist
().
jsonPath
(
"_links.test-part"
).
doesNotExist
());
}
private
ReactiveWebServerApplicationContext
createApplicationContext
(
Class
<?>...
config
)
{
ReactiveWebServerApplicationContext
context
=
new
ReactiveWebServerApplicationContext
();
context
.
register
(
config
);
return
context
;
}
protected
int
getPort
(
ReactiveWebServerApplicationContext
context
)
{
return
context
.
getBean
(
CloudFoundryReactiveConfiguration
.
class
).
port
;
}
private
void
load
(
Class
<?>
configuration
,
Consumer
<
WebTestClient
>
clientConsumer
)
{
BiConsumer
<
ApplicationContext
,
WebTestClient
>
consumer
=
(
context
,
client
)
->
clientConsumer
.
accept
(
client
);
ReactiveWebServerApplicationContext
context
=
createApplicationContext
(
configuration
,
CloudFoundryReactiveConfiguration
.
class
);
context
.
refresh
();
try
{
consumer
.
accept
(
context
,
WebTestClient
.
bindToServer
()
.
baseUrl
(
"http://localhost:"
+
getPort
(
context
)).
build
());
}
finally
{
context
.
close
();
}
}
private
String
mockAccessToken
()
{
return
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0b3B0YWwu"
+
"Y29tIiwiZXhwIjoxNDI2NDIwODAwLCJhd2Vzb21lIjp0cnVlfQ."
+
Base64Utils
.
encodeToString
(
"signature"
.
getBytes
());
}
@Configuration
@EnableWebFlux
static
class
CloudFoundryReactiveConfiguration
{
private
int
port
;
@Bean
public
ReactiveCloudFoundrySecurityInterceptor
interceptor
()
{
return
new
ReactiveCloudFoundrySecurityInterceptor
(
tokenValidator
,
securityService
,
"app-id"
);
}
@Bean
public
EndpointMediaTypes
EndpointMediaTypes
()
{
return
new
EndpointMediaTypes
(
Collections
.
singletonList
(
"application/json"
),
Collections
.
singletonList
(
"application/json"
));
}
@Bean
public
CloudFoundryWebFluxEndpointHandlerMapping
cloudFoundryWebEndpointServletHandlerMapping
(
WebAnnotationEndpointDiscoverer
webEndpointDiscoverer
,
EndpointMediaTypes
endpointMediaTypes
,
ReactiveCloudFoundrySecurityInterceptor
interceptor
)
{
CorsConfiguration
corsConfiguration
=
new
CorsConfiguration
();
corsConfiguration
.
setAllowedOrigins
(
Arrays
.
asList
(
"http://example.com"
));
corsConfiguration
.
setAllowedMethods
(
Arrays
.
asList
(
"GET"
,
"POST"
));
return
new
CloudFoundryWebFluxEndpointHandlerMapping
(
new
EndpointMapping
(
"/cfApplication"
),
webEndpointDiscoverer
.
discoverEndpoints
(),
endpointMediaTypes
,
corsConfiguration
,
interceptor
);
}
@Bean
public
WebAnnotationEndpointDiscoverer
webEndpointDiscoverer
(
ApplicationContext
applicationContext
,
EndpointMediaTypes
endpointMediaTypes
)
{
ParameterMapper
parameterMapper
=
new
ConversionServiceParameterMapper
(
DefaultConversionService
.
getSharedInstance
());
return
new
WebAnnotationEndpointDiscoverer
(
applicationContext
,
parameterMapper
,
(
id
)
->
new
CachingConfiguration
(
0
),
endpointMediaTypes
,
(
id
)
->
id
);
}
@Bean
public
EndpointDelegate
endpointDelegate
()
{
return
mock
(
EndpointDelegate
.
class
);
}
@Bean
public
NettyReactiveWebServerFactory
netty
()
{
return
new
NettyReactiveWebServerFactory
(
0
);
}
@Bean
public
HttpHandler
httpHandler
(
ApplicationContext
applicationContext
)
{
return
WebHttpHandlerBuilder
.
applicationContext
(
applicationContext
).
build
();
}
@Bean
public
ApplicationListener
<
ReactiveWebServerInitializedEvent
>
serverInitializedListener
()
{
return
(
event
)
->
this
.
port
=
event
.
getWebServer
().
getPort
();
}
}
@Endpoint
(
id
=
"test"
)
static
class
TestEndpoint
{
private
final
EndpointDelegate
endpointDelegate
;
TestEndpoint
(
EndpointDelegate
endpointDelegate
)
{
this
.
endpointDelegate
=
endpointDelegate
;
}
@ReadOperation
public
Map
<
String
,
Object
>
readAll
()
{
return
Collections
.
singletonMap
(
"All"
,
true
);
}
@ReadOperation
public
Map
<
String
,
Object
>
readPart
(
@Selector
String
part
)
{
return
Collections
.
singletonMap
(
"part"
,
part
);
}
@WriteOperation
public
void
write
(
String
foo
,
String
bar
)
{
this
.
endpointDelegate
.
write
(
foo
,
bar
);
}
}
@Endpoint
(
id
=
"env"
)
static
class
TestEnvEndpoint
{
@ReadOperation
public
Map
<
String
,
Object
>
readAll
()
{
return
Collections
.
singletonMap
(
"All"
,
true
);
}
}
@Endpoint
(
id
=
"info"
)
static
class
TestInfoEndpoint
{
@ReadOperation
public
Map
<
String
,
Object
>
readAll
()
{
return
Collections
.
singletonMap
(
"All"
,
true
);
}
}
@Configuration
@Import
(
CloudFoundryReactiveConfiguration
.
class
)
protected
static
class
TestEndpointConfiguration
{
@Bean
public
TestEndpoint
testEndpoint
(
EndpointDelegate
endpointDelegate
)
{
return
new
TestEndpoint
(
endpointDelegate
);
}
@Bean
public
TestInfoEndpoint
testInfoEnvEndpoint
()
{
return
new
TestInfoEndpoint
();
}
@Bean
public
TestEnvEndpoint
testEnvEndpoint
()
{
return
new
TestEnvEndpoint
();
}
}
public
interface
EndpointDelegate
{
void
write
();
void
write
(
String
foo
,
String
bar
);
}
}
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java
0 → 100644
View file @
d50fe887
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
reactive
;
import
java.util.Arrays
;
import
java.util.List
;
import
org.junit.After
;
import
org.junit.Before
;
import
org.junit.Test
;
import
org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration
;
import
org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration
;
import
org.springframework.boot.actuate.endpoint.EndpointInfo
;
import
org.springframework.boot.actuate.endpoint.annotation.Endpoint
;
import
org.springframework.boot.actuate.endpoint.annotation.ReadOperation
;
import
org.springframework.boot.actuate.endpoint.http.ActuatorMediaType
;
import
org.springframework.boot.actuate.endpoint.web.WebEndpointOperation
;
import
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
;
import
org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration
;
import
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
;
import
org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration
;
import
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration
;
import
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration
;
import
org.springframework.boot.endpoint.web.EndpointMapping
;
import
org.springframework.boot.test.util.TestPropertyValues
;
import
org.springframework.boot.web.reactive.context.GenericReactiveWebApplicationContext
;
import
org.springframework.boot.web.reactive.function.client.WebClientCustomizer
;
import
org.springframework.context.annotation.Bean
;
import
org.springframework.context.annotation.Configuration
;
import
org.springframework.http.HttpMethod
;
import
org.springframework.mock.http.server.reactive.MockServerHttpRequest
;
import
org.springframework.mock.web.server.MockServerWebExchange
;
import
org.springframework.security.web.server.SecurityWebFilterChain
;
import
org.springframework.security.web.server.WebFilterChainProxy
;
import
org.springframework.test.util.ReflectionTestUtils
;
import
org.springframework.test.web.reactive.server.WebTestClient
;
import
org.springframework.web.cors.CorsConfiguration
;
import
static
org
.
assertj
.
core
.
api
.
Assertions
.
assertThat
;
import
static
org
.
mockito
.
Mockito
.
mock
;
/**
* Tests for {@link ReactiveCloudFoundryActuatorAutoConfiguration}.
*
* @author Madhura Bhave
*/
public
class
ReactiveCloudFoundryActuatorAutoConfigurationTests
{
private
GenericReactiveWebApplicationContext
context
;
@Before
public
void
setup
()
{
this
.
context
=
new
GenericReactiveWebApplicationContext
();
}
@After
public
void
close
()
{
if
(
this
.
context
!=
null
)
{
this
.
context
.
close
();
}
}
@Test
public
void
cloudFoundryPlatformActive
()
throws
Exception
{
setupContextWithCloudEnabled
();
this
.
context
.
refresh
();
CloudFoundryWebFluxEndpointHandlerMapping
handlerMapping
=
getHandlerMapping
();
EndpointMapping
endpointMapping
=
(
EndpointMapping
)
ReflectionTestUtils
.
getField
(
handlerMapping
,
"endpointMapping"
);
assertThat
(
endpointMapping
.
getPath
())
.
isEqualTo
(
"/cloudfoundryapplication"
);
CorsConfiguration
corsConfiguration
=
(
CorsConfiguration
)
ReflectionTestUtils
.
getField
(
handlerMapping
,
"corsConfiguration"
);
assertThat
(
corsConfiguration
.
getAllowedOrigins
()).
contains
(
"*"
);
assertThat
(
corsConfiguration
.
getAllowedMethods
()).
containsAll
(
Arrays
.
asList
(
HttpMethod
.
GET
.
name
(),
HttpMethod
.
POST
.
name
()));
assertThat
(
corsConfiguration
.
getAllowedHeaders
()).
containsAll
(
Arrays
.
asList
(
"Authorization"
,
"X-Cf-App-Instance"
,
"Content-Type"
));
}
@Test
public
void
cloudfoundryapplicationProducesActuatorMediaType
()
throws
Exception
{
setupContextWithCloudEnabled
();
this
.
context
.
refresh
();
WebTestClient
webTestClient
=
WebTestClient
.
bindToApplicationContext
(
this
.
context
).
build
();
webTestClient
.
get
().
uri
(
"/cloudfoundryapplication"
)
.
header
(
"Content-Type"
,
ActuatorMediaType
.
V2_JSON
+
";charset=UTF-8"
);
}
@Test
public
void
cloudFoundryPlatformActiveSetsApplicationId
()
throws
Exception
{
setupContextWithCloudEnabled
();
this
.
context
.
refresh
();
CloudFoundryWebFluxEndpointHandlerMapping
handlerMapping
=
getHandlerMapping
();
Object
interceptor
=
ReflectionTestUtils
.
getField
(
handlerMapping
,
"securityInterceptor"
);
String
applicationId
=
(
String
)
ReflectionTestUtils
.
getField
(
interceptor
,
"applicationId"
);
assertThat
(
applicationId
).
isEqualTo
(
"my-app-id"
);
}
@Test
public
void
cloudFoundryPlatformActiveSetsCloudControllerUrl
()
throws
Exception
{
setupContextWithCloudEnabled
();
this
.
context
.
refresh
();
CloudFoundryWebFluxEndpointHandlerMapping
handlerMapping
=
getHandlerMapping
();
Object
interceptor
=
ReflectionTestUtils
.
getField
(
handlerMapping
,
"securityInterceptor"
);
Object
interceptorSecurityService
=
ReflectionTestUtils
.
getField
(
interceptor
,
"cloudFoundrySecurityService"
);
String
cloudControllerUrl
=
(
String
)
ReflectionTestUtils
.
getField
(
interceptorSecurityService
,
"cloudControllerUrl"
);
assertThat
(
cloudControllerUrl
).
isEqualTo
(
"http://my-cloud-controller.com"
);
}
@Test
public
void
cloudFoundryPlatformActiveAndCloudControllerUrlNotPresent
()
throws
Exception
{
TestPropertyValues
.
of
(
"VCAP_APPLICATION:---"
,
"vcap.application.application_id:my-app-id"
)
.
applyTo
(
this
.
context
);
setupContext
();
this
.
context
.
refresh
();
CloudFoundryWebFluxEndpointHandlerMapping
handlerMapping
=
this
.
context
.
getBean
(
"cloudFoundryWebFluxEndpointHandlerMapping"
,
CloudFoundryWebFluxEndpointHandlerMapping
.
class
);
Object
securityInterceptor
=
ReflectionTestUtils
.
getField
(
handlerMapping
,
"securityInterceptor"
);
Object
interceptorSecurityService
=
ReflectionTestUtils
.
getField
(
securityInterceptor
,
"cloudFoundrySecurityService"
);
assertThat
(
interceptorSecurityService
).
isNull
();
}
@Test
public
void
cloudFoundryPathsIgnoredBySpringSecurity
()
throws
Exception
{
setupContextWithCloudEnabled
();
this
.
context
.
refresh
();
WebFilterChainProxy
chainProxy
=
this
.
context
.
getBean
(
WebFilterChainProxy
.
class
);
List
<
SecurityWebFilterChain
>
filters
=
(
List
<
SecurityWebFilterChain
>)
ReflectionTestUtils
.
getField
(
chainProxy
,
"filters"
);
Boolean
cfRequestMatches
=
filters
.
get
(
0
).
matches
(
MockServerWebExchange
.
from
(
MockServerHttpRequest
.
get
(
"/cloudfoundryapplication/my-path"
).
build
())).
block
();
Boolean
otherRequestMatches
=
filters
.
get
(
0
).
matches
(
MockServerWebExchange
.
from
(
MockServerHttpRequest
.
get
(
"/some-other-path"
).
build
())).
block
();
assertThat
(
cfRequestMatches
).
isTrue
();
assertThat
(
otherRequestMatches
).
isFalse
();
otherRequestMatches
=
filters
.
get
(
1
).
matches
(
MockServerWebExchange
.
from
(
MockServerHttpRequest
.
get
(
"/some-other-path"
).
build
())).
block
();
assertThat
(
otherRequestMatches
).
isTrue
();
}
@Test
public
void
cloudFoundryPlatformInactive
()
throws
Exception
{
setupContext
();
this
.
context
.
refresh
();
assertThat
(
this
.
context
.
containsBean
(
"cloudFoundryWebFluxEndpointHandlerMapping"
))
.
isFalse
();
}
@Test
public
void
cloudFoundryManagementEndpointsDisabled
()
throws
Exception
{
setupContextWithCloudEnabled
();
TestPropertyValues
.
of
(
"VCAP_APPLICATION=---"
,
"management.cloudfoundry.enabled:false"
)
.
applyTo
(
this
.
context
);
this
.
context
.
refresh
();
assertThat
(
this
.
context
.
containsBean
(
"cloudFoundryWebFluxEndpointHandlerMapping"
))
.
isFalse
();
}
@Test
public
void
allEndpointsAvailableUnderCloudFoundryWithoutEnablingWeb
()
throws
Exception
{
setupContextWithCloudEnabled
();
this
.
context
.
register
(
TestConfiguration
.
class
);
this
.
context
.
refresh
();
CloudFoundryWebFluxEndpointHandlerMapping
handlerMapping
=
getHandlerMapping
();
List
<
EndpointInfo
<
WebEndpointOperation
>>
endpoints
=
(
List
<
EndpointInfo
<
WebEndpointOperation
>>)
handlerMapping
.
getEndpoints
();
assertThat
(
endpoints
.
size
()).
isEqualTo
(
1
);
assertThat
(
endpoints
.
get
(
0
).
getId
()).
isEqualTo
(
"test"
);
}
@Test
public
void
endpointPathCustomizationIsNotApplied
()
throws
Exception
{
setupContextWithCloudEnabled
();
this
.
context
.
register
(
TestConfiguration
.
class
);
this
.
context
.
refresh
();
CloudFoundryWebFluxEndpointHandlerMapping
handlerMapping
=
getHandlerMapping
();
List
<
EndpointInfo
<
WebEndpointOperation
>>
endpoints
=
(
List
<
EndpointInfo
<
WebEndpointOperation
>>)
handlerMapping
.
getEndpoints
();
assertThat
(
endpoints
.
size
()).
isEqualTo
(
1
);
assertThat
(
endpoints
.
get
(
0
).
getOperations
()).
hasSize
(
1
);
assertThat
(
endpoints
.
get
(
0
).
getOperations
().
iterator
().
next
()
.
getRequestPredicate
().
getPath
()).
isEqualTo
(
"test"
);
}
private
void
setupContextWithCloudEnabled
()
{
TestPropertyValues
.
of
(
"VCAP_APPLICATION:---"
,
"vcap.application.application_id:my-app-id"
,
"vcap.application.cf_api:http://my-cloud-controller.com"
)
.
applyTo
(
this
.
context
);
setupContext
();
}
private
void
setupContext
()
{
this
.
context
.
register
(
ReactiveSecurityAutoConfiguration
.
class
,
WebFluxAutoConfiguration
.
class
,
JacksonAutoConfiguration
.
class
,
HttpMessageConvertersAutoConfiguration
.
class
,
PropertyPlaceholderAutoConfiguration
.
class
,
WebClientCustomizerConfig
.
class
,
WebClientAutoConfiguration
.
class
,
ManagementContextAutoConfiguration
.
class
,
EndpointAutoConfiguration
.
class
,
ReactiveCloudFoundryActuatorAutoConfiguration
.
class
);
}
private
CloudFoundryWebFluxEndpointHandlerMapping
getHandlerMapping
()
{
return
this
.
context
.
getBean
(
"cloudFoundryWebFluxEndpointHandlerMapping"
,
CloudFoundryWebFluxEndpointHandlerMapping
.
class
);
}
@Configuration
static
class
TestConfiguration
{
@Bean
public
TestEndpoint
testEndpoint
()
{
return
new
TestEndpoint
();
}
}
@Endpoint
(
id
=
"test"
)
static
class
TestEndpoint
{
@ReadOperation
public
String
hello
()
{
return
"hello world"
;
}
}
@Configuration
static
class
WebClientCustomizerConfig
{
@Bean
public
WebClientCustomizer
webClientCustomizer
()
{
return
mock
(
WebClientCustomizer
.
class
);
}
}
}
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptorTests.java
0 → 100644
View file @
d50fe887
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
reactive
;
import
org.junit.Before
;
import
org.junit.Test
;
import
org.mockito.BDDMockito
;
import
org.mockito.Mock
;
import
org.mockito.MockitoAnnotations
;
import
reactor.core.publisher.Mono
;
import
reactor.test.StepVerifier
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason
;
import
org.springframework.http.HttpHeaders
;
import
org.springframework.http.HttpStatus
;
import
org.springframework.mock.http.server.reactive.MockServerHttpRequest
;
import
org.springframework.mock.web.server.MockServerWebExchange
;
import
org.springframework.util.Base64Utils
;
import
static
org
.
assertj
.
core
.
api
.
Assertions
.
assertThat
;
import
static
org
.
mockito
.
ArgumentMatchers
.
any
;
/**
* Tests for {@link ReactiveCloudFoundrySecurityInterceptor}.
*
* @author Madhura Bhave
*/
public
class
ReactiveCloudFoundrySecurityInterceptorTests
{
@Mock
private
ReactiveTokenValidator
tokenValidator
;
@Mock
private
ReactiveCloudFoundrySecurityService
securityService
;
private
ReactiveCloudFoundrySecurityInterceptor
interceptor
;
@Before
public
void
setup
()
throws
Exception
{
MockitoAnnotations
.
initMocks
(
this
);
this
.
interceptor
=
new
ReactiveCloudFoundrySecurityInterceptor
(
this
.
tokenValidator
,
this
.
securityService
,
"my-app-id"
);
}
@Test
public
void
preHandleWhenRequestIsPreFlightShouldBeOk
()
throws
Exception
{
MockServerWebExchange
request
=
MockServerWebExchange
.
from
(
MockServerHttpRequest
.
options
(
"/a"
)
.
header
(
HttpHeaders
.
ORIGIN
,
"http://example.com"
)
.
header
(
HttpHeaders
.
ACCESS_CONTROL_REQUEST_METHOD
,
"GET"
)
.
build
());
StepVerifier
.
create
(
this
.
interceptor
.
preHandle
(
request
,
"/a"
))
.
consumeNextWith
(
response
->
assertThat
(
response
.
getStatus
()).
isEqualTo
(
HttpStatus
.
OK
))
.
verifyComplete
();
}
@Test
public
void
preHandleWhenTokenIsMissingShouldReturnMissingAuthorization
()
throws
Exception
{
MockServerWebExchange
request
=
MockServerWebExchange
.
from
(
MockServerHttpRequest
.
get
(
"/a"
)
.
build
());
StepVerifier
.
create
(
this
.
interceptor
.
preHandle
(
request
,
"/a"
))
.
consumeNextWith
(
response
->
assertThat
(
response
.
getStatus
())
.
isEqualTo
(
Reason
.
MISSING_AUTHORIZATION
.
getStatus
()))
.
verifyComplete
();
}
@Test
public
void
preHandleWhenTokenIsNotBearerShouldReturnMissingAuthorization
()
throws
Exception
{
MockServerWebExchange
request
=
MockServerWebExchange
.
from
(
MockServerHttpRequest
.
get
(
"/a"
)
.
header
(
HttpHeaders
.
AUTHORIZATION
,
mockAccessToken
())
.
build
());
StepVerifier
.
create
(
this
.
interceptor
.
preHandle
(
request
,
"/a"
))
.
consumeNextWith
(
response
->
assertThat
(
response
.
getStatus
())
.
isEqualTo
(
Reason
.
MISSING_AUTHORIZATION
.
getStatus
()))
.
verifyComplete
();
}
@Test
public
void
preHandleWhenApplicationIdIsNullShouldReturnError
()
throws
Exception
{
this
.
interceptor
=
new
ReactiveCloudFoundrySecurityInterceptor
(
this
.
tokenValidator
,
this
.
securityService
,
null
);
MockServerWebExchange
request
=
MockServerWebExchange
.
from
(
MockServerHttpRequest
.
get
(
"/a"
)
.
header
(
HttpHeaders
.
AUTHORIZATION
,
"bearer "
+
mockAccessToken
())
.
build
());
StepVerifier
.
create
(
this
.
interceptor
.
preHandle
(
request
,
"/a"
))
.
consumeErrorWith
(
throwable
->
assertThat
(((
CloudFoundryAuthorizationException
)
throwable
).
getReason
())
.
isEqualTo
(
Reason
.
SERVICE_UNAVAILABLE
))
.
verify
();
}
@Test
public
void
preHandleWhenCloudFoundrySecurityServiceIsNullShouldReturnError
()
throws
Exception
{
this
.
interceptor
=
new
ReactiveCloudFoundrySecurityInterceptor
(
this
.
tokenValidator
,
null
,
"my-app-id"
);
MockServerWebExchange
request
=
MockServerWebExchange
.
from
(
MockServerHttpRequest
.
get
(
"/a"
)
.
header
(
HttpHeaders
.
AUTHORIZATION
,
mockAccessToken
())
.
build
());
StepVerifier
.
create
(
this
.
interceptor
.
preHandle
(
request
,
"/a"
))
.
consumeErrorWith
(
throwable
->
assertThat
(((
CloudFoundryAuthorizationException
)
throwable
).
getReason
())
.
isEqualTo
(
Reason
.
SERVICE_UNAVAILABLE
))
.
verify
();
}
@Test
public
void
preHandleWhenAccessIsNotAllowedShouldReturnAccessDenied
()
throws
Exception
{
BDDMockito
.
given
(
this
.
securityService
.
getAccessLevel
(
mockAccessToken
(),
"my-app-id"
))
.
willReturn
(
Mono
.
just
(
AccessLevel
.
RESTRICTED
));
BDDMockito
.
given
(
this
.
tokenValidator
.
validate
(
any
()))
.
willReturn
(
Mono
.
empty
());
MockServerWebExchange
request
=
MockServerWebExchange
.
from
(
MockServerHttpRequest
.
get
(
"/a"
)
.
header
(
HttpHeaders
.
AUTHORIZATION
,
"bearer "
+
mockAccessToken
())
.
build
());
StepVerifier
.
create
(
this
.
interceptor
.
preHandle
(
request
,
"/a"
))
.
consumeNextWith
(
response
->
{
assertThat
(
response
.
getStatus
())
.
isEqualTo
(
Reason
.
ACCESS_DENIED
.
getStatus
());
})
.
verifyComplete
();
}
@Test
public
void
preHandleSuccessfulWithFullAccess
()
throws
Exception
{
String
accessToken
=
mockAccessToken
();
BDDMockito
.
given
(
this
.
securityService
.
getAccessLevel
(
accessToken
,
"my-app-id"
))
.
willReturn
(
Mono
.
just
(
AccessLevel
.
FULL
));
BDDMockito
.
given
(
this
.
tokenValidator
.
validate
(
any
()))
.
willReturn
(
Mono
.
empty
());
MockServerWebExchange
exchange
=
MockServerWebExchange
.
from
(
MockServerHttpRequest
.
get
(
"/a"
)
.
header
(
HttpHeaders
.
AUTHORIZATION
,
"bearer "
+
mockAccessToken
())
.
build
());
StepVerifier
.
create
(
this
.
interceptor
.
preHandle
(
exchange
,
"/a"
))
.
consumeNextWith
(
response
->
{
assertThat
(
response
.
getStatus
()).
isEqualTo
(
HttpStatus
.
OK
);
assertThat
((
AccessLevel
)
exchange
.
getAttribute
(
"cloudFoundryAccessLevel"
))
.
isEqualTo
(
AccessLevel
.
FULL
);
}).
verifyComplete
();
}
@Test
public
void
preHandleSuccessfulWithRestrictedAccess
()
throws
Exception
{
String
accessToken
=
mockAccessToken
();
BDDMockito
.
given
(
this
.
securityService
.
getAccessLevel
(
accessToken
,
"my-app-id"
))
.
willReturn
(
Mono
.
just
(
AccessLevel
.
RESTRICTED
));
BDDMockito
.
given
(
this
.
tokenValidator
.
validate
(
any
()))
.
willReturn
(
Mono
.
empty
());
MockServerWebExchange
exchange
=
MockServerWebExchange
.
from
(
MockServerHttpRequest
.
get
(
"/info"
)
.
header
(
HttpHeaders
.
AUTHORIZATION
,
"bearer "
+
mockAccessToken
())
.
build
());
StepVerifier
.
create
(
this
.
interceptor
.
preHandle
(
exchange
,
"info"
))
.
consumeNextWith
(
response
->
{
assertThat
(
response
.
getStatus
()).
isEqualTo
(
HttpStatus
.
OK
);
assertThat
((
AccessLevel
)
exchange
.
getAttribute
(
"cloudFoundryAccessLevel"
))
.
isEqualTo
(
AccessLevel
.
RESTRICTED
);
}).
verifyComplete
();
}
private
String
mockAccessToken
()
{
return
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0b3B0YWwu"
+
"Y29tIiwiZXhwIjoxNDI2NDIwODAwLCJhd2Vzb21lIjp0cnVlfQ."
+
Base64Utils
.
encodeToString
(
"signature"
.
getBytes
());
}
}
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityServiceTests.java
0 → 100644
View file @
d50fe887
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
reactive
;
import
java.util.function.Consumer
;
import
okhttp3.mockwebserver.MockResponse
;
import
okhttp3.mockwebserver.MockWebServer
;
import
okhttp3.mockwebserver.RecordedRequest
;
import
org.junit.After
;
import
org.junit.Before
;
import
org.junit.Rule
;
import
org.junit.Test
;
import
org.junit.rules.ExpectedException
;
import
reactor.test.StepVerifier
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason
;
import
org.springframework.http.HttpHeaders
;
import
org.springframework.web.reactive.function.client.WebClient
;
import
static
org
.
assertj
.
core
.
api
.
Assertions
.
assertThat
;
/**
* Tests for {@link ReactiveCloudFoundrySecurityService}.
*
* @author Madhura Bhave
*/
public
class
ReactiveCloudFoundrySecurityServiceTests
{
@Rule
public
ExpectedException
thrown
=
ExpectedException
.
none
();
private
static
final
String
CLOUD_CONTROLLER
=
"/my-cloud-controller.com"
;
private
static
final
String
CLOUD_CONTROLLER_PERMISSIONS
=
CLOUD_CONTROLLER
+
"/v2/apps/my-app-id/permissions"
;
private
static
final
String
UAA_URL
=
"http://my-cloud-controller.com/uaa"
;
private
ReactiveCloudFoundrySecurityService
securityService
;
private
MockWebServer
server
;
private
WebClient
.
Builder
builder
;
@Before
public
void
setup
()
throws
Exception
{
this
.
server
=
new
MockWebServer
();
this
.
builder
=
WebClient
.
builder
().
baseUrl
(
this
.
server
.
url
(
"/"
).
toString
());
this
.
securityService
=
new
ReactiveCloudFoundrySecurityService
(
this
.
builder
,
CLOUD_CONTROLLER
);
}
@After
public
void
shutdown
()
throws
Exception
{
this
.
server
.
shutdown
();
}
@Test
public
void
getAccessLevelWhenSpaceDeveloperShouldReturnFull
()
throws
Exception
{
String
responseBody
=
"{\"read_sensitive_data\": true,\"read_basic_data\": true}"
;
prepareResponse
(
response
->
response
.
setBody
(
responseBody
)
.
setHeader
(
"Content-Type"
,
"application/json"
));
StepVerifier
.
create
(
this
.
securityService
.
getAccessLevel
(
"my-access-token"
,
"my-app-id"
))
.
consumeNextWith
(
accessLevel
->
assertThat
(
accessLevel
).
isEqualTo
(
AccessLevel
.
FULL
))
.
expectComplete
().
verify
();
expectRequest
(
request
->
{
assertThat
(
request
.
getHeader
(
HttpHeaders
.
AUTHORIZATION
)).
isEqualTo
(
"bearer my-access-token"
);
assertThat
(
request
.
getPath
()).
isEqualTo
(
CLOUD_CONTROLLER_PERMISSIONS
);
});
}
@Test
public
void
getAccessLevelWhenNotSpaceDeveloperShouldReturnRestricted
()
throws
Exception
{
String
responseBody
=
"{\"read_sensitive_data\": false,\"read_basic_data\": true}"
;
prepareResponse
(
response
->
response
.
setBody
(
responseBody
)
.
setHeader
(
"Content-Type"
,
"application/json"
));
StepVerifier
.
create
(
this
.
securityService
.
getAccessLevel
(
"my-access-token"
,
"my-app-id"
))
.
consumeNextWith
(
accessLevel
->
assertThat
(
accessLevel
).
isEqualTo
(
AccessLevel
.
RESTRICTED
))
.
expectComplete
().
verify
();
expectRequest
(
request
->
{
assertThat
(
request
.
getHeader
(
HttpHeaders
.
AUTHORIZATION
)).
isEqualTo
(
"bearer my-access-token"
);
assertThat
(
request
.
getPath
()).
isEqualTo
(
CLOUD_CONTROLLER_PERMISSIONS
);
});
}
@Test
public
void
getAccessLevelWhenTokenIsNotValidShouldThrowException
()
throws
Exception
{
prepareResponse
(
response
->
response
.
setResponseCode
(
401
));
StepVerifier
.
create
(
this
.
securityService
.
getAccessLevel
(
"my-access-token"
,
"my-app-id"
))
.
consumeErrorWith
(
throwable
->
{
assertThat
(
throwable
).
isInstanceOf
(
CloudFoundryAuthorizationException
.
class
);
assertThat
(((
CloudFoundryAuthorizationException
)
throwable
).
getReason
()).
isEqualTo
(
Reason
.
INVALID_TOKEN
);
})
.
verify
();
expectRequest
(
request
->
{
assertThat
(
request
.
getHeader
(
HttpHeaders
.
AUTHORIZATION
)).
isEqualTo
(
"bearer my-access-token"
);
assertThat
(
request
.
getPath
()).
isEqualTo
(
CLOUD_CONTROLLER_PERMISSIONS
);
});
}
@Test
public
void
getAccessLevelWhenForbiddenShouldThrowException
()
throws
Exception
{
prepareResponse
(
response
->
response
.
setResponseCode
(
403
));
StepVerifier
.
create
(
this
.
securityService
.
getAccessLevel
(
"my-access-token"
,
"my-app-id"
))
.
consumeErrorWith
(
throwable
->
{
assertThat
(
throwable
).
isInstanceOf
(
CloudFoundryAuthorizationException
.
class
);
assertThat
(((
CloudFoundryAuthorizationException
)
throwable
).
getReason
()).
isEqualTo
(
Reason
.
ACCESS_DENIED
);
})
.
verify
();
expectRequest
(
request
->
{
assertThat
(
request
.
getHeader
(
HttpHeaders
.
AUTHORIZATION
)).
isEqualTo
(
"bearer my-access-token"
);
assertThat
(
request
.
getPath
()).
isEqualTo
(
CLOUD_CONTROLLER_PERMISSIONS
);
});
}
@Test
public
void
getAccessLevelWhenCloudControllerIsNotReachableThrowsException
()
throws
Exception
{
prepareResponse
(
response
->
response
.
setResponseCode
(
500
));
StepVerifier
.
create
(
this
.
securityService
.
getAccessLevel
(
"my-access-token"
,
"my-app-id"
))
.
consumeErrorWith
(
throwable
->
{
assertThat
(
throwable
).
isInstanceOf
(
CloudFoundryAuthorizationException
.
class
);
assertThat
(((
CloudFoundryAuthorizationException
)
throwable
).
getReason
()).
isEqualTo
(
Reason
.
SERVICE_UNAVAILABLE
);
})
.
verify
();
expectRequest
(
request
->
{
assertThat
(
request
.
getHeader
(
HttpHeaders
.
AUTHORIZATION
)).
isEqualTo
(
"bearer my-access-token"
);
assertThat
(
request
.
getPath
()).
isEqualTo
(
CLOUD_CONTROLLER_PERMISSIONS
);
});
}
@Test
public
void
fetchTokenKeysWhenSuccessfulShouldReturnListOfKeysFromUAA
()
throws
Exception
{
String
tokenKeyValue
=
"-----BEGIN PUBLIC KEY-----\n"
+
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0m59l2u9iDnMbrXHfqkO\n"
+
"rn2dVQ3vfBJqcDuFUK03d+1PZGbVlNCqnkpIJ8syFppW8ljnWweP7+LiWpRoz0I7\n"
+
"fYb3d8TjhV86Y997Fl4DBrxgM6KTJOuE/uxnoDhZQ14LgOU2ckXjOzOdTsnGMKQB\n"
+
"LCl0vpcXBtFLMaSbpv1ozi8h7DJyVZ6EnFQZUWGdgTMhDrmqevfx95U/16c5WBDO\n"
+
"kqwIn7Glry9n9Suxygbf8g5AzpWcusZgDLIIZ7JTUldBb8qU2a0Dl4mvLZOn4wPo\n"
+
"jfj9Cw2QICsc5+Pwf21fP+hzf+1WSRHbnYv8uanRO0gZ8ekGaghM/2H6gqJbo2nI\n"
+
"JwIDAQAB\n-----END PUBLIC KEY-----"
;
prepareResponse
(
response
->
{
response
.
setBody
(
"{\"token_endpoint\":\"/my-uaa.com\"}"
);
response
.
setHeader
(
"Content-Type"
,
"application/json"
);
});
String
responseBody
=
"{\"keys\" : [ {\"kid\":\"test-key\",\"value\" : \""
+
tokenKeyValue
.
replace
(
"\n"
,
"\\n"
)
+
"\"} ]}"
;
prepareResponse
(
response
->
{
response
.
setBody
(
responseBody
);
response
.
setHeader
(
"Content-Type"
,
"application/json"
);
});
StepVerifier
.
create
(
this
.
securityService
.
fetchTokenKeys
())
.
consumeNextWith
(
tokenKeys
->
assertThat
(
tokenKeys
.
get
(
"test-key"
)).
isEqualTo
(
tokenKeyValue
))
.
expectComplete
().
verify
();
expectRequest
(
request
->
assertThat
(
request
.
getPath
()).
isEqualTo
(
"/my-cloud-controller.com/info"
));
expectRequest
(
request
->
assertThat
(
request
.
getPath
()).
isEqualTo
(
"/my-uaa.com/token_keys"
));
}
@Test
public
void
fetchTokenKeysWhenNoKeysReturnedFromUAA
()
throws
Exception
{
prepareResponse
(
response
->
{
response
.
setBody
(
"{\"token_endpoint\":\"/my-uaa.com\"}"
);
response
.
setHeader
(
"Content-Type"
,
"application/json"
);
});
String
responseBody
=
"{\"keys\": []}"
;
prepareResponse
(
response
->
{
response
.
setBody
(
responseBody
);
response
.
setHeader
(
"Content-Type"
,
"application/json"
);
});
StepVerifier
.
create
(
this
.
securityService
.
fetchTokenKeys
())
.
consumeNextWith
(
tokenKeys
->
assertThat
(
tokenKeys
).
hasSize
(
0
))
.
expectComplete
().
verify
();
expectRequest
(
request
->
assertThat
(
request
.
getPath
()).
isEqualTo
(
"/my-cloud-controller.com/info"
));
expectRequest
(
request
->
assertThat
(
request
.
getPath
()).
isEqualTo
(
"/my-uaa.com/token_keys"
));
}
@Test
public
void
fetchTokenKeysWhenUnsuccessfulShouldThrowException
()
throws
Exception
{
prepareResponse
(
response
->
{
response
.
setBody
(
"{\"token_endpoint\":\"/my-uaa.com\"}"
);
response
.
setHeader
(
"Content-Type"
,
"application/json"
);
});
prepareResponse
(
response
->
{
response
.
setResponseCode
(
500
);
});
StepVerifier
.
create
(
this
.
securityService
.
fetchTokenKeys
())
.
consumeErrorWith
(
throwable
->
assertThat
(((
CloudFoundryAuthorizationException
)
throwable
)
.
getReason
()).
isEqualTo
(
Reason
.
SERVICE_UNAVAILABLE
))
.
verify
();
expectRequest
(
request
->
assertThat
(
request
.
getPath
()).
isEqualTo
(
"/my-cloud-controller.com/info"
));
expectRequest
(
request
->
assertThat
(
request
.
getPath
()).
isEqualTo
(
"/my-uaa.com/token_keys"
));
}
@Test
public
void
getUaaUrlShouldCallCloudControllerInfoOnlyOnce
()
throws
Exception
{
prepareResponse
(
response
->
{
response
.
setBody
(
"{\"token_endpoint\":\""
+
UAA_URL
+
"\"}"
);
response
.
setHeader
(
"Content-Type"
,
"application/json"
);
});
StepVerifier
.
create
(
this
.
securityService
.
getUaaUrl
())
.
consumeNextWith
(
uaaUrl
->
assertThat
(
uaaUrl
).
isEqualTo
(
UAA_URL
))
.
expectComplete
().
verify
();
//this.securityService.getUaaUrl().block(); //FIXME subscribe again to check that it isn't called again
expectRequest
(
request
->
assertThat
(
request
.
getPath
()).
isEqualTo
(
CLOUD_CONTROLLER
+
"/info"
));
expectRequestCount
(
1
);
}
@Test
public
void
getUaaUrlWhenCloudControllerUrlIsNotReachableShouldThrowException
()
throws
Exception
{
prepareResponse
(
response
->
response
.
setResponseCode
(
500
));
StepVerifier
.
create
(
this
.
securityService
.
getUaaUrl
())
.
consumeErrorWith
(
throwable
->
{
assertThat
(
throwable
).
isInstanceOf
(
CloudFoundryAuthorizationException
.
class
);
assertThat
(((
CloudFoundryAuthorizationException
)
throwable
).
getReason
()).
isEqualTo
(
Reason
.
SERVICE_UNAVAILABLE
);
})
.
verify
();
expectRequest
(
request
->
assertThat
(
request
.
getPath
()).
isEqualTo
(
CLOUD_CONTROLLER
+
"/info"
));
}
private
void
prepareResponse
(
Consumer
<
MockResponse
>
consumer
)
{
MockResponse
response
=
new
MockResponse
();
consumer
.
accept
(
response
);
this
.
server
.
enqueue
(
response
);
}
private
void
expectRequest
(
Consumer
<
RecordedRequest
>
consumer
)
throws
InterruptedException
{
consumer
.
accept
(
this
.
server
.
takeRequest
());
}
private
void
expectRequestCount
(
int
count
)
{
assertThat
(
count
).
isEqualTo
(
this
.
server
.
getRequestCount
());
}
}
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveTokenValidatorTests.java
0 → 100644
View file @
d50fe887
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
reactive
;
import
java.io.ByteArrayOutputStream
;
import
java.io.IOException
;
import
java.nio.charset.Charset
;
import
java.security.KeyFactory
;
import
java.security.NoSuchAlgorithmException
;
import
java.security.PrivateKey
;
import
java.security.Signature
;
import
java.security.spec.InvalidKeySpecException
;
import
java.security.spec.PKCS8EncodedKeySpec
;
import
java.util.Collections
;
import
java.util.Map
;
import
org.apache.commons.codec.binary.Base64
;
import
org.junit.Before
;
import
org.junit.Rule
;
import
org.junit.Test
;
import
org.junit.rules.ExpectedException
;
import
org.mockito.Mock
;
import
org.mockito.MockitoAnnotations
;
import
reactor.core.publisher.Mono
;
import
reactor.test.StepVerifier
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.Token
;
import
org.springframework.util.Base64Utils
;
import
org.springframework.util.StreamUtils
;
import
static
org
.
assertj
.
core
.
api
.
Assertions
.
assertThat
;
import
static
org
.
mockito
.
BDDMockito
.
given
;
/**
* Tests for {@link ReactiveTokenValidator}.
*
* @author Madhura Bhave
*/
public
class
ReactiveTokenValidatorTests
{
private
static
final
byte
[]
DOT
=
"."
.
getBytes
();
private
static
final
Charset
UTF_8
=
Charset
.
forName
(
"UTF-8"
);
@Rule
public
ExpectedException
thrown
=
ExpectedException
.
none
();
@Mock
private
ReactiveCloudFoundrySecurityService
securityService
;
private
ReactiveTokenValidator
tokenValidator
;
private
static
final
String
VALID_KEY
=
"-----BEGIN PUBLIC KEY-----\n"
+
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0m59l2u9iDnMbrXHfqkO\n"
+
"rn2dVQ3vfBJqcDuFUK03d+1PZGbVlNCqnkpIJ8syFppW8ljnWweP7+LiWpRoz0I7\n"
+
"fYb3d8TjhV86Y997Fl4DBrxgM6KTJOuE/uxnoDhZQ14LgOU2ckXjOzOdTsnGMKQB\n"
+
"LCl0vpcXBtFLMaSbpv1ozi8h7DJyVZ6EnFQZUWGdgTMhDrmqevfx95U/16c5WBDO\n"
+
"kqwIn7Glry9n9Suxygbf8g5AzpWcusZgDLIIZ7JTUldBb8qU2a0Dl4mvLZOn4wPo\n"
+
"jfj9Cw2QICsc5+Pwf21fP+hzf+1WSRHbnYv8uanRO0gZ8ekGaghM/2H6gqJbo2nI\n"
+
"JwIDAQAB\n-----END PUBLIC KEY-----"
;
private
static
final
String
INVALID_KEY
=
"-----BEGIN PUBLIC KEY-----\n"
+
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxzYuc22QSst/dS7geYYK\n"
+
"5l5kLxU0tayNdixkEQ17ix+CUcUbKIsnyftZxaCYT46rQtXgCaYRdJcbB3hmyrOa\n"
+
"vkhTpX79xJZnQmfuamMbZBqitvscxW9zRR9tBUL6vdi/0rpoUwPMEh8+Bw7CgYR0\n"
+
"FK0DhWYBNDfe9HKcyZEv3max8Cdq18htxjEsdYO0iwzhtKRXomBWTdhD5ykd/fAC\n"
+
"VTr4+KEY+IeLvubHVmLUhbE5NgWXxrRpGasDqzKhCTmsa2Ysf712rl57SlH0Wz/M\n"
+
"r3F7aM9YpErzeYLrl0GhQr9BVJxOvXcVd4kmY+XkiCcrkyS1cnghnllh+LCwQu1s\n"
+
"YwIDAQAB\n-----END PUBLIC KEY-----"
;
private
static
final
Map
<
String
,
String
>
INVALID_KEYS
=
Collections
.
singletonMap
(
"invalid-key"
,
INVALID_KEY
);
private
static
final
Map
<
String
,
String
>
VALID_KEYS
=
Collections
.
singletonMap
(
"valid-key"
,
VALID_KEY
);
@Before
public
void
setup
()
throws
Exception
{
MockitoAnnotations
.
initMocks
(
this
);
this
.
tokenValidator
=
new
ReactiveTokenValidator
(
this
.
securityService
);
}
@Test
public
void
validateTokenWhenKidValidationFailsShouldThrowException
()
throws
Exception
{
given
(
this
.
securityService
.
fetchTokenKeys
()).
willReturn
(
Mono
.
just
(
INVALID_KEYS
));
given
(
this
.
securityService
.
getUaaUrl
()).
willReturn
(
Mono
.
just
(
"http://localhost:8080/uaa"
));
String
header
=
"{\"alg\": \"RS256\", \"kid\": \"valid-key\",\"typ\": \"JWT\"}"
;
String
claims
=
"{\"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}"
;
StepVerifier
.
create
(
this
.
tokenValidator
.
validate
(
new
Token
(
getSignedToken
(
header
.
getBytes
(),
claims
.
getBytes
())))).
consumeErrorWith
(
throwable
->
{
assertThat
(
throwable
).
isExactlyInstanceOf
(
CloudFoundryAuthorizationException
.
class
);
assertThat
(((
CloudFoundryAuthorizationException
)
throwable
)
.
getReason
()).
isEqualTo
(
Reason
.
INVALID_KEY_ID
);
}).
verify
();
}
@Test
public
void
validateTokenWhenKidValidationSucceeds
()
throws
Exception
{
given
(
this
.
securityService
.
fetchTokenKeys
()).
willReturn
(
Mono
.
just
(
VALID_KEYS
));
given
(
this
.
securityService
.
getUaaUrl
()).
willReturn
(
Mono
.
just
(
"http://localhost:8080/uaa"
));
String
header
=
"{ \"alg\": \"RS256\", \"kid\": \"valid-key\",\"typ\": \"JWT\"}"
;
String
claims
=
"{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}"
;
StepVerifier
.
create
(
this
.
tokenValidator
.
validate
(
new
Token
(
getSignedToken
(
header
.
getBytes
(),
claims
.
getBytes
())))).
verifyComplete
();
}
@Test
public
void
validateTokenWhenSignatureInvalidShouldThrowException
()
throws
Exception
{
Map
<
String
,
String
>
KEYS
=
Collections
.
singletonMap
(
"valid-key"
,
INVALID_KEY
);
given
(
this
.
securityService
.
fetchTokenKeys
()).
willReturn
(
Mono
.
just
(
KEYS
));
given
(
this
.
securityService
.
getUaaUrl
()).
willReturn
(
Mono
.
just
(
"http://localhost:8080/uaa"
));
String
header
=
"{ \"alg\": \"RS256\", \"kid\": \"valid-key\",\"typ\": \"JWT\"}"
;
String
claims
=
"{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}"
;
StepVerifier
.
create
(
this
.
tokenValidator
.
validate
(
new
Token
(
getSignedToken
(
header
.
getBytes
(),
claims
.
getBytes
())))).
consumeErrorWith
(
throwable
->
{
assertThat
(
throwable
).
isExactlyInstanceOf
(
CloudFoundryAuthorizationException
.
class
);
assertThat
(((
CloudFoundryAuthorizationException
)
throwable
)
.
getReason
()).
isEqualTo
(
Reason
.
INVALID_SIGNATURE
);
}).
verify
();
}
@Test
public
void
validateTokenWhenTokenAlgorithmIsNotRS256ShouldThrowException
()
throws
Exception
{
given
(
this
.
securityService
.
fetchTokenKeys
()).
willReturn
(
Mono
.
just
(
VALID_KEYS
));
given
(
this
.
securityService
.
getUaaUrl
()).
willReturn
(
Mono
.
just
(
"http://localhost:8080/uaa"
));
String
header
=
"{ \"alg\": \"HS256\", \"kid\": \"valid-key\", \"typ\": \"JWT\"}"
;
String
claims
=
"{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}"
;
StepVerifier
.
create
(
this
.
tokenValidator
.
validate
(
new
Token
(
getSignedToken
(
header
.
getBytes
(),
claims
.
getBytes
())))).
consumeErrorWith
(
throwable
->
{
assertThat
(
throwable
).
isExactlyInstanceOf
(
CloudFoundryAuthorizationException
.
class
);
assertThat
(((
CloudFoundryAuthorizationException
)
throwable
)
.
getReason
()).
isEqualTo
(
Reason
.
UNSUPPORTED_TOKEN_SIGNING_ALGORITHM
);
}).
verify
();
}
@Test
public
void
validateTokenWhenExpiredShouldThrowException
()
throws
Exception
{
given
(
this
.
securityService
.
fetchTokenKeys
()).
willReturn
(
Mono
.
just
(
VALID_KEYS
));
given
(
this
.
securityService
.
getUaaUrl
()).
willReturn
(
Mono
.
just
(
"http://localhost:8080/uaa"
));
String
header
=
"{ \"alg\": \"RS256\", \"kid\": \"valid-key\", \"typ\": \"JWT\"}"
;
String
claims
=
"{ \"jti\": \"0236399c350c47f3ae77e67a75e75e7d\", \"exp\": 1477509977, \"scope\": [\"actuator.read\"]}"
;
StepVerifier
.
create
(
this
.
tokenValidator
.
validate
(
new
Token
(
getSignedToken
(
header
.
getBytes
(),
claims
.
getBytes
())))).
consumeErrorWith
(
throwable
->
{
assertThat
(
throwable
).
isExactlyInstanceOf
(
CloudFoundryAuthorizationException
.
class
);
assertThat
(((
CloudFoundryAuthorizationException
)
throwable
)
.
getReason
()).
isEqualTo
(
Reason
.
TOKEN_EXPIRED
);
}).
verify
();
}
@Test
public
void
validateTokenWhenIssuerIsNotValidShouldThrowException
()
throws
Exception
{
given
(
this
.
securityService
.
fetchTokenKeys
()).
willReturn
(
Mono
.
just
(
VALID_KEYS
));
given
(
this
.
securityService
.
getUaaUrl
()).
willReturn
(
Mono
.
just
(
"http://other-uaa.com"
));
String
header
=
"{ \"alg\": \"RS256\", \"kid\": \"valid-key\", \"typ\": \"JWT\", \"scope\": [\"actuator.read\"]}"
;
String
claims
=
"{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"foo.bar\"]}"
;
StepVerifier
.
create
(
this
.
tokenValidator
.
validate
(
new
Token
(
getSignedToken
(
header
.
getBytes
(),
claims
.
getBytes
())))).
consumeErrorWith
(
throwable
->
{
assertThat
(
throwable
).
isExactlyInstanceOf
(
CloudFoundryAuthorizationException
.
class
);
assertThat
(((
CloudFoundryAuthorizationException
)
throwable
)
.
getReason
()).
isEqualTo
(
Reason
.
INVALID_ISSUER
);
}).
verify
();
}
@Test
public
void
validateTokenWhenAudienceIsNotValidShouldThrowException
()
throws
Exception
{
given
(
this
.
securityService
.
fetchTokenKeys
()).
willReturn
(
Mono
.
just
(
VALID_KEYS
));
given
(
this
.
securityService
.
getUaaUrl
()).
willReturn
(
Mono
.
just
(
"http://localhost:8080/uaa"
));
String
header
=
"{ \"alg\": \"RS256\", \"kid\": \"valid-key\", \"typ\": \"JWT\"}"
;
String
claims
=
"{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"foo.bar\"]}"
;
StepVerifier
.
create
(
this
.
tokenValidator
.
validate
(
new
Token
(
getSignedToken
(
header
.
getBytes
(),
claims
.
getBytes
())))).
consumeErrorWith
(
throwable
->
{
assertThat
(
throwable
).
isExactlyInstanceOf
(
CloudFoundryAuthorizationException
.
class
);
assertThat
(((
CloudFoundryAuthorizationException
)
throwable
)
.
getReason
()).
isEqualTo
(
Reason
.
INVALID_AUDIENCE
);
}).
verify
();
}
private
String
getSignedToken
(
byte
[]
header
,
byte
[]
claims
)
throws
Exception
{
PrivateKey
privateKey
=
getPrivateKey
();
Signature
signature
=
Signature
.
getInstance
(
"SHA256WithRSA"
);
signature
.
initSign
(
privateKey
);
byte
[]
content
=
dotConcat
(
Base64Utils
.
encodeUrlSafe
(
header
),
Base64Utils
.
encode
(
claims
));
signature
.
update
(
content
);
byte
[]
crypto
=
signature
.
sign
();
byte
[]
token
=
dotConcat
(
Base64Utils
.
encodeUrlSafe
(
header
),
Base64Utils
.
encodeUrlSafe
(
claims
),
Base64Utils
.
encodeUrlSafe
(
crypto
));
return
new
String
(
token
,
UTF_8
);
}
private
PrivateKey
getPrivateKey
()
throws
InvalidKeySpecException
,
NoSuchAlgorithmException
{
String
signingKey
=
"-----BEGIN PRIVATE KEY-----\n"
+
"MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDSbn2Xa72IOcxu\n"
+
"tcd+qQ6ufZ1VDe98EmpwO4VQrTd37U9kZtWU0KqeSkgnyzIWmlbyWOdbB4/v4uJa\n"
+
"lGjPQjt9hvd3xOOFXzpj33sWXgMGvGAzopMk64T+7GegOFlDXguA5TZyReM7M51O\n"
+
"ycYwpAEsKXS+lxcG0UsxpJum/WjOLyHsMnJVnoScVBlRYZ2BMyEOuap69/H3lT/X\n"
+
"pzlYEM6SrAifsaWvL2f1K7HKBt/yDkDOlZy6xmAMsghnslNSV0FvypTZrQOXia8t\n"
+
"k6fjA+iN+P0LDZAgKxzn4/B/bV8/6HN/7VZJEdudi/y5qdE7SBnx6QZqCEz/YfqC\n"
+
"olujacgnAgMBAAECggEAc9X2tJ/OWWrXqinOg160gkELloJxTi8lAFsDbAGuAwpT\n"
+
"JcWl1KF5CmGBjsY/8ElNi2J9GJL1HOwcBhikCVNARD1DhF6RkB13mvquWwWtTMvt\n"
+
"eP8JWM19DIc+E+hw2rCuTGngqs7l4vTqpzBTNPtS2eiIJ1IsjsgvSEiAlk/wnW48\n"
+
"11cf6SQMQcT3HNTWrS+yLycEuWKb6Khh8RpD9D+i8w2+IspWz5lTP7BrKCUNsLOx\n"
+
"6+5T52HcaZ9z3wMnDqfqIKWl3h8M+q+HFQ4EN5BPWYV4fF7EOx7+Qf2fKDFPoTjC\n"
+
"VTWzDRNAA1xPqwdF7IdPVOXCdaUJDOhHeXZGaTNSwQKBgQDxb9UiR/Jh1R3muL7I\n"
+
"neIt1gXa0O+SK7NWYl4DkArYo7V81ztxI8r+xKEeu5zRZZkpaJHxOnd3VfADascw\n"
+
"UfALvxGxN2z42lE6zdhrmxZ3ma+akQFsv7NyXcBT00sdW+xmOiCaAj0cgxNOXiV3\n"
+
"sYOwUy3SqUIPO2obpb+KC5ALHwKBgQDfH+NSQ/jn89oVZ3lzUORa+Z+aL1TGsgzs\n"
+
"p7IG0MTEYiR9/AExYUwJab0M4PDXhumeoACMfkCFALNVhpch2nXZv7X5445yRgfD\n"
+
"ONY4WknecuA0rfCLTruNWnQ3RR+BXmd9jD/5igd9hEIawz3V+jCHvAtzI8/CZIBt\n"
+
"AArBs5kp+QKBgQCdxwN1n6baIDemK10iJWtFoPO6h4fH8h8EeMwPb/ZmlLVpnA4Q\n"
+
"Zd+mlkDkoJ5eiRKKaPfWuOqRZeuvj/wTq7g/NOIO+bWQ+rrSvuqLh5IrHpgPXmub\n"
+
"8bsHJhUlspMH4KagN6ROgOAG3fGj6Qp7KdpxRCpR3KJ66czxvGNrhxre6QKBgB+s\n"
+
"MCGiYnfSprd5G8VhyziazKwfYeJerfT+DQhopDXYVKPJnQW8cQW5C8wDNkzx6sHI\n"
+
"pqtK1K/MnKhcVaHJmAcT7qoNQlA4Xqu4qrgPIQNBvU/dDRNJVthG6c5aspEzrG8m\n"
+
"9IHgtRV9K8EOy/1O6YqrB9kNUVWf3JccdWpvqyNJAoGAORzJiQCOk4egbdcozDTo\n"
+
"4Tg4qk/03qpTy5k64DxkX1nJHu8V/hsKwq9Af7Fj/iHy2Av54BLPlBaGPwMi2bzB\n"
+
"gYjmUomvx/fqOTQks9Rc4PIMB43p6Rdj0sh+52SKPDR2eHbwsmpuQUXnAs20BPPI\n"
+
"J/OOn5zOs8yf26os0q3+JUM=\n-----END PRIVATE KEY-----"
;
String
privateKey
=
signingKey
.
replace
(
"-----BEGIN PRIVATE KEY-----\n"
,
""
);
privateKey
=
privateKey
.
replace
(
"-----END PRIVATE KEY-----"
,
""
);
byte
[]
pkcs8EncodedBytes
=
Base64
.
decodeBase64
(
privateKey
);
PKCS8EncodedKeySpec
keySpec
=
new
PKCS8EncodedKeySpec
(
pkcs8EncodedBytes
);
KeyFactory
keyFactory
=
KeyFactory
.
getInstance
(
"RSA"
);
return
keyFactory
.
generatePrivate
(
keySpec
);
}
private
byte
[]
dotConcat
(
byte
[]...
bytes
)
throws
IOException
{
ByteArrayOutputStream
result
=
new
ByteArrayOutputStream
();
for
(
int
i
=
0
;
i
<
bytes
.
length
;
i
++)
{
if
(
i
>
0
)
{
StreamUtils
.
copy
(
DOT
,
result
);
}
StreamUtils
.
copy
(
bytes
[
i
],
result
);
}
return
result
.
toByteArray
();
}
}
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryActuatorAutoConfigurationTests.java
→
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/
servlet/
CloudFoundryActuatorAutoConfigurationTests.java
View file @
d50fe887
...
...
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
;
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
servlet
;
import
java.util.Arrays
;
import
java.util.List
;
...
...
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryMvcWebEndpointIntegrationTests.java
→
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/
servlet/
CloudFoundryMvcWebEndpointIntegrationTests.java
View file @
d50fe887
...
...
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
;
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
servlet
;
import
java.util.Arrays
;
import
java.util.Collections
;
...
...
@@ -24,6 +24,9 @@ import java.util.function.Consumer;
import
org.junit.Test
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason
;
import
org.springframework.boot.actuate.endpoint.ParameterMapper
;
import
org.springframework.boot.actuate.endpoint.annotation.Endpoint
;
import
org.springframework.boot.actuate.endpoint.annotation.ReadOperation
;
...
...
@@ -127,7 +130,7 @@ public class CloudFoundryMvcWebEndpointIntegrationTests {
@Test
public
void
linksToOtherEndpointsForbidden
()
{
CloudFoundryAuthorizationException
exception
=
new
CloudFoundryAuthorizationException
(
CloudFoundryAuthorizationException
.
Reason
.
INVALID_TOKEN
,
"invalid-token"
);
Reason
.
INVALID_TOKEN
,
"invalid-token"
);
willThrow
(
exception
).
given
(
tokenValidator
).
validate
(
any
());
load
(
TestEndpointConfiguration
.
class
,
(
client
)
->
client
.
get
().
uri
(
"/cfApplication"
)
...
...
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundrySecurityInterceptorTests.java
→
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/
servlet/
CloudFoundrySecurityInterceptorTests.java
View file @
d50fe887
...
...
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
;
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
servlet
;
import
org.junit.Before
;
import
org.junit.Test
;
...
...
@@ -22,7 +22,10 @@ import org.mockito.ArgumentCaptor;
import
org.mockito.Mock
;
import
org.mockito.MockitoAnnotations
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.SecurityResponse
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.Token
;
import
org.springframework.http.HttpHeaders
;
import
org.springframework.http.HttpStatus
;
import
org.springframework.mock.web.MockHttpServletRequest
;
...
...
@@ -62,14 +65,14 @@ public class CloudFoundrySecurityInterceptorTests {
this
.
request
.
setMethod
(
"OPTIONS"
);
this
.
request
.
addHeader
(
HttpHeaders
.
ORIGIN
,
"http://example.com"
);
this
.
request
.
addHeader
(
HttpHeaders
.
ACCESS_CONTROL_REQUEST_METHOD
,
"GET"
);
CloudFoundrySecurityInterceptor
.
SecurityResponse
response
=
this
.
interceptor
SecurityResponse
response
=
this
.
interceptor
.
preHandle
(
this
.
request
,
"/a"
);
assertThat
(
response
.
getStatus
()).
isEqualTo
(
HttpStatus
.
OK
);
}
@Test
public
void
preHandleWhenTokenIsMissingShouldReturnFalse
()
throws
Exception
{
CloudFoundrySecurityInterceptor
.
SecurityResponse
response
=
this
.
interceptor
SecurityResponse
response
=
this
.
interceptor
.
preHandle
(
this
.
request
,
"/a"
);
assertThat
(
response
.
getStatus
())
.
isEqualTo
(
Reason
.
MISSING_AUTHORIZATION
.
getStatus
());
...
...
@@ -78,7 +81,7 @@ public class CloudFoundrySecurityInterceptorTests {
@Test
public
void
preHandleWhenTokenIsNotBearerShouldReturnFalse
()
throws
Exception
{
this
.
request
.
addHeader
(
"Authorization"
,
mockAccessToken
());
CloudFoundrySecurityInterceptor
.
SecurityResponse
response
=
this
.
interceptor
SecurityResponse
response
=
this
.
interceptor
.
preHandle
(
this
.
request
,
"/a"
);
assertThat
(
response
.
getStatus
())
.
isEqualTo
(
Reason
.
MISSING_AUTHORIZATION
.
getStatus
());
...
...
@@ -89,7 +92,7 @@ public class CloudFoundrySecurityInterceptorTests {
this
.
interceptor
=
new
CloudFoundrySecurityInterceptor
(
this
.
tokenValidator
,
this
.
securityService
,
null
);
this
.
request
.
addHeader
(
"Authorization"
,
"bearer "
+
mockAccessToken
());
CloudFoundrySecurityInterceptor
.
SecurityResponse
response
=
this
.
interceptor
SecurityResponse
response
=
this
.
interceptor
.
preHandle
(
this
.
request
,
"/a"
);
assertThat
(
response
.
getStatus
())
.
isEqualTo
(
Reason
.
SERVICE_UNAVAILABLE
.
getStatus
());
...
...
@@ -101,7 +104,7 @@ public class CloudFoundrySecurityInterceptorTests {
this
.
interceptor
=
new
CloudFoundrySecurityInterceptor
(
this
.
tokenValidator
,
null
,
"my-app-id"
);
this
.
request
.
addHeader
(
"Authorization"
,
"bearer "
+
mockAccessToken
());
CloudFoundrySecurityInterceptor
.
SecurityResponse
response
=
this
.
interceptor
SecurityResponse
response
=
this
.
interceptor
.
preHandle
(
this
.
request
,
"/a"
);
assertThat
(
response
.
getStatus
())
.
isEqualTo
(
Reason
.
SERVICE_UNAVAILABLE
.
getStatus
());
...
...
@@ -113,7 +116,7 @@ public class CloudFoundrySecurityInterceptorTests {
this
.
request
.
addHeader
(
"Authorization"
,
"bearer "
+
accessToken
);
given
(
this
.
securityService
.
getAccessLevel
(
accessToken
,
"my-app-id"
))
.
willReturn
(
AccessLevel
.
RESTRICTED
);
CloudFoundrySecurityInterceptor
.
SecurityResponse
response
=
this
.
interceptor
SecurityResponse
response
=
this
.
interceptor
.
preHandle
(
this
.
request
,
"/a"
);
assertThat
(
response
.
getStatus
()).
isEqualTo
(
Reason
.
ACCESS_DENIED
.
getStatus
());
}
...
...
@@ -124,7 +127,7 @@ public class CloudFoundrySecurityInterceptorTests {
this
.
request
.
addHeader
(
"Authorization"
,
"Bearer "
+
accessToken
);
given
(
this
.
securityService
.
getAccessLevel
(
accessToken
,
"my-app-id"
))
.
willReturn
(
AccessLevel
.
FULL
);
CloudFoundrySecurityInterceptor
.
SecurityResponse
response
=
this
.
interceptor
SecurityResponse
response
=
this
.
interceptor
.
preHandle
(
this
.
request
,
"/a"
);
ArgumentCaptor
<
Token
>
tokenArgumentCaptor
=
ArgumentCaptor
.
forClass
(
Token
.
class
);
verify
(
this
.
tokenValidator
).
validate
(
tokenArgumentCaptor
.
capture
());
...
...
@@ -141,7 +144,7 @@ public class CloudFoundrySecurityInterceptorTests {
this
.
request
.
addHeader
(
"Authorization"
,
"Bearer "
+
accessToken
);
given
(
this
.
securityService
.
getAccessLevel
(
accessToken
,
"my-app-id"
))
.
willReturn
(
AccessLevel
.
RESTRICTED
);
CloudFoundrySecurityInterceptor
.
SecurityResponse
response
=
this
.
interceptor
SecurityResponse
response
=
this
.
interceptor
.
preHandle
(
this
.
request
,
"info"
);
ArgumentCaptor
<
Token
>
tokenArgumentCaptor
=
ArgumentCaptor
.
forClass
(
Token
.
class
);
verify
(
this
.
tokenValidator
).
validate
(
tokenArgumentCaptor
.
capture
());
...
...
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundrySecurityServiceTests.java
→
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/
servlet/
CloudFoundrySecurityServiceTests.java
View file @
d50fe887
...
...
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
;
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
servlet
;
import
java.util.Map
;
...
...
@@ -23,6 +23,8 @@ import org.junit.Rule;
import
org.junit.Test
;
import
org.junit.rules.ExpectedException
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.AuthorizationExceptionMatcher
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason
;
import
org.springframework.boot.test.web.client.MockServerRestTemplateCustomizer
;
import
org.springframework.boot.web.client.RestTemplateBuilder
;
...
...
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/SkipSslVerificationHttpRequestFactoryTests.java
→
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/
servlet/
SkipSslVerificationHttpRequestFactoryTests.java
View file @
d50fe887
...
...
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
;
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
servlet
;
import
javax.net.ssl.SSLHandshakeException
;
...
...
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/TokenValidatorTests.java
→
spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/
servlet/
TokenValidatorTests.java
View file @
d50fe887
...
...
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
;
package
org
.
springframework
.
boot
.
actuate
.
autoconfigure
.
cloudfoundry
.
servlet
;
import
java.io.ByteArrayOutputStream
;
import
java.io.IOException
;
...
...
@@ -37,7 +37,9 @@ import org.mockito.Mock;
import
org.mockito.Mockito
;
import
org.mockito.MockitoAnnotations
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.AuthorizationExceptionMatcher
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason
;
import
org.springframework.boot.actuate.autoconfigure.cloudfoundry.Token
;
import
org.springframework.test.util.ReflectionTestUtils
;
import
org.springframework.util.Base64Utils
;
import
org.springframework.util.StreamUtils
;
...
...
spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java
0 → 100644
View file @
d50fe887
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
actuate
.
endpoint
.
web
.
reactive
;
import
java.lang.reflect.Method
;
import
java.util.Collection
;
import
java.util.Map
;
import
reactor.core.publisher.Mono
;
import
reactor.core.publisher.MonoSink
;
import
reactor.core.scheduler.Schedulers
;
import
org.springframework.boot.actuate.endpoint.EndpointInfo
;
import
org.springframework.boot.actuate.endpoint.OperationInvoker
;
import
org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes
;
import
org.springframework.boot.actuate.endpoint.web.OperationRequestPredicate
;
import
org.springframework.boot.actuate.endpoint.web.WebEndpointOperation
;
import
org.springframework.boot.endpoint.web.EndpointMapping
;
import
org.springframework.util.StringUtils
;
import
org.springframework.web.bind.annotation.RequestMethod
;
import
org.springframework.web.cors.CorsConfiguration
;
import
org.springframework.web.reactive.HandlerMapping
;
import
org.springframework.web.reactive.result.condition.ConsumesRequestCondition
;
import
org.springframework.web.reactive.result.condition.PatternsRequestCondition
;
import
org.springframework.web.reactive.result.condition.ProducesRequestCondition
;
import
org.springframework.web.reactive.result.condition.RequestMethodsRequestCondition
;
import
org.springframework.web.reactive.result.method.RequestMappingInfo
;
import
org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping
;
import
org.springframework.web.util.pattern.PathPatternParser
;
/**
* A custom {@link HandlerMapping} that makes web endpoints available over HTTP using
* Spring WebFlux.
*
* @author Andy Wilkinson
* @author Madhura Bhave
*/
public
abstract
class
AbstractWebFluxEndpointHandlerMapping
extends
RequestMappingInfoHandlerMapping
{
private
static
final
PathPatternParser
pathPatternParser
=
new
PathPatternParser
();
private
final
EndpointMapping
endpointMapping
;
private
final
Collection
<
EndpointInfo
<
WebEndpointOperation
>>
webEndpoints
;
private
final
EndpointMediaTypes
endpointMediaTypes
;
private
final
CorsConfiguration
corsConfiguration
;
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointMapping the base mapping for all endpoints
* @param collection the web endpoints
* @param endpointMediaTypes media types consumed and produced by the endpoints
*/
public
AbstractWebFluxEndpointHandlerMapping
(
EndpointMapping
endpointMapping
,
Collection
<
EndpointInfo
<
WebEndpointOperation
>>
collection
,
EndpointMediaTypes
endpointMediaTypes
)
{
this
(
endpointMapping
,
collection
,
endpointMediaTypes
,
null
);
}
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointMapping the base mapping for all endpoints
* @param webEndpoints the web endpoints
* @param endpointMediaTypes media types consumed and produced by the endpoints
* @param corsConfiguration the CORS configuration for the endpoints
*/
public
AbstractWebFluxEndpointHandlerMapping
(
EndpointMapping
endpointMapping
,
Collection
<
EndpointInfo
<
WebEndpointOperation
>>
webEndpoints
,
EndpointMediaTypes
endpointMediaTypes
,
CorsConfiguration
corsConfiguration
)
{
this
.
endpointMapping
=
endpointMapping
;
this
.
webEndpoints
=
webEndpoints
;
this
.
endpointMediaTypes
=
endpointMediaTypes
;
this
.
corsConfiguration
=
corsConfiguration
;
setOrder
(-
100
);
}
@Override
protected
void
initHandlerMethods
()
{
this
.
webEndpoints
.
stream
()
.
flatMap
((
webEndpoint
)
->
webEndpoint
.
getOperations
().
stream
())
.
forEach
(
this
::
registerMappingForOperation
);
if
(
StringUtils
.
hasText
(
this
.
endpointMapping
.
getPath
()))
{
registerLinksMapping
();
}
}
private
void
registerLinksMapping
()
{
registerMapping
(
new
RequestMappingInfo
(
new
PatternsRequestCondition
(
pathPatternParser
.
parse
(
this
.
endpointMapping
.
getPath
())),
new
RequestMethodsRequestCondition
(
RequestMethod
.
GET
),
null
,
null
,
null
,
new
ProducesRequestCondition
(
this
.
endpointMediaTypes
.
getProduced
()
.
toArray
(
new
String
[
this
.
endpointMediaTypes
.
getProduced
().
size
()])),
null
),
this
,
getLinks
());
}
protected
RequestMappingInfo
createRequestMappingInfo
(
WebEndpointOperation
operationInfo
)
{
OperationRequestPredicate
requestPredicate
=
operationInfo
.
getRequestPredicate
();
PatternsRequestCondition
patterns
=
new
PatternsRequestCondition
(
pathPatternParser
.
parse
(
this
.
endpointMapping
.
createSubPath
(
requestPredicate
.
getPath
())));
RequestMethodsRequestCondition
methods
=
new
RequestMethodsRequestCondition
(
RequestMethod
.
valueOf
(
requestPredicate
.
getHttpMethod
().
name
()));
ConsumesRequestCondition
consumes
=
new
ConsumesRequestCondition
(
toStringArray
(
requestPredicate
.
getConsumes
()));
ProducesRequestCondition
produces
=
new
ProducesRequestCondition
(
toStringArray
(
requestPredicate
.
getProduces
()));
return
new
RequestMappingInfo
(
null
,
patterns
,
methods
,
null
,
null
,
consumes
,
produces
,
null
);
}
private
String
[]
toStringArray
(
Collection
<
String
>
collection
)
{
return
collection
.
toArray
(
new
String
[
collection
.
size
()]);
}
@Override
protected
CorsConfiguration
initCorsConfiguration
(
Object
handler
,
Method
method
,
RequestMappingInfo
mapping
)
{
return
this
.
corsConfiguration
;
}
public
Collection
<
EndpointInfo
<
WebEndpointOperation
>>
getEndpoints
()
{
return
this
.
webEndpoints
;
}
protected
abstract
Method
getLinks
();
protected
abstract
void
registerMappingForOperation
(
WebEndpointOperation
operation
);
@Override
protected
boolean
isHandler
(
Class
<?>
beanType
)
{
return
false
;
}
@Override
protected
RequestMappingInfo
getMappingForMethod
(
Method
method
,
Class
<?>
handlerType
)
{
return
null
;
}
/**
* An {@link OperationInvoker} that performs the invocation of a blocking operation on
* a separate thread using Reactor's {@link Schedulers#elastic() elastic scheduler}.
*/
protected
static
final
class
ElasticSchedulerOperationInvoker
implements
OperationInvoker
{
private
final
OperationInvoker
delegate
;
public
ElasticSchedulerOperationInvoker
(
OperationInvoker
delegate
)
{
this
.
delegate
=
delegate
;
}
@Override
public
Object
invoke
(
Map
<
String
,
Object
>
arguments
)
{
return
Mono
.
create
((
sink
)
->
Schedulers
.
elastic
()
.
schedule
(()
->
invoke
(
arguments
,
sink
)));
}
private
void
invoke
(
Map
<
String
,
Object
>
arguments
,
MonoSink
<
Object
>
sink
)
{
try
{
Object
result
=
this
.
delegate
.
invoke
(
arguments
);
sink
.
success
(
result
);
}
catch
(
Exception
ex
)
{
sink
.
error
(
ex
);
}
}
}
}
spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointHandlerMapping.java
View file @
d50fe887
...
...
@@ -24,8 +24,6 @@ import java.util.Map;
import
org.reactivestreams.Publisher
;
import
reactor.core.publisher.Mono
;
import
reactor.core.publisher.MonoSink
;
import
reactor.core.scheduler.Schedulers
;
import
org.springframework.beans.factory.InitializingBean
;
import
org.springframework.boot.actuate.endpoint.EndpointInfo
;
...
...
@@ -36,7 +34,6 @@ import org.springframework.boot.actuate.endpoint.ParametersMissingException;
import
org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver
;
import
org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes
;
import
org.springframework.boot.actuate.endpoint.web.Link
;
import
org.springframework.boot.actuate.endpoint.web.OperationRequestPredicate
;
import
org.springframework.boot.actuate.endpoint.web.WebEndpointOperation
;
import
org.springframework.boot.actuate.endpoint.web.WebEndpointResponse
;
import
org.springframework.boot.endpoint.web.EndpointMapping
;
...
...
@@ -45,21 +42,12 @@ import org.springframework.http.HttpStatus;
import
org.springframework.http.ResponseEntity
;
import
org.springframework.http.server.reactive.ServerHttpRequest
;
import
org.springframework.util.ReflectionUtils
;
import
org.springframework.util.StringUtils
;
import
org.springframework.web.bind.annotation.RequestBody
;
import
org.springframework.web.bind.annotation.RequestMethod
;
import
org.springframework.web.bind.annotation.ResponseBody
;
import
org.springframework.web.cors.CorsConfiguration
;
import
org.springframework.web.reactive.HandlerMapping
;
import
org.springframework.web.reactive.result.condition.ConsumesRequestCondition
;
import
org.springframework.web.reactive.result.condition.PatternsRequestCondition
;
import
org.springframework.web.reactive.result.condition.ProducesRequestCondition
;
import
org.springframework.web.reactive.result.condition.RequestMethodsRequestCondition
;
import
org.springframework.web.reactive.result.method.RequestMappingInfo
;
import
org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping
;
import
org.springframework.web.server.ServerWebExchange
;
import
org.springframework.web.util.UriComponentsBuilder
;
import
org.springframework.web.util.pattern.PathPatternParser
;
/**
* A custom {@link HandlerMapping} that makes web endpoints available over HTTP using
...
...
@@ -68,10 +56,7 @@ import org.springframework.web.util.pattern.PathPatternParser;
* @author Andy Wilkinson
* @since 2.0.0
*/
public
class
WebFluxEndpointHandlerMapping
extends
RequestMappingInfoHandlerMapping
implements
InitializingBean
{
private
static
final
PathPatternParser
pathPatternParser
=
new
PathPatternParser
();
public
class
WebFluxEndpointHandlerMapping
extends
AbstractWebFluxEndpointHandlerMapping
implements
InitializingBean
{
private
final
Method
handleRead
=
ReflectionUtils
.
findMethod
(
ReadOperationHandler
.
class
,
"handle"
,
ServerWebExchange
.
class
);
...
...
@@ -84,14 +69,6 @@ public class WebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapp
private
final
EndpointLinksResolver
endpointLinksResolver
=
new
EndpointLinksResolver
();
private
final
EndpointMapping
endpointMapping
;
private
final
Collection
<
EndpointInfo
<
WebEndpointOperation
>>
webEndpoints
;
private
final
EndpointMediaTypes
endpointMediaTypes
;
private
final
CorsConfiguration
corsConfiguration
;
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
...
...
@@ -116,45 +93,17 @@ public class WebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapp
public
WebFluxEndpointHandlerMapping
(
EndpointMapping
endpointMapping
,
Collection
<
EndpointInfo
<
WebEndpointOperation
>>
webEndpoints
,
EndpointMediaTypes
endpointMediaTypes
,
CorsConfiguration
corsConfiguration
)
{
this
.
endpointMapping
=
endpointMapping
;
this
.
webEndpoints
=
webEndpoints
;
this
.
endpointMediaTypes
=
endpointMediaTypes
;
this
.
corsConfiguration
=
corsConfiguration
;
super
(
endpointMapping
,
webEndpoints
,
endpointMediaTypes
,
corsConfiguration
);
setOrder
(-
100
);
}
@Override
protected
void
initHandlerMethods
()
{
this
.
webEndpoints
.
stream
()
.
flatMap
((
webEndpoint
)
->
webEndpoint
.
getOperations
().
stream
())
.
forEach
(
this
::
registerMappingForOperation
);
if
(
StringUtils
.
hasText
(
this
.
endpointMapping
.
getPath
()))
{
registerLinksMapping
();
}
}
private
void
registerLinksMapping
()
{
registerMapping
(
new
RequestMappingInfo
(
new
PatternsRequestCondition
(
pathPatternParser
.
parse
(
this
.
endpointMapping
.
getPath
())),
new
RequestMethodsRequestCondition
(
RequestMethod
.
GET
),
null
,
null
,
null
,
new
ProducesRequestCondition
(
this
.
endpointMediaTypes
.
getProduced
()
.
toArray
(
new
String
[
this
.
endpointMediaTypes
.
getProduced
().
size
()])),
null
),
this
,
this
.
links
);
protected
Method
getLinks
()
{
return
this
.
links
;
}
@Override
protected
CorsConfiguration
initCorsConfiguration
(
Object
handler
,
Method
method
,
RequestMappingInfo
mapping
)
{
return
this
.
corsConfiguration
;
}
private
void
registerMappingForOperation
(
WebEndpointOperation
operation
)
{
protected
void
registerMappingForOperation
(
WebEndpointOperation
operation
)
{
OperationType
operationType
=
operation
.
getType
();
OperationInvoker
operationInvoker
=
operation
.
getInvoker
();
if
(
operation
.
isBlocking
())
{
...
...
@@ -162,50 +111,19 @@ public class WebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapp
}
registerMapping
(
createRequestMappingInfo
(
operation
),
operationType
==
OperationType
.
WRITE
?
new
WriteOperationHandler
(
operationInvoker
)
:
new
ReadOperationHandler
(
operationInvoker
),
?
new
W
ebFluxEndpointHandlerMapping
.
W
riteOperationHandler
(
operationInvoker
)
:
new
WebFluxEndpointHandlerMapping
.
ReadOperationHandler
(
operationInvoker
),
operationType
==
OperationType
.
WRITE
?
this
.
handleWrite
:
this
.
handleRead
);
}
private
RequestMappingInfo
createRequestMappingInfo
(
WebEndpointOperation
operationInfo
)
{
OperationRequestPredicate
requestPredicate
=
operationInfo
.
getRequestPredicate
();
PatternsRequestCondition
patterns
=
new
PatternsRequestCondition
(
pathPatternParser
.
parse
(
this
.
endpointMapping
.
createSubPath
(
requestPredicate
.
getPath
())));
RequestMethodsRequestCondition
methods
=
new
RequestMethodsRequestCondition
(
RequestMethod
.
valueOf
(
requestPredicate
.
getHttpMethod
().
name
()));
ConsumesRequestCondition
consumes
=
new
ConsumesRequestCondition
(
toStringArray
(
requestPredicate
.
getConsumes
()));
ProducesRequestCondition
produces
=
new
ProducesRequestCondition
(
toStringArray
(
requestPredicate
.
getProduces
()));
return
new
RequestMappingInfo
(
null
,
patterns
,
methods
,
null
,
null
,
consumes
,
produces
,
null
);
}
private
String
[]
toStringArray
(
Collection
<
String
>
collection
)
{
return
collection
.
toArray
(
new
String
[
collection
.
size
()]);
}
@Override
protected
boolean
isHandler
(
Class
<?>
beanType
)
{
return
false
;
}
@Override
protected
RequestMappingInfo
getMappingForMethod
(
Method
method
,
Class
<?>
handlerType
)
{
return
null
;
}
@ResponseBody
private
Map
<
String
,
Map
<
String
,
Link
>>
links
(
ServerHttpRequest
request
)
{
return
Collections
.
singletonMap
(
"_links"
,
this
.
endpointLinksResolver
.
resolveLinks
(
this
.
webEndpoints
,
this
.
endpointLinksResolver
.
resolveLinks
(
getEndpoints
()
,
UriComponentsBuilder
.
fromUri
(
request
.
getURI
()).
replaceQuery
(
null
)
.
toUriString
()));
}
/**
* Base class for handlers for endpoint operations.
*/
...
...
@@ -286,35 +204,4 @@ public class WebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapp
}
/**
* An {@link OperationInvoker} that performs the invocation of a blocking operation on
* a separate thread using Reactor's {@link Schedulers#elastic() elastic scheduler}.
*/
private
static
final
class
ElasticSchedulerOperationInvoker
implements
OperationInvoker
{
private
final
OperationInvoker
delegate
;
private
ElasticSchedulerOperationInvoker
(
OperationInvoker
delegate
)
{
this
.
delegate
=
delegate
;
}
@Override
public
Object
invoke
(
Map
<
String
,
Object
>
arguments
)
{
return
Mono
.
create
((
sink
)
->
Schedulers
.
elastic
()
.
schedule
(()
->
invoke
(
arguments
,
sink
)));
}
private
void
invoke
(
Map
<
String
,
Object
>
arguments
,
MonoSink
<
Object
>
sink
)
{
try
{
Object
result
=
this
.
delegate
.
invoke
(
arguments
);
sink
.
success
(
result
);
}
catch
(
Exception
ex
)
{
sink
.
error
(
ex
);
}
}
}
}
spring-boot-project/spring-boot-parent/pom.xml
View file @
d50fe887
...
...
@@ -61,7 +61,12 @@
<dependency>
<groupId>
com.squareup.okhttp3
</groupId>
<artifactId>
okhttp
</artifactId>
<version>
3.4.1
</version>
<version>
3.9.0
</version>
</dependency>
<dependency>
<groupId>
com.squareup.okhttp3
</groupId>
<artifactId>
mockwebserver
</artifactId>
<version>
3.9.0
</version>
</dependency>
<dependency>
<groupId>
com.vaadin.external.google
</groupId>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment