diff --git a/CHANGELOG.md b/CHANGELOG.md index cfdc11e69c..6b0c1df8d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Add explicit provider setup for bouncy castle ([#1500](https://github.com/scm-manager/scm-manager/pull/1500)) - Repository contact information is editable ([#1508](https://github.com/scm-manager/scm-manager/pull/1508)) +- Usage of custom realm description for scm protocols ([#1512](https://github.com/scm-manager/scm-manager/pull/1512)) ## [2.12.0] - 2020-12-17 ### Added diff --git a/scm-core/src/main/java/sonia/scm/util/HttpUtil.java b/scm-core/src/main/java/sonia/scm/util/HttpUtil.java index 7056911a20..951ac33d1d 100644 --- a/scm-core/src/main/java/sonia/scm/util/HttpUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/HttpUtil.java @@ -582,20 +582,20 @@ public final class HttpUtil HttpServletResponse response, String realmDescription) throws IOException { - if ((request == null) ||!isWUIRequest(request)) - { - response.setHeader(HEADER_WWW_AUTHENTICATE, - "Basic realm=\"".concat(realmDescription).concat("\"")); - - } - else if (logger.isTraceEnabled()) - { - logger.trace( - "do not send WWW-Authenticate header, because the client is the web interface"); + if ((request == null) ||!isWUIRequest(request)) { + String headerValue = "Basic realm=\""; + if (Strings.isNullOrEmpty(realmDescription)) { + headerValue += AUTHENTICATION_REALM; + } else { + headerValue += realmDescription; + } + headerValue += "\""; + response.setHeader(HEADER_WWW_AUTHENTICATE, headerValue); + } else if (logger.isTraceEnabled()) { + logger.trace("do not send WWW-Authenticate header, because the client is the web interface"); } - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, - STATUS_UNAUTHORIZED_MESSAGE); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, STATUS_UNAUTHORIZED_MESSAGE); } /** diff --git a/scm-core/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBase.java b/scm-core/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBase.java index e6716f0013..3750673ed3 100644 --- a/scm-core/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBase.java +++ b/scm-core/src/main/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBase.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.web.filter; import sonia.scm.config.ScmConfiguration; @@ -57,7 +57,7 @@ public class HttpProtocolServletAuthenticationFilterBase extends AuthenticationF // we can proceed the filter chain because the HttpProtocolServlet will render the ui if the client is a browser chain.doFilter(request, response); } else { - HttpUtil.sendUnauthorized(request, response); + HttpUtil.sendUnauthorized(request, response, configuration.getRealmDescription()); } } diff --git a/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java b/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java index 40aa184145..953c36e768 100644 --- a/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java +++ b/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java @@ -39,6 +39,8 @@ import static org.mockito.Mockito.*; //~--- JDK imports ------------------------------------------------------------ import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; /** * @@ -396,13 +398,10 @@ public class HttpUtilTest @Test public void getPortFromUrlTest() { - assertTrue(HttpUtil.getPortFromUrl("http://www.scm-manager.org") == 80); - assertTrue(HttpUtil.getPortFromUrl("https://www.scm-manager.org") == 443); - assertTrue(HttpUtil.getPortFromUrl("http://www.scm-manager.org:8080") - == 8080); - assertTrue( - HttpUtil.getPortFromUrl("http://www.scm-manager.org:8181/test/folder") - == 8181); + assertThat(HttpUtil.getPortFromUrl("http://www.scm-manager.org")).isEqualTo(80); + assertThat(HttpUtil.getPortFromUrl("https://www.scm-manager.org")).isEqualTo(443); + assertThat(HttpUtil.getPortFromUrl("http://www.scm-manager.org:8080")).isEqualTo(8080); + assertThat(HttpUtil.getPortFromUrl("http://www.scm-manager.org:8181/test/folder")).isEqualTo(8181); } /** @@ -418,9 +417,9 @@ public class HttpUtilTest ScmConfiguration config = new ScmConfiguration(); - assertTrue(HttpUtil.getServerPort(config, request) == 443); + assertThat(HttpUtil.getServerPort(config, request)).isEqualTo(443); config.setBaseUrl("http://www.scm-manager.org:8080"); - assertTrue(HttpUtil.getServerPort(config, request) == 8080); + assertThat(HttpUtil.getServerPort(config, request)).isEqualTo(8080); } /** @@ -508,4 +507,26 @@ public class HttpUtilTest assertThat(HttpUtil.isWUIRequest(request)).isTrue(); } + + @Test + public void sendUnauthorized() throws IOException { + HttpServletResponse response = mock(HttpServletResponse.class); + HttpUtil.sendUnauthorized(response, "Hitchhikers finest"); + verify(response).setHeader(HttpUtil.HEADER_WWW_AUTHENTICATE, "Basic realm=\"Hitchhikers finest\""); + verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, HttpUtil.STATUS_UNAUTHORIZED_MESSAGE); + } + + @Test + public void sendUnauthorizedWithDefaultRealmForNullDescription() throws IOException { + HttpServletResponse response = mock(HttpServletResponse.class); + HttpUtil.sendUnauthorized(response, null); + verify(response).setHeader(HttpUtil.HEADER_WWW_AUTHENTICATE, "Basic realm=\"" + HttpUtil.AUTHENTICATION_REALM + "\""); + } + + @Test + public void sendUnauthorizedWithDefaultRealmForEmptyDescription() throws IOException { + HttpServletResponse response = mock(HttpServletResponse.class); + HttpUtil.sendUnauthorized(response, ""); + verify(response).setHeader(HttpUtil.HEADER_WWW_AUTHENTICATE, "Basic realm=\"" + HttpUtil.AUTHENTICATION_REALM + "\""); + } } diff --git a/scm-core/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBaseTest.java b/scm-core/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBaseTest.java index a3b9318e5a..f76b116f69 100644 --- a/scm-core/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBaseTest.java +++ b/scm-core/src/test/java/sonia/scm/web/filter/HttpProtocolServletAuthenticationFilterBaseTest.java @@ -51,7 +51,7 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class HttpProtocolServletAuthenticationFilterBaseTest { - private ScmConfiguration configuration = new ScmConfiguration(); + private ScmConfiguration configuration; private Set tokenGenerators = Collections.emptySet(); @@ -74,6 +74,7 @@ class HttpProtocolServletAuthenticationFilterBaseTest { @BeforeEach void setUpObjectUnderTest() { + configuration = new ScmConfiguration(); authenticationFilter = new HttpProtocolServletAuthenticationFilterBase(configuration, tokenGenerators, userAgentParser); } @@ -86,6 +87,16 @@ class HttpProtocolServletAuthenticationFilterBaseTest { verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, HttpUtil.STATUS_UNAUTHORIZED_MESSAGE); } + @Test + void shouldSendConfiguredRealmDescription() throws IOException, ServletException { + configuration.setRealmDescription("Hitchhikers finest"); + when(userAgentParser.parse(request)).thenReturn(nonBrowser); + + authenticationFilter.handleUnauthorized(request, response, filterChain); + + verify(response).setHeader(HttpUtil.HEADER_WWW_AUTHENTICATE, "Basic realm=\"Hitchhikers finest\""); + } + @Test void shouldCallFilterChain() throws IOException, ServletException { when(userAgentParser.parse(request)).thenReturn(browser); diff --git a/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java b/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java index 84f4f75191..c46f05a360 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.web.protocol; import com.google.inject.Inject; @@ -31,6 +31,7 @@ import org.apache.http.HttpStatus; import org.apache.shiro.authz.AuthorizationException; import sonia.scm.NotFoundException; import sonia.scm.PushStateDispatcher; +import sonia.scm.config.ScmConfiguration; import sonia.scm.filter.WebElement; import sonia.scm.repository.DefaultRepositoryProvider; import sonia.scm.repository.NamespaceAndName; @@ -57,6 +58,7 @@ public class HttpProtocolServlet extends HttpServlet { public static final String PATH = "/repo"; public static final String PATTERN = PATH + "/*"; + private final ScmConfiguration configuration; private final RepositoryServiceFactory serviceFactory; private final NamespaceAndNameFromPathExtractor pathExtractor; private final PushStateDispatcher dispatcher; @@ -64,7 +66,8 @@ public class HttpProtocolServlet extends HttpServlet { @Inject - public HttpProtocolServlet(RepositoryServiceFactory serviceFactory, NamespaceAndNameFromPathExtractor pathExtractor, PushStateDispatcher dispatcher, UserAgentParser userAgentParser) { + public HttpProtocolServlet(ScmConfiguration configuration, RepositoryServiceFactory serviceFactory, NamespaceAndNameFromPathExtractor pathExtractor, PushStateDispatcher dispatcher, UserAgentParser userAgentParser) { + this.configuration = configuration; this.serviceFactory = serviceFactory; this.pathExtractor = pathExtractor; this.dispatcher = dispatcher; @@ -100,7 +103,7 @@ public class HttpProtocolServlet extends HttpServlet { } catch (AuthorizationException e) { log.debug(e.getMessage()); if (Authentications.isAuthenticatedSubjectAnonymous()) { - HttpUtil.sendUnauthorized(resp); + HttpUtil.sendUnauthorized(resp, configuration.getRealmDescription()); } else { resp.setStatus(HttpStatus.SC_FORBIDDEN); } diff --git a/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java b/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java index 9dac395df9..6ac4698149 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java @@ -21,9 +21,13 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.web.protocol; +import org.apache.shiro.authz.AuthorizationException; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -33,6 +37,8 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.NotFoundException; import sonia.scm.PushStateDispatcher; +import sonia.scm.SCMContext; +import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.DefaultRepositoryProvider; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; @@ -40,6 +46,9 @@ import sonia.scm.repository.RepositoryTestData; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.spi.HttpScmProtocol; +import sonia.scm.security.AnonymousMode; +import sonia.scm.security.AnonymousToken; +import sonia.scm.util.HttpUtil; import sonia.scm.web.UserAgent; import sonia.scm.web.UserAgentParser; @@ -67,6 +76,9 @@ class HttpProtocolServletTest { @Mock private UserAgentParser userAgentParser; + @Mock + private ScmConfiguration configuration; + @InjectMocks private HttpProtocolServlet servlet; @@ -153,5 +165,56 @@ class HttpProtocolServletTest { verify(repositoryService).close(); } + @Nested + class WithSubject { + + @Mock + private Subject subject; + + @BeforeEach + void setUpSubject() { + ThreadContext.bind(subject); + } + + @AfterEach + void tearDownSubject() { + ThreadContext.unbindSubject(); + } + + @Test + void shouldSendUnauthorizedWithCustomRealmDescription() throws IOException, ServletException { + when(subject.getPrincipal()).thenReturn(SCMContext.USER_ANONYMOUS); + when(configuration.getRealmDescription()).thenReturn("Hitchhikers finest"); + + callServiceWithAuthorizationException(); + + verify(response).setHeader(HttpUtil.HEADER_WWW_AUTHENTICATE, "Basic realm=\"Hitchhikers finest\""); + verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, HttpUtil.STATUS_UNAUTHORIZED_MESSAGE); + } + + @Test + void shouldSendForbidden() throws IOException, ServletException { + callServiceWithAuthorizationException(); + + verify(response).setStatus(HttpServletResponse.SC_FORBIDDEN); + } + + private void callServiceWithAuthorizationException() throws IOException, ServletException { + NamespaceAndName repo = new NamespaceAndName("space", "name"); + when(extractor.fromUri("/space/name")).thenReturn(Optional.of(repo)); + when(serviceFactory.create(repo)).thenReturn(repositoryService); + + when(request.getPathInfo()).thenReturn("/space/name"); + Repository repository = RepositoryTestData.createHeartOfGold(); + when(repositoryService.getRepository()).thenReturn(repository); + when(repositoryService.getProtocol(HttpScmProtocol.class)).thenThrow( + new AuthorizationException("failed") + ); + + servlet.service(request, response); + } + + } + } }