Skip to content

Commit a94ab67

Browse files
committed
Add health endpoint 'show-components' support
Add a `show-components` property under `management.endpoint.health` and `management.endpoint.health.group.<name>` that can be used to change when components are displayed. Prior to this commit it was only possible to set `show-details` which offered an "all or nothing" approach to the resulting JSON. The new switch allows component information to be displayed whilst still hiding potentially sensitive details returned from the actual `HealthIndicator`. Closes gh-15076
1 parent 69c561a commit a94ab67

File tree

15 files changed

+261
-102
lines changed

15 files changed

+261
-102
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/health.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ The following table describes the structure of the response:
2424
[cols="2,1,3"]
2525
include::{snippets}health/response-fields.adoc[]
2626

27+
NOTE: The response fields above are for the V3 API.
28+
If you need to return V2 JSON you should use an accept header or `application/vnd.spring-boot.actuator.v2+json`
29+
2730

2831

2932
[[health-retrieving-component]]

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import java.util.Collection;
2020
import java.util.function.Predicate;
2121

22-
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.ShowDetails;
22+
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Show;
2323
import org.springframework.boot.actuate.endpoint.SecurityContext;
2424
import org.springframework.boot.actuate.health.HealthEndpointGroup;
2525
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
@@ -40,7 +40,9 @@ class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup {
4040

4141
private final HttpCodeStatusMapper httpCodeStatusMapper;
4242

43-
private final ShowDetails showDetails;
43+
private final Show showComponents;
44+
45+
private final Show showDetails;
4446

4547
private final Collection<String> roles;
4648

@@ -49,14 +51,17 @@ class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup {
4951
* @param members a predicate used to test for group membership
5052
* @param statusAggregator the status aggregator to use
5153
* @param httpCodeStatusMapper the HTTP code status mapper to use
54+
* @param showComponents the show components setting
5255
* @param showDetails the show details setting
5356
* @param roles the roles to match
5457
*/
5558
AutoConfiguredHealthEndpointGroup(Predicate<String> members, StatusAggregator statusAggregator,
56-
HttpCodeStatusMapper httpCodeStatusMapper, ShowDetails showDetails, Collection<String> roles) {
59+
HttpCodeStatusMapper httpCodeStatusMapper, Show showComponents, Show showDetails,
60+
Collection<String> roles) {
5761
this.members = members;
5862
this.statusAggregator = statusAggregator;
5963
this.httpCodeStatusMapper = httpCodeStatusMapper;
64+
this.showComponents = showComponents;
6065
this.showDetails = showDetails;
6166
this.roles = roles;
6267
}
@@ -67,17 +72,28 @@ public boolean isMember(String name) {
6772
}
6873

6974
@Override
70-
public boolean includeDetails(SecurityContext securityContext) {
71-
ShowDetails showDetails = this.showDetails;
72-
switch (showDetails) {
75+
public boolean showComponents(SecurityContext securityContext) {
76+
if (this.showComponents == null) {
77+
return showDetails(securityContext);
78+
}
79+
return getShowResult(securityContext, this.showComponents);
80+
}
81+
82+
@Override
83+
public boolean showDetails(SecurityContext securityContext) {
84+
return getShowResult(securityContext, this.showDetails);
85+
}
86+
87+
private boolean getShowResult(SecurityContext securityContext, Show show) {
88+
switch (show) {
7389
case NEVER:
7490
return false;
7591
case ALWAYS:
7692
return true;
7793
case WHEN_AUTHORIZED:
7894
return isAuthorized(securityContext);
7995
}
80-
throw new IllegalStateException("Unsupported ShowDetails value " + showDetails);
96+
throw new IllegalStateException("Unsupported 'show' value " + show);
8197
}
8298

8399
private boolean isAuthorized(SecurityContext securityContext) {

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
3232
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
3333
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointProperties.Group;
34-
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.ShowDetails;
34+
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Show;
3535
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Status;
3636
import org.springframework.boot.actuate.health.HealthEndpointGroup;
3737
import org.springframework.boot.actuate.health.HealthEndpointGroups;
@@ -65,7 +65,8 @@ class AutoConfiguredHealthEndpointGroups implements HealthEndpointGroups {
6565
AutoConfiguredHealthEndpointGroups(ApplicationContext applicationContext, HealthEndpointProperties properties) {
6666
ListableBeanFactory beanFactory = (applicationContext instanceof ConfigurableApplicationContext)
6767
? ((ConfigurableApplicationContext) applicationContext).getBeanFactory() : applicationContext;
68-
ShowDetails showDetails = properties.getShowDetails();
68+
Show showComponents = properties.getShowComponents();
69+
Show showDetails = properties.getShowDetails();
6970
Set<String> roles = properties.getRoles();
7071
StatusAggregator statusAggregator = getNonQualifiedBean(beanFactory, StatusAggregator.class);
7172
if (statusAggregator == null) {
@@ -76,18 +77,20 @@ class AutoConfiguredHealthEndpointGroups implements HealthEndpointGroups {
7677
httpCodeStatusMapper = new SimpleHttpCodeStatusMapper(properties.getStatus().getHttpMapping());
7778
}
7879
this.primaryGroup = new AutoConfiguredHealthEndpointGroup(ALL, statusAggregator, httpCodeStatusMapper,
79-
showDetails, roles);
80+
showComponents, showDetails, roles);
8081
this.groups = createGroups(properties.getGroup(), beanFactory, statusAggregator, httpCodeStatusMapper,
81-
showDetails, roles);
82+
showComponents, showDetails, roles);
8283
}
8384

8485
private Map<String, HealthEndpointGroup> createGroups(Map<String, Group> groupProperties, BeanFactory beanFactory,
8586
StatusAggregator defaultStatusAggregator, HttpCodeStatusMapper defaultHttpCodeStatusMapper,
86-
ShowDetails defaultShowDetails, Set<String> defaultRoles) {
87+
Show defaultShowComponents, Show defaultShowDetails, Set<String> defaultRoles) {
8788
Map<String, HealthEndpointGroup> groups = new LinkedHashMap<String, HealthEndpointGroup>();
8889
groupProperties.forEach((groupName, group) -> {
8990
Status status = group.getStatus();
90-
ShowDetails showDetails = (group.getShowDetails() != null) ? group.getShowDetails() : defaultShowDetails;
91+
Show showComponents = (group.getShowComponents() != null) ? group.getShowComponents()
92+
: defaultShowComponents;
93+
Show showDetails = (group.getShowDetails() != null) ? group.getShowDetails() : defaultShowDetails;
9194
Set<String> roles = !CollectionUtils.isEmpty(group.getRoles()) ? group.getRoles() : defaultRoles;
9295
StatusAggregator statusAggregator = getQualifiedBean(beanFactory, StatusAggregator.class, groupName, () -> {
9396
if (!CollectionUtils.isEmpty(status.getOrder())) {
@@ -104,7 +107,7 @@ private Map<String, HealthEndpointGroup> createGroups(Map<String, Group> groupPr
104107
});
105108
Predicate<String> members = new IncludeExcludeGroupMemberPredicate(group.getInclude(), group.getExclude());
106109
groups.put(groupName, new AutoConfiguredHealthEndpointGroup(members, statusAggregator, httpCodeStatusMapper,
107-
showDetails, roles));
110+
showComponents, showDetails, roles));
108111
});
109112
return Collections.unmodifiableMap(groups);
110113
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthProperties.java

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,15 @@ public abstract class HealthProperties {
3838
@NestedConfigurationProperty
3939
private final Status status = new Status();
4040

41+
/**
42+
* When to show components. If not specified the 'show-details' setting will be used.
43+
*/
44+
private Show showComponents;
45+
4146
/**
4247
* When to show full health details.
4348
*/
44-
private ShowDetails showDetails = ShowDetails.NEVER;
49+
private Show showDetails = Show.NEVER;
4550

4651
/**
4752
* Roles used to determine whether or not a user is authorized to be shown details.
@@ -53,11 +58,19 @@ public Status getStatus() {
5358
return this.status;
5459
}
5560

56-
public ShowDetails getShowDetails() {
61+
public Show getShowComponents() {
62+
return this.showComponents;
63+
}
64+
65+
public void setShowComponents(Show showComponents) {
66+
this.showComponents = showComponents;
67+
}
68+
69+
public Show getShowDetails() {
5770
return this.showDetails;
5871
}
5972

60-
public void setShowDetails(ShowDetails showDetails) {
73+
public void setShowDetails(Show showDetails) {
6174
this.showDetails = showDetails;
6275
}
6376

@@ -102,23 +115,23 @@ public Map<String, Integer> getHttpMapping() {
102115
}
103116

104117
/**
105-
* Options for showing details in responses from the {@link HealthEndpoint} web
118+
* Options for showing items in responses from the {@link HealthEndpoint} web
106119
* extensions.
107120
*/
108-
public enum ShowDetails {
121+
public enum Show {
109122

110123
/**
111-
* Never show details in the response.
124+
* Never show the item in the response.
112125
*/
113126
NEVER,
114127

115128
/**
116-
* Show details in the response when accessed by an authorized user.
129+
* Show the item in the response when accessed by an authorized user.
117130
*/
118131
WHEN_AUTHORIZED,
119132

120133
/**
121-
* Always show details in the response.
134+
* Always show the item in the response.
122135
*/
123136
ALWAYS
124137

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HealthEndpointDocumentationTests.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,12 @@ public boolean isMember(String name) {
146146
}
147147

148148
@Override
149-
public boolean includeDetails(SecurityContext securityContext) {
149+
public boolean showComponents(SecurityContext securityContext) {
150+
return true;
151+
}
152+
153+
@Override
154+
public boolean showDetails(SecurityContext securityContext) {
150155
return true;
151156
}
152157

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java

Lines changed: 85 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import org.mockito.Mock;
2626
import org.mockito.MockitoAnnotations;
2727

28-
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.ShowDetails;
28+
import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Show;
2929
import org.springframework.boot.actuate.endpoint.SecurityContext;
3030
import org.springframework.boot.actuate.health.HttpCodeStatusMapper;
3131
import org.springframework.boot.actuate.health.StatusAggregator;
@@ -60,80 +60,142 @@ void setup() {
6060
@Test
6161
void isMemberWhenMemberPredicateMatchesAcceptsTrue() {
6262
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> name.startsWith("a"),
63-
this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet());
63+
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
6464
assertThat(group.isMember("albert")).isTrue();
6565
assertThat(group.isMember("arnold")).isTrue();
6666
}
6767

6868
@Test
6969
void isMemberWhenMemberPredicateRejectsReturnsTrue() {
7070
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> name.startsWith("a"),
71-
this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet());
71+
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
7272
assertThat(group.isMember("bert")).isFalse();
7373
assertThat(group.isMember("ernie")).isFalse();
7474
}
7575

7676
@Test
77-
void includeDetailsWhenShowDetailsIsNeverReturnsFalse() {
77+
void showDetailsWhenShowDetailsIsNeverReturnsFalse() {
7878
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
79-
this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.NEVER, Collections.emptySet());
80-
assertThat(group.includeDetails(SecurityContext.NONE)).isFalse();
79+
this.statusAggregator, this.httpCodeStatusMapper, null, Show.NEVER, Collections.emptySet());
80+
assertThat(group.showDetails(SecurityContext.NONE)).isFalse();
8181
}
8282

8383
@Test
84-
void includeDetailsWhenShowDetailsIsAlwaysReturnsTrue() {
84+
void showDetailsWhenShowDetailsIsAlwaysReturnsTrue() {
8585
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
86-
this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet());
87-
assertThat(group.includeDetails(SecurityContext.NONE)).isTrue();
86+
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
87+
assertThat(group.showDetails(SecurityContext.NONE)).isTrue();
8888
}
8989

9090
@Test
91-
void includeDetailsWhenShowDetailsIsWhenAuthorizedAndPrincipalIsNullReturnsFalse() {
91+
void showDetailsWhenShowDetailsIsWhenAuthorizedAndPrincipalIsNullReturnsFalse() {
9292
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
93-
this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED, Collections.emptySet());
93+
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, Collections.emptySet());
9494
given(this.securityContext.getPrincipal()).willReturn(null);
95-
assertThat(group.includeDetails(this.securityContext)).isFalse();
95+
assertThat(group.showDetails(this.securityContext)).isFalse();
9696
}
9797

9898
@Test
99-
void includeDetailsWhenShowDetailsIsWhenAuthorizedAndRolesAreEmptyReturnsTrue() {
99+
void showDetailsWhenShowDetailsIsWhenAuthorizedAndRolesAreEmptyReturnsTrue() {
100100
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
101-
this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED, Collections.emptySet());
101+
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, Collections.emptySet());
102102
given(this.securityContext.getPrincipal()).willReturn(this.principal);
103-
assertThat(group.includeDetails(this.securityContext)).isTrue();
103+
assertThat(group.showDetails(this.securityContext)).isTrue();
104104
}
105105

106106
@Test
107-
void includeDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsInRoleReturnsTrue() {
107+
void showDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsInRoleReturnsTrue() {
108108
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
109-
this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED,
109+
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED,
110110
Arrays.asList("admin", "root", "bossmode"));
111111
given(this.securityContext.getPrincipal()).willReturn(this.principal);
112112
given(this.securityContext.isUserInRole("root")).willReturn(true);
113-
assertThat(group.includeDetails(this.securityContext)).isTrue();
113+
assertThat(group.showDetails(this.securityContext)).isTrue();
114114
}
115115

116116
@Test
117-
void includeDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsNotInRoleReturnsFalse() {
117+
void showDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsNotInRoleReturnsFalse() {
118118
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
119-
this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.WHEN_AUTHORIZED,
119+
this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED,
120120
Arrays.asList("admin", "rot", "bossmode"));
121121
given(this.securityContext.getPrincipal()).willReturn(this.principal);
122122
given(this.securityContext.isUserInRole("root")).willReturn(true);
123-
assertThat(group.includeDetails(this.securityContext)).isFalse();
123+
assertThat(group.showDetails(this.securityContext)).isFalse();
124+
}
125+
126+
@Test
127+
void showComponentsWhenShowComponentsIsNullDelegatesToShowDetails() {
128+
AutoConfiguredHealthEndpointGroup alwaysGroup = new AutoConfiguredHealthEndpointGroup((name) -> true,
129+
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
130+
assertThat(alwaysGroup.showComponents(SecurityContext.NONE)).isTrue();
131+
AutoConfiguredHealthEndpointGroup neverGroup = new AutoConfiguredHealthEndpointGroup((name) -> true,
132+
this.statusAggregator, this.httpCodeStatusMapper, null, Show.NEVER, Collections.emptySet());
133+
assertThat(neverGroup.showComponents(SecurityContext.NONE)).isFalse();
134+
}
135+
136+
@Test
137+
void showComponentsWhenShowDetailsIsNeverReturnsFalse() {
138+
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
139+
this.statusAggregator, this.httpCodeStatusMapper, Show.NEVER, Show.ALWAYS, Collections.emptySet());
140+
assertThat(group.showComponents(SecurityContext.NONE)).isFalse();
141+
}
142+
143+
@Test
144+
void showComponentsWhenShowDetailsIsAlwaysReturnsTrue() {
145+
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
146+
this.statusAggregator, this.httpCodeStatusMapper, Show.ALWAYS, Show.NEVER, Collections.emptySet());
147+
assertThat(group.showComponents(SecurityContext.NONE)).isTrue();
148+
}
149+
150+
@Test
151+
void showComponentsWhenShowDetailsIsWhenAuthorizedAndPrincipalIsNullReturnsFalse() {
152+
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
153+
this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
154+
Collections.emptySet());
155+
given(this.securityContext.getPrincipal()).willReturn(null);
156+
assertThat(group.showComponents(this.securityContext)).isFalse();
157+
}
158+
159+
@Test
160+
void showComponentsWhenShowDetailsIsWhenAuthorizedAndRolesAreEmptyReturnsTrue() {
161+
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
162+
this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
163+
Collections.emptySet());
164+
given(this.securityContext.getPrincipal()).willReturn(this.principal);
165+
assertThat(group.showComponents(this.securityContext)).isTrue();
166+
}
167+
168+
@Test
169+
void showComponentsWhenShowDetailsIsWhenAuthorizedAndUseIsInRoleReturnsTrue() {
170+
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
171+
this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
172+
Arrays.asList("admin", "root", "bossmode"));
173+
given(this.securityContext.getPrincipal()).willReturn(this.principal);
174+
given(this.securityContext.isUserInRole("root")).willReturn(true);
175+
assertThat(group.showComponents(this.securityContext)).isTrue();
176+
}
177+
178+
@Test
179+
void showComponentsWhenShowDetailsIsWhenAuthorizedAndUseIsNotInRoleReturnsFalse() {
180+
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
181+
this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER,
182+
Arrays.asList("admin", "rot", "bossmode"));
183+
given(this.securityContext.getPrincipal()).willReturn(this.principal);
184+
given(this.securityContext.isUserInRole("root")).willReturn(true);
185+
assertThat(group.showComponents(this.securityContext)).isFalse();
124186
}
125187

126188
@Test
127189
void getStatusAggregatorReturnsStatusAggregator() {
128190
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
129-
this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet());
191+
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
130192
assertThat(group.getStatusAggregator()).isSameAs(this.statusAggregator);
131193
}
132194

133195
@Test
134196
void getHttpCodeStatusMapperReturnsHttpCodeStatusMapper() {
135197
AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true,
136-
this.statusAggregator, this.httpCodeStatusMapper, ShowDetails.ALWAYS, Collections.emptySet());
198+
this.statusAggregator, this.httpCodeStatusMapper, null, Show.ALWAYS, Collections.emptySet());
137199
assertThat(group.getHttpCodeStatusMapper()).isSameAs(this.httpCodeStatusMapper);
138200
}
139201

0 commit comments

Comments
 (0)