mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-07-05 20:28:28 +02:00
Add anonymous health check endpoint
Besides the endpoint, another API was added, the ShouldRequestPassChecker. This api should be used to check whether a REST Request should be passed or rejected.
This commit is contained in:
2
gradle/changelog/health-check.yaml
Normal file
2
gradle/changelog/health-check.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Anonymous endpoint to check whether the SCM-Manager is healthy or not
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
public interface ShouldRequestPassChecker {
|
||||
|
||||
boolean shouldPass(HttpServletRequest request);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import sonia.scm.security.AllowAnonymousAccess;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
@Path(HealthCheckResource.PATH)
|
||||
public class HealthCheckResource {
|
||||
public static final String PATH = "v2/health-check";
|
||||
|
||||
@GET
|
||||
@Path("")
|
||||
@Operation(
|
||||
summary = "Get health check",
|
||||
description = "Returns depending on the response code, whether the scm manager is up and running",
|
||||
tags = "Health Check"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success"
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
@AllowAnonymousAccess
|
||||
public Response getHealthCheck() {
|
||||
return Response.ok().build();
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,8 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.plugin.WebElementDescriptor;
|
||||
import sonia.scm.security.DefaultShouldRequestPassChecker;
|
||||
import sonia.scm.security.ShouldRequestPassChecker;
|
||||
|
||||
|
||||
public class WebElementModule extends ServletModule {
|
||||
@@ -59,6 +61,7 @@ public class WebElementModule extends ServletModule {
|
||||
|
||||
// filters must be in singleton scope
|
||||
bind(clazz).in(Scopes.SINGLETON);
|
||||
bind(ShouldRequestPassChecker.class).to(DefaultShouldRequestPassChecker.class);
|
||||
|
||||
WebElementDescriptor opts = filter.getDescriptor();
|
||||
FilterKeyBindingBuilder builder;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import sonia.scm.api.v2.resources.HealthCheckResource;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
import sonia.scm.web.UserAgentParser;
|
||||
|
||||
public class DefaultShouldRequestPassChecker implements ShouldRequestPassChecker {
|
||||
|
||||
private final ScmConfiguration configuration;
|
||||
private final UserAgentParser userAgentParser;
|
||||
|
||||
@Inject
|
||||
public DefaultShouldRequestPassChecker(ScmConfiguration configuration, UserAgentParser userAgentParser) {
|
||||
this.configuration = configuration;
|
||||
this.userAgentParser = userAgentParser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldPass(HttpServletRequest request) {
|
||||
return isUserAuthenticated()
|
||||
|| isAnonymousProtocolRequest(request)
|
||||
|| isMercurialHookRequest(request)
|
||||
|| (!isLoginRequest(request) && isFullAnonymousAccessEnabled())
|
||||
|| isHealthCheckRequest(request);
|
||||
}
|
||||
|
||||
private boolean isUserAuthenticated() {
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
return subject.isAuthenticated() && !Authentications.isAuthenticatedSubjectAnonymous();
|
||||
}
|
||||
|
||||
private boolean isAnonymousProtocolRequest(HttpServletRequest request) {
|
||||
return !HttpUtil.isWUIRequest(request)
|
||||
&& Authentications.isAuthenticatedSubjectAnonymous()
|
||||
&& configuration.getAnonymousMode() == AnonymousMode.PROTOCOL_ONLY
|
||||
&& !userAgentParser.parse(request).isBrowser();
|
||||
}
|
||||
|
||||
private boolean isMercurialHookRequest(HttpServletRequest request) {
|
||||
return request.getRequestURI().startsWith(request.getContextPath() + "/hook/hg/");
|
||||
}
|
||||
|
||||
private boolean isLoginRequest(HttpServletRequest request) {
|
||||
final String requestURI = request.getRequestURI();
|
||||
final String contextPath = request.getContextPath();
|
||||
return requestURI != null && requestURI.startsWith(contextPath + "/login");
|
||||
}
|
||||
|
||||
private boolean isFullAnonymousAccessEnabled() {
|
||||
return configuration.getAnonymousMode() == AnonymousMode.FULL;
|
||||
}
|
||||
|
||||
private boolean isHealthCheckRequest(HttpServletRequest request) {
|
||||
return request.getRequestURI().startsWith(request.getContextPath() + "/api/" + HealthCheckResource.PATH);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class HealthCheckResourceTest {
|
||||
|
||||
private RestDispatcher dispatcher;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
HealthCheckResource resource = new HealthCheckResource();
|
||||
dispatcher = new RestDispatcher();
|
||||
dispatcher.addSingletonResource(resource);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnPositiveHealthCheck() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.get("/v2/health-check");
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.github.sdorra.jse.ShiroExtension;
|
||||
import org.github.sdorra.jse.SubjectAware;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.api.v2.resources.HealthCheckResource;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.web.UserAgent;
|
||||
import sonia.scm.web.UserAgentParser;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith({MockitoExtension.class, ShiroExtension.class})
|
||||
class DefaultShouldRequestPassCheckerTest {
|
||||
|
||||
@Mock
|
||||
private HttpServletRequest request;
|
||||
@Mock
|
||||
private UserAgentParser userAgentParser;
|
||||
private ScmConfiguration configuration;
|
||||
private DefaultShouldRequestPassChecker checker;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
lenient().when(request.getContextPath()).thenReturn("/scm");
|
||||
lenient().when(request.getRequestURI()).thenReturn("/scm/random/uri");
|
||||
configuration = new ScmConfiguration();
|
||||
checker = new DefaultShouldRequestPassChecker(configuration, userAgentParser);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware("Trainer Red")
|
||||
void shouldPassBecauseUserIsAuthenticated() {
|
||||
assertThat(checker.shouldPass(request)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(Authentications.PRINCIPAL_ANONYMOUS)
|
||||
void shouldNotPassBecauseUserIsAnonymous() {
|
||||
assertThat(checker.shouldPass(request)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(Authentications.PRINCIPAL_ANONYMOUS)
|
||||
void shouldPassBecauseAnonymousProtocolRequestIsEnabled() {
|
||||
configuration.setAnonymousMode(AnonymousMode.PROTOCOL_ONLY);
|
||||
when(userAgentParser.parse(request)).thenReturn(UserAgent.other("git").build());
|
||||
assertThat(checker.shouldPass(request)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(Authentications.PRINCIPAL_ANONYMOUS)
|
||||
void shouldPassBecauseMercurialHookRequest() {
|
||||
when(request.getContextPath()).thenReturn("/scm");
|
||||
when(request.getRequestURI()).thenReturn("/scm/hook/hg/");
|
||||
assertThat(checker.shouldPass(request)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(Authentications.PRINCIPAL_ANONYMOUS)
|
||||
void shouldPassBecauseFullAnonymousAccessIsEnabled() {
|
||||
configuration.setAnonymousMode(AnonymousMode.FULL);
|
||||
assertThat(checker.shouldPass(request)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(Authentications.PRINCIPAL_ANONYMOUS)
|
||||
void shouldPassBecauseRequestIsHealthCheck() {
|
||||
when(request.getRequestURI()).thenReturn("/scm/api/" + HealthCheckResource.PATH);
|
||||
assertThat(checker.shouldPass(request)).isTrue();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user