From 707886efe083ba5b07becb9554b599ab406ef873 Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Fri, 30 Nov 2018 22:13:12 +0100 Subject: [PATCH] SONARCLOUD-213 add core extension CustomAuthentication --- .../authentication/BasicAuthentication.java | 26 +++++------- .../authentication/CustomAuthentication.java | 41 +++++++++++++++++++ .../authentication/RequestAuthenticator.java | 2 + .../RequestAuthenticatorImpl.java | 19 ++++++++- .../RequestAuthenticatorImplTest.java | 31 +++++++++++++- 5 files changed, 100 insertions(+), 19 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/authentication/CustomAuthentication.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/BasicAuthentication.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/BasicAuthentication.java index 28dc21f8573..0ecd1f6b72f 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/BasicAuthentication.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/BasicAuthentication.java @@ -30,7 +30,7 @@ import org.sonar.server.authentication.event.AuthenticationException; import org.sonar.server.usertoken.UserTokenAuthentication; import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Locale.ENGLISH; +import static org.apache.commons.lang.StringUtils.startsWithIgnoreCase; import static org.sonar.server.authentication.event.AuthenticationEvent.Method; import static org.sonar.server.authentication.event.AuthenticationEvent.Source; @@ -43,11 +43,6 @@ import static org.sonar.server.authentication.event.AuthenticationEvent.Source; */ public class BasicAuthentication { - private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder(); - - private static final String AUTHORIZATION_HEADER = "Authorization"; - private static final String BASIC_AUTHORIZATION = "BASIC"; - private final DbClient dbClient; private final CredentialsAuthentication credentialsAuthentication; private final UserTokenAuthentication userTokenAuthentication; @@ -62,17 +57,16 @@ public class BasicAuthentication { } public Optional authenticate(HttpServletRequest request) { - String authorizationHeader = request.getHeader(AUTHORIZATION_HEADER); - if (authorizationHeader == null || !authorizationHeader.toUpperCase(ENGLISH).startsWith(BASIC_AUTHORIZATION)) { + return extractCredentialsFromHeader(request) + .flatMap(credentials -> Optional.of(authenticate(credentials, request))); + } + + public static Optional extractCredentialsFromHeader(HttpServletRequest request) { + String authorizationHeader = request.getHeader("Authorization"); + if (authorizationHeader == null || !startsWithIgnoreCase(authorizationHeader, "BASIC")) { return Optional.empty(); } - Credentials credentials = extractCredentials(authorizationHeader); - UserDto userDto = authenticate(credentials, request); - return Optional.of(userDto); - } - - private static Credentials extractCredentials(String authorizationHeader) { String basicAuthEncoded = authorizationHeader.substring(6); String basicAuthDecoded = getDecodedBasicAuth(basicAuthEncoded); @@ -85,12 +79,12 @@ public class BasicAuthentication { } String login = basicAuthDecoded.substring(0, semiColonPos); String password = basicAuthDecoded.substring(semiColonPos + 1); - return new Credentials(login, password); + return Optional.of(new Credentials(login, password)); } private static String getDecodedBasicAuth(String basicAuthEncoded) { try { - return new String(BASE64_DECODER.decode(basicAuthEncoded.getBytes(UTF_8)), UTF_8); + return new String(Base64.getDecoder().decode(basicAuthEncoded.getBytes(UTF_8)), UTF_8); } catch (Exception e) { throw AuthenticationException.newBuilder() .setSource(Source.local(Method.BASIC)) diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/CustomAuthentication.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/CustomAuthentication.java new file mode 100644 index 00000000000..db20b60b989 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/CustomAuthentication.java @@ -0,0 +1,41 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.authentication; + +import java.util.Optional; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.sonar.api.server.ServerSide; +import org.sonar.server.user.UserSession; + +/** + * Authentication that can create {@link org.sonar.server.user.UserSession} + * that are not associated to a user. + * That is convenient for authenticating bots that need special permissions. + * + * This is not an extension point, plugins can not provide their own + * implementations. + */ +@ServerSide +public interface CustomAuthentication { + + Optional authenticate(HttpServletRequest request, HttpServletResponse response); + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/RequestAuthenticator.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/RequestAuthenticator.java index ab565c615c8..5f74851796a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/RequestAuthenticator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/RequestAuthenticator.java @@ -21,9 +21,11 @@ package org.sonar.server.authentication; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.sonar.api.server.ServerSide; import org.sonar.server.authentication.event.AuthenticationException; import org.sonar.server.user.UserSession; +@ServerSide public interface RequestAuthenticator { UserSession authenticate(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException; diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/RequestAuthenticatorImpl.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/RequestAuthenticatorImpl.java index ee0f3e53816..1e23d5ee27a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/RequestAuthenticatorImpl.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/RequestAuthenticatorImpl.java @@ -19,11 +19,12 @@ */ package org.sonar.server.authentication; +import java.util.Arrays; +import java.util.List; import java.util.Optional; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.sonar.db.user.UserDto; -import org.sonar.server.authentication.event.AuthenticationException; import org.sonar.server.user.UserSession; import org.sonar.server.user.UserSessionFactory; @@ -33,17 +34,31 @@ public class RequestAuthenticatorImpl implements RequestAuthenticator { private final BasicAuthentication basicAuthentication; private final HttpHeadersAuthentication httpHeadersAuthentication; private final UserSessionFactory userSessionFactory; + private final List customAuthentications; public RequestAuthenticatorImpl(JwtHttpHandler jwtHttpHandler, BasicAuthentication basicAuthentication, HttpHeadersAuthentication httpHeadersAuthentication, - UserSessionFactory userSessionFactory) throws AuthenticationException { + UserSessionFactory userSessionFactory, CustomAuthentication[] customAuthentications) { this.jwtHttpHandler = jwtHttpHandler; this.basicAuthentication = basicAuthentication; this.httpHeadersAuthentication = httpHeadersAuthentication; this.userSessionFactory = userSessionFactory; + this.customAuthentications = Arrays.asList(customAuthentications); + } + + public RequestAuthenticatorImpl(JwtHttpHandler jwtHttpHandler, BasicAuthentication basicAuthentication, HttpHeadersAuthentication httpHeadersAuthentication, + UserSessionFactory userSessionFactory) { + this(jwtHttpHandler, basicAuthentication, httpHeadersAuthentication, userSessionFactory, new CustomAuthentication[0]); } @Override public UserSession authenticate(HttpServletRequest request, HttpServletResponse response) { + for (CustomAuthentication customAuthentication : customAuthentications) { + Optional session = customAuthentication.authenticate(request, response); + if (session.isPresent()) { + return session.get(); + } + } + Optional userOpt = loadUser(request, response); if (userOpt.isPresent()) { return userSessionFactory.create(userOpt.get()); diff --git a/server/sonar-server/src/test/java/org/sonar/server/authentication/RequestAuthenticatorImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/authentication/RequestAuthenticatorImplTest.java index e6cd47ea8e4..65d56484fe1 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/authentication/RequestAuthenticatorImplTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/authentication/RequestAuthenticatorImplTest.java @@ -25,6 +25,8 @@ import javax.servlet.http.HttpServletResponse; import org.junit.Before; import org.junit.Test; import org.sonar.db.user.UserDto; +import org.sonar.server.authentication.event.AuthenticationEvent; +import org.sonar.server.authentication.event.AuthenticationException; import org.sonar.server.tester.AnonymousMockUserSession; import org.sonar.server.tester.MockUserSession; import org.sonar.server.user.UserSession; @@ -48,7 +50,10 @@ public class RequestAuthenticatorImplTest { private BasicAuthentication basicAuthentication = mock(BasicAuthentication.class); private HttpHeadersAuthentication httpHeadersAuthentication = mock(HttpHeadersAuthentication.class); private UserSessionFactory sessionFactory = mock(UserSessionFactory.class); - private RequestAuthenticator underTest = new RequestAuthenticatorImpl(jwtHttpHandler, basicAuthentication, httpHeadersAuthentication, sessionFactory); + private CustomAuthentication customAuthentication1 = mock(CustomAuthentication.class); + private CustomAuthentication customAuthentication2 = mock(CustomAuthentication.class); + private RequestAuthenticator underTest = new RequestAuthenticatorImpl(jwtHttpHandler, basicAuthentication, httpHeadersAuthentication, sessionFactory, + new CustomAuthentication[]{customAuthentication1, customAuthentication2}); @Before public void setUp() throws Exception { @@ -102,4 +107,28 @@ public class RequestAuthenticatorImplTest { verify(response, never()).setStatus(anyInt()); } + @Test + public void delegate_to_CustomAuthentication() { + when(customAuthentication1.authenticate(request, response)).thenReturn(Optional.of(new MockUserSession("foo"))); + + UserSession session = underTest.authenticate(request, response); + + assertThat(session.getLogin()).isEqualTo("foo"); + } + + @Test + public void CustomAuthentication_has_priority_over_core_authentications() { + // use-case: both custom and core authentications check the HTTP header "Authorization". + // The custom authentication should be able to test the header because that the core authentication + // throws an exception. + when(customAuthentication1.authenticate(request, response)).thenReturn(Optional.of(new MockUserSession("foo"))); + when(basicAuthentication.authenticate(request)).thenThrow(AuthenticationException.newBuilder() + .setSource(AuthenticationEvent.Source.sso()) + .setMessage("message") + .build()); + + UserSession session = underTest.authenticate(request, response); + + assertThat(session.getLogin()).isEqualTo("foo"); + } } -- 2.39.5