diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java index 52c44bd2826..f460617b443 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.health; +import java.security.Principal; import java.util.Collection; import java.util.function.Predicate; @@ -24,6 +25,9 @@ import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.health.HealthEndpointGroup; import org.springframework.boot.actuate.health.HttpCodeStatusMapper; import org.springframework.boot.actuate.health.StatusAggregator; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; /** @@ -97,10 +101,34 @@ class AutoConfiguredHealthEndpointGroup implements HealthEndpointGroup { } private boolean isAuthorized(SecurityContext securityContext) { - if (securityContext.getPrincipal() == null) { + Principal principal = securityContext.getPrincipal(); + if (principal == null) { return false; } - return CollectionUtils.isEmpty(this.roles) || this.roles.stream().anyMatch(securityContext::isUserInRole); + if (CollectionUtils.isEmpty(this.roles)) { + return true; + } + boolean checkAuthorities = isSpringSecurityAuthentication(principal); + for (String role : this.roles) { + if (securityContext.isUserInRole(role)) { + return true; + } + if (checkAuthorities) { + Authentication authentication = (Authentication) principal; + for (GrantedAuthority authority : authentication.getAuthorities()) { + String name = authority.getAuthority(); + if (role.equals(name)) { + return true; + } + } + } + } + return false; + } + + private boolean isSpringSecurityAuthentication(Principal principal) { + return ClassUtils.isPresent("org.springframework.security.core.Authentication", null) + && (principal instanceof Authentication); } @Override diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java index 8a31c44361d..06125f1759e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java @@ -29,9 +29,12 @@ import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Sh import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.health.HttpCodeStatusMapper; import org.springframework.boot.actuate.health.StatusAggregator; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; /** * Tests for {@link AutoConfiguredHealthEndpointGroup}. @@ -123,6 +126,30 @@ class AutoConfiguredHealthEndpointGroupTests { assertThat(group.showDetails(this.securityContext)).isFalse(); } + @Test + void showDetailsWhenShowDetailsIsWhenAuthorizedAndUserHasRightAuthorityReturnsTrue() { + AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, + this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, + Arrays.asList("admin", "root", "bossmode")); + Authentication principal = mock(Authentication.class); + given(principal.getAuthorities()) + .willAnswer((invocation) -> Collections.singleton(new SimpleGrantedAuthority("admin"))); + given(this.securityContext.getPrincipal()).willReturn(principal); + assertThat(group.showDetails(this.securityContext)).isTrue(); + } + + @Test + void showDetailsWhenShowDetailsIsWhenAuthorizedAndUserDoesNotHaveRightAuthoritiesReturnsFalse() { + AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, + this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, + Arrays.asList("admin", "rot", "bossmode")); + Authentication principal = mock(Authentication.class); + given(principal.getAuthorities()) + .willAnswer((invocation) -> Collections.singleton(new SimpleGrantedAuthority("other"))); + given(this.securityContext.getPrincipal()).willReturn(principal); + assertThat(group.showDetails(this.securityContext)).isFalse(); + } + @Test void showComponentsWhenShowComponentsIsNullDelegatesToShowDetails() { AutoConfiguredHealthEndpointGroup alwaysGroup = new AutoConfiguredHealthEndpointGroup((name) -> true, @@ -185,6 +212,30 @@ class AutoConfiguredHealthEndpointGroupTests { assertThat(group.showComponents(this.securityContext)).isFalse(); } + @Test + void showComponentsWhenShowComponentsIsWhenAuthorizedAndUserHasRightAuthoritiesReturnsTrue() { + AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, + this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER, + Arrays.asList("admin", "root", "bossmode")); + Authentication principal = mock(Authentication.class); + given(principal.getAuthorities()) + .willAnswer((invocation) -> Collections.singleton(new SimpleGrantedAuthority("admin"))); + given(this.securityContext.getPrincipal()).willReturn(principal); + assertThat(group.showComponents(this.securityContext)).isTrue(); + } + + @Test + void showComponentsWhenShowComponentsIsWhenAuthorizedAndUserDoesNotHaveRightAuthoritiesReturnsFalse() { + AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, + this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER, + Arrays.asList("admin", "rot", "bossmode")); + Authentication principal = mock(Authentication.class); + given(principal.getAuthorities()) + .willAnswer((invocation) -> Collections.singleton(new SimpleGrantedAuthority("other"))); + given(this.securityContext.getPrincipal()).willReturn(principal); + assertThat(group.showComponents(this.securityContext)).isFalse(); + } + @Test void getStatusAggregatorReturnsStatusAggregator() { AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthWebEndpointResponseMapper.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthWebEndpointResponseMapper.java index c26f6263ac9..5d61550618e 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthWebEndpointResponseMapper.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthWebEndpointResponseMapper.java @@ -16,11 +16,15 @@ package org.springframework.boot.actuate.health; +import java.security.Principal; import java.util.Set; import java.util.function.Supplier; import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; /** @@ -108,12 +112,29 @@ public class HealthWebEndpointResponseMapper { if (CollectionUtils.isEmpty(this.authorizedRoles)) { return true; } + Principal principal = securityContext.getPrincipal(); + boolean checkAuthorities = isSpringSecurityAuthentication(principal); for (String role : this.authorizedRoles) { if (securityContext.isUserInRole(role)) { return true; } + if (checkAuthorities) { + Authentication authentication = (Authentication) principal; + for (GrantedAuthority authority : authentication.getAuthorities()) { + String name = authority.getAuthority(); + if (role.equals(name)) { + return true; + } + } + } } + return false; } + private boolean isSpringSecurityAuthentication(Principal principal) { + return ClassUtils.isPresent("org.springframework.security.core.Authentication", null) + && (principal instanceof Authentication); + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthWebEndpointResponseMapperTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthWebEndpointResponseMapperTests.java index 66cd6ab428e..42324c8ad75 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthWebEndpointResponseMapperTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthWebEndpointResponseMapperTests.java @@ -29,6 +29,8 @@ import org.mockito.stubbing.Answer; import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyString; @@ -85,6 +87,39 @@ class HealthWebEndpointResponseMapperTests { verify(securityContext).isUserInRole("ACTUATOR"); } + @Test + void mapDetailsWithRightAuthoritiesInvokesSupplier() { + HealthWebEndpointResponseMapper mapper = createMapper(ShowDetails.WHEN_AUTHORIZED); + Supplier supplier = mockSupplier(); + given(supplier.get()).willReturn(Health.down().build()); + SecurityContext securityContext = getSecurityContext("ACTUATOR"); + WebEndpointResponse response = mapper.mapDetails(supplier, securityContext); + assertThat(response.getStatus()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE.value()); + assertThat(response.getBody().getStatus()).isEqualTo(Status.DOWN); + verify(supplier).get(); + } + + @Test + void mapDetailsWithOtherAuthoritiesShouldNotInvokeSupplier() { + HealthWebEndpointResponseMapper mapper = createMapper(ShowDetails.WHEN_AUTHORIZED); + Supplier supplier = mockSupplier(); + given(supplier.get()).willReturn(Health.down().build()); + SecurityContext securityContext = getSecurityContext("OTHER"); + WebEndpointResponse response = mapper.mapDetails(supplier, securityContext); + assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); + assertThat(response.getBody()).isNull(); + verifyNoInteractions(supplier); + } + + private SecurityContext getSecurityContext(String other) { + SecurityContext securityContext = mock(SecurityContext.class); + Authentication principal = mock(Authentication.class); + given(securityContext.getPrincipal()).willReturn(principal); + given(principal.getAuthorities()) + .willAnswer((invocation) -> Collections.singleton(new SimpleGrantedAuthority(other))); + return securityContext; + } + @Test void mapDetailsWithUnavailableHealth() { HealthWebEndpointResponseMapper mapper = createMapper(ShowDetails.ALWAYS);