diff --git a/pom.xml b/pom.xml index 910287abf0..7a8e16b869 100644 --- a/pom.xml +++ b/pom.xml @@ -839,7 +839,7 @@ 2.3.0 - 1.5.1 + 1.6.0 9.4.22.v20191022 diff --git a/scm-core/src/main/java/sonia/scm/security/BearerToken.java b/scm-core/src/main/java/sonia/scm/security/BearerToken.java index 4dd60a09c7..43a225b5b6 100644 --- a/scm-core/src/main/java/sonia/scm/security/BearerToken.java +++ b/scm-core/src/main/java/sonia/scm/security/BearerToken.java @@ -37,6 +37,8 @@ import com.google.common.base.Preconditions; import com.google.common.base.Strings; import org.apache.shiro.authc.AuthenticationToken; +import javax.annotation.Nullable; + /** * Token used for authentication with bearer tokens. * @@ -45,20 +47,23 @@ import org.apache.shiro.authc.AuthenticationToken; */ public final class BearerToken implements AuthenticationToken { + private final SessionId sessionId; private final String raw; /** * Constructs a new instance. - * + * + * @param sessionId session id of the client * @param raw raw bearer token */ - private BearerToken(String raw) { + private BearerToken(SessionId sessionId, String raw) { + this.sessionId = sessionId; this.raw = raw; } - + /** * Returns the wrapped raw format of the token. - * + * * @return raw format */ @Override @@ -67,24 +72,41 @@ public final class BearerToken implements AuthenticationToken { } /** - * Returns always {@code null}. - * - * @return {@code null} + * Returns the session id or {@code null}. + * + * @return session id or {@code null} */ @Override - public Object getPrincipal() { - return null; + public SessionId getPrincipal() { + return sessionId; } - + /** * Creates a new {@link BearerToken} from raw string representation. - * + * * @param raw string representation - * + * * @return new bearer token */ public static BearerToken valueOf(String raw){ Preconditions.checkArgument(!Strings.isNullOrEmpty(raw), "raw token is required"); - return new BearerToken(raw); + return new BearerToken(null, raw); + } + + /** + * Creates a new {@link BearerToken} from raw string representation for the given ui session id. + * + * @param sessionId session id of the client + * @param rawToken bearer token string representation + * + * @return new bearer token + */ + public static BearerToken create(@Nullable String sessionId, String rawToken) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(rawToken), "raw token is required"); + SessionId session = null; + if (!Strings.isNullOrEmpty(sessionId)) { + session = SessionId.valueOf(sessionId); + } + return new BearerToken(session, rawToken); } } diff --git a/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java b/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java index 6ec64a67de..6987fab0e1 100644 --- a/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java +++ b/scm-core/src/main/java/sonia/scm/security/DAORealmHelper.java @@ -67,13 +67,13 @@ public final class DAORealmHelper { private static final Logger LOG = LoggerFactory.getLogger(DAORealmHelper.class); private final LoginAttemptHandler loginAttemptHandler; - + private final UserDAO userDAO; - + private final String realm; - + //~--- constructors --------------------------------------------------------- - + /** * Constructs a new instance. Consider to use {@link DAORealmHelperFactory} which * handles dependency injection. @@ -92,9 +92,9 @@ public final class DAORealmHelper { /** * Wraps credentials matcher and applies login attempt policies. - * + * * @param credentialsMatcher credentials matcher to wrap - * + * * @return wrapped credentials matcher */ public CredentialsMatcher wrapCredentialsMatcher(CredentialsMatcher credentialsMatcher) { @@ -115,7 +115,7 @@ public final class DAORealmHelper { UsernamePasswordToken upt = (UsernamePasswordToken) token; String principal = upt.getUsername(); - return getAuthenticationInfo(principal, null, null); + return getAuthenticationInfo(principal, null, null, null); } /** @@ -129,8 +129,9 @@ public final class DAORealmHelper { return new AuthenticationInfoBuilder(principal); } - - private AuthenticationInfo getAuthenticationInfo(String principal, String credentials, Scope scope) { + private SimpleAuthenticationInfo getAuthenticationInfo( + String principal, String credentials, Scope scope, SessionId sessionId + ) { checkArgument(!Strings.isNullOrEmpty(principal), "username is required"); LOG.debug("try to authenticate {}", principal); @@ -150,6 +151,10 @@ public final class DAORealmHelper { collection.add(user, realm); collection.add(MoreObjects.firstNonNull(scope, Scope.empty()), realm); + if (sessionId != null) { + collection.add(sessionId, realm); + } + String creds = credentials; if (credentials == null) { @@ -170,7 +175,7 @@ public final class DAORealmHelper { private String credentials; private Scope scope; - private Iterable groups = Collections.emptySet(); + private SessionId sessionId; private AuthenticationInfoBuilder(String principal) { this.principal = principal; @@ -201,17 +206,17 @@ public final class DAORealmHelper { return this; } -// /** -// * With groups adds extra groups, besides those which come from the {@link GroupDAO}, to the authentication info. -// * -// * @param groups extra groups -// * -// * @return {@code this} -// */ -// public AuthenticationInfoBuilder withGroups(Iterable groups) { -// this.groups = groups; -// return this; -// } + /** + * With the session id. + * + * @param sessionId session id + * + * @return {@code this} + */ + public AuthenticationInfoBuilder withSessionId(SessionId sessionId) { + this.sessionId = sessionId; + return this; + } /** * Build creates the authentication info from the given information. @@ -219,7 +224,7 @@ public final class DAORealmHelper { * @return authentication info */ public AuthenticationInfo build() { - return getAuthenticationInfo(principal, credentials, scope); + return getAuthenticationInfo(principal, credentials, scope, sessionId); } } @@ -233,7 +238,7 @@ public final class DAORealmHelper { this.loginAttemptHandler = loginAttemptHandler; this.credentialsMatcher = credentialsMatcher; } - + @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { loginAttemptHandler.beforeAuthentication(token); @@ -245,7 +250,7 @@ public final class DAORealmHelper { } return result; } - + } - + } diff --git a/scm-core/src/main/java/sonia/scm/security/SessionID.java b/scm-core/src/main/java/sonia/scm/security/SessionID.java new file mode 100644 index 0000000000..3c4fb126ff --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/SessionID.java @@ -0,0 +1,41 @@ +package sonia.scm.security; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; + +import java.util.Objects; + +/** + * Client side session id. + */ +public final class SessionId { + + private final String value; + + private SessionId(String value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SessionId sessionID = (SessionId) o; + return Objects.equals(value, sessionID.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public String toString() { + return value; + } + + public static SessionId valueOf(String value) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(value), "session id could not be empty or null"); + return new SessionId(value); + } +} 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 2744addc62..8edc0c8e14 100644 --- a/scm-core/src/main/java/sonia/scm/util/HttpUtil.java +++ b/scm-core/src/main/java/sonia/scm/util/HttpUtil.java @@ -116,6 +116,12 @@ public final class HttpUtil */ public static final String HEADER_SCM_CLIENT = "X-SCM-Client"; + /** + * header for identifying the scm-manager client session + * @since 2.0.0 + */ + public static final String HEADER_SCM_SESSION = "X-SCM-Session-ID"; + /** Field description */ public static final String HEADER_USERAGENT = "User-Agent"; @@ -698,8 +704,10 @@ public final class HttpUtil String defaultValue) { String value = request.getHeader(header); - - return MoreObjects.firstNonNull(value, defaultValue); + if (value == null) { + value = defaultValue; + } + return value; } /** diff --git a/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java b/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java index 0fbcc20ac0..cc6d4020c1 100644 --- a/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java +++ b/scm-core/src/test/java/sonia/scm/security/DAORealmHelperTest.java @@ -27,9 +27,6 @@ class DAORealmHelperTest { @Mock private UserDAO userDAO; - @Mock - private GroupDAO groupDAO; - private DAORealmHelper helper; @BeforeEach @@ -87,6 +84,21 @@ class DAORealmHelperTest { assertThat(principals.oneByType(Scope.class)).isSameAs(scope); } + @Test + void shouldReturnAuthenticationInfoWithSessionId() { + User user = new User("trillian"); + when(userDAO.get("trillian")).thenReturn(user); + + SessionId session = SessionId.valueOf("abc123"); + + AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian") + .withSessionId(session) + .build(); + + PrincipalCollection principals = authenticationInfo.getPrincipals(); + assertThat(principals.oneByType(SessionId.class)).isSameAs(session); + } + @Test void shouldReturnAuthenticationInfoWithCredentials() { User user = new User("trillian"); 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 5e6fa3e10e..c827764a92 100644 --- a/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java +++ b/scm-core/src/test/java/sonia/scm/util/HttpUtilTest.java @@ -54,6 +54,31 @@ import javax.servlet.http.HttpServletRequest; public class HttpUtilTest { + @Test + public void testGetHeader() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader("Test")).thenReturn("Value-One"); + + String value = HttpUtil.getHeader(request, "Test", "Fallback"); + assertEquals("Value-One", value); + } + + @Test + public void testGetHeaderWithDefaultValue() { + HttpServletRequest request = mock(HttpServletRequest.class); + + String value = HttpUtil.getHeader(request, "Test", "Fallback"); + assertEquals("Fallback", value); + } + + @Test + public void testGetHeaderWithNullAsDefaultValue() { + HttpServletRequest request = mock(HttpServletRequest.class); + + String value = HttpUtil.getHeader(request, "Test", null); + assertNull(value); + } + @Test public void concatenateTest() { assertEquals( diff --git a/scm-ui/ui-components/src/apiclient.ts b/scm-ui/ui-components/src/apiclient.ts index 396200f1c1..60789ff5dd 100644 --- a/scm-ui/ui-components/src/apiclient.ts +++ b/scm-ui/ui-components/src/apiclient.ts @@ -2,12 +2,20 @@ import { contextPath } from "./urls"; import { createBackendError, ForbiddenError, isBackendError, UnauthorizedError } from "./errors"; import { BackendErrorContent } from "./errors"; +const sessionId = ( + Date.now().toString(36) + + Math.random() + .toString(36) + .substr(2, 5) +).toUpperCase(); + const applyFetchOptions: (p: RequestInit) => RequestInit = o => { o.credentials = "same-origin"; o.headers = { Cache: "no-cache", // identify the request as ajax request - "X-Requested-With": "XMLHttpRequest" + "X-Requested-With": "XMLHttpRequest", + "X-SCM-Session-ID": sessionId }; return o; }; diff --git a/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java b/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java index 324dfe9082..a2064575db 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java +++ b/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java @@ -56,7 +56,7 @@ import static com.google.common.base.Preconditions.checkArgument; @Extension public class BearerRealm extends AuthenticatingRealm { - + /** realm name */ @VisibleForTesting static final String REALM = "BearerRealm"; @@ -104,6 +104,7 @@ public class BearerRealm extends AuthenticatingRealm return helper.authenticationInfoBuilder(accessToken.getSubject()) .withCredentials(bt.getCredentials()) .withScope(Scopes.fromClaims(accessToken.getClaims())) + .withSessionId(bt.getPrincipal()) .build(); } diff --git a/scm-webapp/src/main/java/sonia/scm/web/BearerWebTokenGenerator.java b/scm-webapp/src/main/java/sonia/scm/web/BearerWebTokenGenerator.java index 57f83b4c35..4bb79e7b9a 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/BearerWebTokenGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/web/BearerWebTokenGenerator.java @@ -44,7 +44,7 @@ import javax.servlet.http.HttpServletRequest; /** * Creates a {@link BearerToken} from an authorization header with * bearer authorization. - * + * * @author Sebastian Sdorra * @since 2.0.0 */ @@ -53,7 +53,7 @@ public class BearerWebTokenGenerator extends SchemeBasedWebTokenGenerator { /** - * Creates a {@link BearerToken} from an authorization header + * Creates a {@link BearerToken} from an authorization header * with bearer authorization. * * @param request http servlet request @@ -70,7 +70,8 @@ public class BearerWebTokenGenerator extends SchemeBasedWebTokenGenerator if (HttpUtil.AUTHORIZATION_SCHEME_BEARER.equalsIgnoreCase(scheme)) { - token = BearerToken.valueOf(authorization); + String sessionId = request.getHeader(HttpUtil.HEADER_SCM_SESSION); + token = BearerToken.create(sessionId, authorization); } return token; diff --git a/scm-webapp/src/main/java/sonia/scm/web/CookieBearerWebTokenGenerator.java b/scm-webapp/src/main/java/sonia/scm/web/CookieBearerWebTokenGenerator.java index 27d82f5a41..2a0a97c07e 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/CookieBearerWebTokenGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/web/CookieBearerWebTokenGenerator.java @@ -43,7 +43,7 @@ import javax.servlet.http.HttpServletRequest; import sonia.scm.util.HttpUtil; /** - * Creates an {@link BearerToken} from the {@link #COOKIE_NAME} + * Creates an {@link BearerToken} from the {@link HttpUtil#COOKIE_BEARER_AUTHENTICATION} * cookie. * * @author Sebastian Sdorra @@ -54,7 +54,7 @@ public class CookieBearerWebTokenGenerator implements WebTokenGenerator { /** - * Creates an {@link BearerToken} from the {@link #COOKIE_NAME} + * Creates an {@link BearerToken} from the {@link HttpUtil#COOKIE_BEARER_AUTHENTICATION} * cookie. * * @param request http servlet request @@ -73,7 +73,8 @@ public class CookieBearerWebTokenGenerator implements WebTokenGenerator { if (HttpUtil.COOKIE_BEARER_AUTHENTICATION.equals(cookie.getName())) { - token = BearerToken.valueOf(cookie.getValue()); + String sessionId = HttpUtil.getHeader(request, HttpUtil.HEADER_SCM_SESSION, null); + token = BearerToken.create(sessionId, cookie.getValue()); break; } diff --git a/scm-webapp/src/main/java/sonia/scm/web/security/DefaultAdministrationContext.java b/scm-webapp/src/main/java/sonia/scm/web/security/DefaultAdministrationContext.java index b62b6c63f3..43238e3c3f 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/security/DefaultAdministrationContext.java +++ b/scm-webapp/src/main/java/sonia/scm/web/security/DefaultAdministrationContext.java @@ -177,45 +177,26 @@ public class DefaultAdministrationContext implements AdministrationContext //J+ } - /** - * Method description - * - * - * @param action - */ - private void doRunAsInNonWebSessionContext(PrivilegedAction action) - { - if (logger.isTraceEnabled()) - { - logger.trace("bind shiro security manager to current thread"); - } + private void doRunAsInNonWebSessionContext(PrivilegedAction action) { + logger.trace("bind shiro security manager to current thread"); - try - { + try { SecurityUtils.setSecurityManager(securityManager); Subject subject = createAdminSubject(); ThreadState state = new SubjectThreadState(subject); state.bind(); - try { - if (logger.isInfoEnabled()) - { - logger.info("execute action {} in administration context", - action.getClass().getName()); - } + logger.info("execute action {} in administration context", action.getClass().getName()); action.run(); + } finally { + logger.trace("restore current thread state"); + state.restore(); } - finally - { - state.clear(); - } - } - finally - { + } finally { SecurityUtils.setSecurityManager(null); } } diff --git a/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java b/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java index d458f9c72c..897b251cfa 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java @@ -52,7 +52,7 @@ import static org.mockito.Mockito.when; /** * Unit tests for {@link BearerRealm}. - * + * * @author Sebastian Sdorra */ @ExtendWith(MockitoExtension.class) @@ -84,11 +84,9 @@ class BearerRealmTest { @Test void shouldDoGetAuthentication() { - BearerToken bearerToken = BearerToken.valueOf("__bearer__"); + BearerToken bearerToken = BearerToken.create("__session__", "__bearer__"); AccessToken accessToken = mock(AccessToken.class); - Set groups = ImmutableSet.of("HeartOfGold", "Puzzle42"); - when(accessToken.getSubject()).thenReturn("trillian"); when(accessToken.getClaims()).thenReturn(new HashMap<>()); when(accessTokenResolver.resolve(bearerToken)).thenReturn(accessToken); @@ -96,6 +94,7 @@ class BearerRealmTest { when(realmHelper.authenticationInfoBuilder("trillian")).thenReturn(builder); when(builder.withCredentials("__bearer__")).thenReturn(builder); when(builder.withScope(any(Scope.class))).thenReturn(builder); + when(builder.withSessionId(any(SessionId.class))).thenReturn(builder); when(builder.build()).thenReturn(authenticationInfo); AuthenticationInfo result = realm.doGetAuthenticationInfo(bearerToken); diff --git a/scm-webapp/src/test/java/sonia/scm/web/BearerWebTokenGeneratorTest.java b/scm-webapp/src/test/java/sonia/scm/web/BearerWebTokenGeneratorTest.java index 71195b7f48..1c054111b7 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/BearerWebTokenGeneratorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/BearerWebTokenGeneratorTest.java @@ -1,10 +1,10 @@ /** * Copyright (c) 2014, Sebastian Sdorra * All rights reserved. - * + * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, @@ -13,7 +13,7 @@ * 3. Neither the name of SCM-Manager; nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,56 +24,82 @@ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * + * * http://bitbucket.org/sdorra/scm-manager - * + * */ package sonia.scm.web; import javax.servlet.http.HttpServletRequest; import org.apache.shiro.authc.AuthenticationToken; -import org.junit.Test; -import static org.junit.Assert.*; -import static org.hamcrest.Matchers.*; -import org.junit.runner.RunWith; + + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; + +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; -import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.security.BearerToken; +import sonia.scm.security.SessionId; +import sonia.scm.util.HttpUtil; /** * * @author Sebastian Sdorra */ -@RunWith(MockitoJUnitRunner.class) -public class BearerWebTokenGeneratorTest { +@ExtendWith(MockitoExtension.class) +class BearerWebTokenGeneratorTest { + + private final BearerWebTokenGenerator tokenGenerator = new BearerWebTokenGenerator(); @Mock private HttpServletRequest request; - - private final BearerWebTokenGenerator tokenGenerator = new BearerWebTokenGenerator(); @Test - public void testCreateTokenWithWrongScheme() - { + void shouldNotCreateTokenWithWrongScheme() { when(request.getHeader("Authorization")).thenReturn("BASIC ASD"); - assertNull(tokenGenerator.createToken(request)); - } - - @Test - public void testCreateTokenWithoutAuthorizationHeader(){ - assertNull(tokenGenerator.createToken(request)); - } - - @Test - public void testCreateToken(){ - when(request.getHeader("Authorization")).thenReturn("Bearer asd"); + AuthenticationToken token = tokenGenerator.createToken(request); - assertNotNull(token); - assertThat(token, instanceOf(BearerToken.class)); + + assertThat(token).isNull(); + } + + @Test + void shouldNotCreateTokenWithoutAuthorizationHeader(){ + AuthenticationToken token = tokenGenerator.createToken(request); + + assertThat(token).isNull(); + } + + @Test + void shouldCreateToken(){ + when(request.getHeader("Authorization")).thenReturn("Bearer asd"); + + AuthenticationToken token = tokenGenerator.createToken(request); + assertThat(token) + .isNotNull() + .isInstanceOf(BearerToken.class); + BearerToken bt = (BearerToken) token; - assertThat(bt.getCredentials(), equalTo("asd")); + assertThat(bt.getCredentials()).isEqualTo("asd"); + } + + @Test + void shouldCreateTokenWithSessionId(){ + doReturn("Bearer asd").when(request).getHeader("Authorization"); + doReturn("bcd123").when(request).getHeader(HttpUtil.HEADER_SCM_SESSION); + + AuthenticationToken token = tokenGenerator.createToken(request); + assertThat(token) + .isNotNull() + .isInstanceOf(BearerToken.class); + + BearerToken bt = (BearerToken) token; + assertThat(bt.getPrincipal()).isEqualTo(SessionId.valueOf("bcd123")); + assertThat(bt.getCredentials()).isEqualTo("asd"); } } diff --git a/scm-webapp/src/test/java/sonia/scm/web/CookieBearerWebTokenGeneratorTest.java b/scm-webapp/src/test/java/sonia/scm/web/CookieBearerWebTokenGeneratorTest.java index 115b52fa36..45d2c46b83 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/CookieBearerWebTokenGeneratorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/CookieBearerWebTokenGeneratorTest.java @@ -35,82 +35,81 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- -import org.junit.Test; -import org.junit.runner.RunWith; - +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - +import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.security.BearerToken; - -import static org.junit.Assert.*; - -import static org.mockito.Mockito.*; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.security.SessionId; +import sonia.scm.util.HttpUtil; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; -import sonia.scm.util.HttpUtil; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +//~--- JDK imports ------------------------------------------------------------ /** * * @author Sebastian Sdorra */ -@RunWith(MockitoJUnitRunner.class) -public class CookieBearerWebTokenGeneratorTest -{ +@ExtendWith(MockitoExtension.class) +class CookieBearerWebTokenGeneratorTest { + + private final CookieBearerWebTokenGenerator tokenGenerator = new CookieBearerWebTokenGenerator(); + + @Mock + private HttpServletRequest request; - /** - * Method description - * - */ @Test - public void testCreateToken() - { - Cookie c = mock(Cookie.class); - - when(c.getName()).thenReturn(HttpUtil.COOKIE_BEARER_AUTHENTICATION); - when(c.getValue()).thenReturn("value"); - when(request.getCookies()).thenReturn(new Cookie[] { c }); + void shouldCreateToken() { + assignBearerCookie("value"); BearerToken token = tokenGenerator.createToken(request); - assertNotNull(token); - assertEquals("value", token.getCredentials()); + assertThat(token).isNotNull(); + assertThat(token.getPrincipal()).isNull(); + assertThat(token.getCredentials()).isEqualTo("value"); } - /** - * Method description - * - */ @Test - public void testCreateTokenWithWrongCookie() - { + void shouldCreateTokenWithSessionId() { + when(request.getHeader(HttpUtil.HEADER_SCM_SESSION)).thenReturn("abc123"); + + assignBearerCookie("authc"); + + BearerToken token = tokenGenerator.createToken(request); + + assertThat(token).isNotNull(); + assertThat(token.getPrincipal()).isEqualTo(SessionId.valueOf("abc123")); + assertThat(token.getCredentials()).isEqualTo("authc"); + } + + private void assignBearerCookie(String value) { + assignCookie(HttpUtil.COOKIE_BEARER_AUTHENTICATION, value); + } + + private void assignCookie(String name, String value) { Cookie c = mock(Cookie.class); - when(c.getName()).thenReturn("other-cookie"); - when(request.getCookies()).thenReturn(new Cookie[] { c }); - assertNull(tokenGenerator.createToken(request)); + when(c.getName()).thenReturn(name); + lenient().when(c.getValue()).thenReturn(value); + when(request.getCookies()).thenReturn(new Cookie[]{c}); } - /** - * Method description - * - */ @Test - public void testCreateTokenWithoutCookies() - { - assertNull(tokenGenerator.createToken(request)); + void shouldNotCreateTokenForWrongCookie() { + assignCookie("other-cookie", "with-some-value"); + + BearerToken token = tokenGenerator.createToken(request); + assertThat(token).isNull(); } - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final CookieBearerWebTokenGenerator tokenGenerator = - new CookieBearerWebTokenGenerator(); - - /** Field description */ - @Mock - private HttpServletRequest request; + @Test + void shouldNotCreateTokenWithoutCookies() { + BearerToken token = tokenGenerator.createToken(request); + assertThat(token).isNull(); + } } diff --git a/scm-webapp/src/test/java/sonia/scm/web/security/DefaultAdministrationContextTest.java b/scm-webapp/src/test/java/sonia/scm/web/security/DefaultAdministrationContextTest.java new file mode 100644 index 0000000000..7771559809 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/web/security/DefaultAdministrationContextTest.java @@ -0,0 +1,72 @@ +package sonia.scm.web.security; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.mgt.DefaultSecurityManager; +import org.apache.shiro.mgt.SecurityManager; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; +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 static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DefaultAdministrationContextTest { + + private DefaultAdministrationContext context; + + @Mock + private Subject subject; + + @BeforeEach + void create() { + Injector injector = Guice.createInjector(); + SecurityManager securityManager = new DefaultSecurityManager(); + + context = new DefaultAdministrationContext(injector, securityManager); + } + + @Test + void shouldBindSubject() { + context.runAsAdmin(() -> { + Subject adminSubject = SecurityUtils.getSubject(); + assertThat(adminSubject.getPrincipal()).isEqualTo("scmsystem"); + }); + } + + @Test + void shouldBindSubjectEvenIfAlreadyBound() { + ThreadContext.bind(subject); + try { + + context.runAsAdmin(() -> { + Subject adminSubject = SecurityUtils.getSubject(); + assertThat(adminSubject.getPrincipal()).isEqualTo("scmsystem"); + }); + + } finally { + ThreadContext.unbindSubject(); + } + } + + @Test + void shouldRestoreCurrentSubject() { + when(subject.getPrincipal()).thenReturn("tricia"); + ThreadContext.bind(subject); + try { + context.runAsAdmin(() -> {}); + Subject currentSubject = SecurityUtils.getSubject(); + assertThat(currentSubject.getPrincipal()).isEqualTo("tricia"); + } finally { + ThreadContext.unbindSubject(); + } + } + +}