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/package.json b/scm-ui/ui-components/package.json index 946416ca96..93a08a75f2 100644 --- a/scm-ui/ui-components/package.json +++ b/scm-ui/ui-components/package.json @@ -49,6 +49,7 @@ "@scm-manager/ui-types": "^2.0.0-SNAPSHOT", "classnames": "^2.2.6", "date-fns": "^2.4.1", + "event-source-polyfill": "^1.0.9", "query-string": "5", "react": "^16.8.6", "react-diff-view": "^1.8.1", diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index ac6a6638fc..25e78abbd8 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -336,7 +336,7 @@ exports[`Storyshots DateFromNow Default 1`] = ` exports[`Storyshots Forms|Checkbox Default 1`] = `