diff options
31 files changed, 1154 insertions, 155 deletions
diff --git a/server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java b/server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java index 5dd185823aa..5c899412dca 100644 --- a/server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java +++ b/server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java @@ -112,6 +112,7 @@ public final class SqTables { "rules_profiles", "rule_repositories", "schema_migrations", + "session_tokens", "snapshots", "users", "user_properties", diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java b/server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java index 05637e31fc6..bba7ecd2731 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java @@ -84,6 +84,7 @@ import org.sonar.db.source.FileSourceDao; import org.sonar.db.user.GroupDao; import org.sonar.db.user.GroupMembershipDao; import org.sonar.db.user.RoleDao; +import org.sonar.db.user.SessionTokensDao; import org.sonar.db.user.UserDao; import org.sonar.db.user.UserGroupDao; import org.sonar.db.user.UserPropertiesDao; @@ -156,6 +157,7 @@ public class DaoModule extends Module { RuleRepositoryDao.class, SnapshotDao.class, SchemaMigrationDao.class, + SessionTokensDao.class, UserDao.class, UserGroupDao.class, UserPermissionDao.class, diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java b/server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java index 1ca65d852ae..2d3f79aad57 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java @@ -82,6 +82,7 @@ import org.sonar.db.source.FileSourceDao; import org.sonar.db.user.GroupDao; import org.sonar.db.user.GroupMembershipDao; import org.sonar.db.user.RoleDao; +import org.sonar.db.user.SessionTokensDao; import org.sonar.db.user.UserDao; import org.sonar.db.user.UserGroupDao; import org.sonar.db.user.UserPropertiesDao; @@ -162,6 +163,7 @@ public class DbClient { private final OrganizationAlmBindingDao organizationAlmBindingDao; private final NewCodePeriodDao newCodePeriodDao; private final ProjectDao projectDao; + private final SessionTokensDao sessionTokensDao; public DbClient(Database database, MyBatis myBatis, DBSessions dbSessions, Dao... daos) { this.database = database; @@ -239,6 +241,7 @@ public class DbClient { internalComponentPropertiesDao = getDao(map, InternalComponentPropertiesDao.class); newCodePeriodDao = getDao(map, NewCodePeriodDao.class); projectDao = getDao(map, ProjectDao.class); + sessionTokensDao = getDao(map, SessionTokensDao.class); } public DbSession openSession(boolean batch) { @@ -527,4 +530,8 @@ public class DbClient { return newCodePeriodDao; } + public SessionTokensDao sessionTokensDao() { + return sessionTokensDao; + } + } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java b/server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java index c56a9646c00..ad5d5310b2f 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java @@ -140,6 +140,7 @@ import org.sonar.db.user.GroupMapper; import org.sonar.db.user.GroupMembershipDto; import org.sonar.db.user.GroupMembershipMapper; import org.sonar.db.user.RoleMapper; +import org.sonar.db.user.SessionTokenMapper; import org.sonar.db.user.UserDto; import org.sonar.db.user.UserGroupDto; import org.sonar.db.user.UserGroupMapper; @@ -286,6 +287,7 @@ public class MyBatis implements Startable { RuleMapper.class, RuleRepositoryMapper.class, SchemaMigrationMapper.class, + SessionTokenMapper.class, SnapshotMapper.class, UserGroupMapper.class, UserMapper.class, diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokenDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokenDto.java new file mode 100644 index 00000000000..40c7c73e4f3 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokenDto.java @@ -0,0 +1,74 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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.db.user; + +public class SessionTokenDto { + + private String uuid; + private String userUuid; + private long expirationDate; + private long createdAt; + private long updatedAt; + + public String getUuid() { + return uuid; + } + + SessionTokenDto setUuid(String uuid) { + this.uuid = uuid; + return this; + } + + public String getUserUuid() { + return userUuid; + } + + public SessionTokenDto setUserUuid(String userUuid) { + this.userUuid = userUuid; + return this; + } + + public long getExpirationDate() { + return expirationDate; + } + + public SessionTokenDto setExpirationDate(long expirationDate) { + this.expirationDate = expirationDate; + return this; + } + + public long getCreatedAt() { + return createdAt; + } + + SessionTokenDto setCreatedAt(long createdAt) { + this.createdAt = createdAt; + return this; + } + + public long getUpdatedAt() { + return updatedAt; + } + + SessionTokenDto setUpdatedAt(long updatedAt) { + this.updatedAt = updatedAt; + return this; + } +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokenMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokenMapper.java new file mode 100644 index 00000000000..4aad8e8e6f8 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokenMapper.java @@ -0,0 +1,40 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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.db.user; + +import javax.annotation.CheckForNull; +import org.apache.ibatis.annotations.Param; + +public interface SessionTokenMapper { + + @CheckForNull + SessionTokenDto selectByUuid(String uuid); + + void insert(@Param("dto") SessionTokenDto dto); + + void update(@Param("dto") SessionTokenDto dto); + + void deleteByUuid(@Param("uuid") String uuid); + + void deleteByUserUuid(@Param("userUuid") String userUuid); + + int deleteExpired(@Param("now") long now); + +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokensDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokensDao.java new file mode 100644 index 00000000000..b0142cba32f --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokensDao.java @@ -0,0 +1,72 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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.db.user; + +import java.util.Optional; +import org.sonar.api.utils.System2; +import org.sonar.core.util.UuidFactory; +import org.sonar.db.Dao; +import org.sonar.db.DbSession; + +public class SessionTokensDao implements Dao { + + private final System2 system2; + private final UuidFactory uuidFactory; + + public SessionTokensDao(System2 system2, UuidFactory uuidFactory) { + this.system2 = system2; + this.uuidFactory = uuidFactory; + } + + public Optional<SessionTokenDto> selectByUuid(DbSession session, String uuid) { + return Optional.ofNullable(mapper(session).selectByUuid(uuid)); + } + + public SessionTokenDto insert(DbSession session, SessionTokenDto dto) { + long now = system2.now(); + mapper(session).insert(dto + .setUuid(uuidFactory.create()) + .setCreatedAt(now) + .setUpdatedAt(now)); + return dto; + } + + public SessionTokenDto update(DbSession session, SessionTokenDto dto) { + long now = system2.now(); + mapper(session).update(dto.setUpdatedAt(now)); + return dto; + } + + public void deleteByUuid(DbSession dbSession, String uuid) { + mapper(dbSession).deleteByUuid(uuid); + } + + public void deleteByUser(DbSession dbSession, UserDto user) { + mapper(dbSession).deleteByUserUuid(user.getUuid()); + } + + public int deleteExpired(DbSession dbSession) { + return mapper(dbSession).deleteExpired(system2.now()); + } + + private static SessionTokenMapper mapper(DbSession session) { + return session.getMapper(SessionTokenMapper.class); + } +} diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/user/SessionTokenMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/user/SessionTokenMapper.xml new file mode 100644 index 00000000000..4a655eac015 --- /dev/null +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/user/SessionTokenMapper.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "mybatis-3-mapper.dtd"> + +<mapper namespace="org.sonar.db.user.SessionTokenMapper"> + + <sql id="columns"> + st.uuid as uuid, + st.user_uuid as "userUuid", + st.expiration_date as "expirationDate", + st.created_at as "createdAt", + st.updated_at as "updatedAt" + </sql> + + <select id="selectByUuid" parameterType="String" resultType="org.sonar.db.user.SessionTokenDto"> + select + <include refid="columns"/> + from session_tokens st + where st.uuid=#{uuid, jdbcType=VARCHAR} + </select> + + <insert id="insert" parameterType="Map" useGeneratedKeys="false"> + insert into session_tokens + ( + uuid, + user_uuid, + expiration_date, + created_at, + updated_at + ) + values ( + #{dto.uuid, jdbcType=VARCHAR}, + #{dto.userUuid, jdbcType=VARCHAR}, + #{dto.expirationDate, jdbcType=BIGINT}, + #{dto.createdAt, jdbcType=BIGINT}, + #{dto.updatedAt, jdbcType=BIGINT} + ) + </insert> + + <update id="update" parameterType="Map"> + update session_tokens set + expiration_date = #{dto.expirationDate, jdbcType=BIGINT}, + updated_at = #{dto.updatedAt, jdbcType=BIGINT} + where + uuid = #{dto.uuid, jdbcType=VARCHAR} + </update> + + <delete id="deleteByUuid" parameterType="String"> + delete from session_tokens where uuid = #{uuid, jdbcType=VARCHAR} + </delete> + + <delete id="deleteByUserUuid" parameterType="String"> + delete from session_tokens where user_uuid = #{userUuid, jdbcType=VARCHAR} + </delete> + + <delete id="deleteExpired" parameterType="Long" > + delete from session_tokens where expiration_date < #{now, jdbcType=BIGINT} + </delete> + +</mapper> diff --git a/server/sonar-db-dao/src/schema/schema-sq.ddl b/server/sonar-db-dao/src/schema/schema-sq.ddl index 4690b546381..fc2acf81abc 100644 --- a/server/sonar-db-dao/src/schema/schema-sq.ddl +++ b/server/sonar-db-dao/src/schema/schema-sq.ddl @@ -871,6 +871,16 @@ CREATE TABLE "RULES_PROFILES"( ); ALTER TABLE "RULES_PROFILES" ADD CONSTRAINT "PK_RULES_PROFILES" PRIMARY KEY("UUID"); +CREATE TABLE "SESSION_TOKENS"( + "UUID" VARCHAR(40) NOT NULL, + "USER_UUID" VARCHAR(255) NOT NULL, + "EXPIRATION_DATE" BIGINT NOT NULL, + "CREATED_AT" BIGINT NOT NULL, + "UPDATED_AT" BIGINT NOT NULL +); +ALTER TABLE "SESSION_TOKENS" ADD CONSTRAINT "PK_SESSION_TOKENS" PRIMARY KEY("UUID"); +CREATE INDEX "SESSION_TOKENS_USER_UUID" ON "SESSION_TOKENS"("USER_UUID"); + CREATE TABLE "SNAPSHOTS"( "UUID" VARCHAR(50) NOT NULL, "COMPONENT_UUID" VARCHAR(50) NOT NULL, diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/user/SessionTokensDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/user/SessionTokensDaoTest.java new file mode 100644 index 00000000000..63763b264c1 --- /dev/null +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/user/SessionTokensDaoTest.java @@ -0,0 +1,164 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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.db.user; + +import java.util.Optional; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.impl.utils.TestSystem2; +import org.sonar.core.util.SequenceUuidFactory; +import org.sonar.core.util.UuidFactory; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SessionTokensDaoTest { + + private static final long NOW = 1_000_000_000L; + + private TestSystem2 system2 = new TestSystem2().setNow(NOW); + @Rule + public DbTester db = DbTester.create(system2); + + private DbSession dbSession = db.getSession(); + private UuidFactory uuidFactory = new SequenceUuidFactory(); + + private SessionTokensDao underTest = new SessionTokensDao(system2, uuidFactory); + + @Test + public void selectByUuid() { + SessionTokenDto dto = new SessionTokenDto() + .setUserUuid("ABCD") + .setExpirationDate(15_000_000_000L); + underTest.insert(dbSession, dto); + + Optional<SessionTokenDto> result = underTest.selectByUuid(dbSession, dto.getUuid()); + + assertThat(result.isPresent()); + assertThat(result.get().getUserUuid()).isEqualTo("ABCD"); + assertThat(result.get().getExpirationDate()).isEqualTo(15_000_000_000L); + assertThat(result.get().getCreatedAt()).isEqualTo(NOW); + assertThat(result.get().getUpdatedAt()).isEqualTo(NOW); + } + + @Test + public void uuid_created_at_and_updated_at_are_ignored_during_insert() { + SessionTokenDto dto = new SessionTokenDto() + .setUserUuid("ABCD") + .setExpirationDate(15_000_000_000L) + // Following fields should be ignored + .setUuid("SHOULD_NOT_BE_USED") + .setCreatedAt(8_000_000_000L) + .setUpdatedAt(9_000_000_000L); + underTest.insert(dbSession, dto); + + Optional<SessionTokenDto> result = underTest.selectByUuid(dbSession, dto.getUuid()); + + assertThat(result.isPresent()); + assertThat(result.get().getUuid()).isNotEqualTo("SHOULD_NOT_BE_USED"); + assertThat(result.get().getCreatedAt()).isEqualTo(NOW); + assertThat(result.get().getUpdatedAt()).isEqualTo(NOW); + } + + @Test + public void update() { + SessionTokenDto dto = new SessionTokenDto() + .setUserUuid("ABCD") + .setExpirationDate(15_000_000_000L); + underTest.insert(dbSession, dto); + system2.setNow(NOW + 10_000_000_000L); + underTest.update(dbSession, dto + .setExpirationDate(45_000_000_000L)); + + Optional<SessionTokenDto> result = underTest.selectByUuid(dbSession, dto.getUuid()); + + assertThat(result.get().getExpirationDate()).isEqualTo(45_000_000_000L); + assertThat(result.get().getCreatedAt()).isEqualTo(NOW); + assertThat(result.get().getUpdatedAt()).isEqualTo(NOW + 10_000_000_000L); + } + + @Test + public void only_update_fields_that_makes_sense() { + SessionTokenDto dto = new SessionTokenDto() + .setUserUuid("ABCD") + .setExpirationDate(15_000_000_000L); + underTest.insert(dbSession, dto); + system2.setNow(NOW + 10_000_000_000L); + underTest.update(dbSession, dto + .setExpirationDate(45_000_000_000L) + // Following fields are ignored + .setUserUuid("ANOTHER USER UUID") + .setCreatedAt(NOW -10_000_000_000L) + ); + + Optional<SessionTokenDto> result = underTest.selectByUuid(dbSession, dto.getUuid()); + + assertThat(result.get().getExpirationDate()).isEqualTo(45_000_000_000L); + assertThat(result.get().getUserUuid()).isEqualTo("ABCD"); + assertThat(result.get().getCreatedAt()).isEqualTo(NOW); + assertThat(result.get().getUpdatedAt()).isEqualTo(NOW + 10_000_000_000L); + } + + @Test + public void deleteByUuid() { + UserDto user = db.users().insertUser(); + SessionTokenDto sessionToken1 = db.users().insertSessionToken(user); + SessionTokenDto sessionToken2 = db.users().insertSessionToken(user); + UserDto anotherUser = db.users().insertUser(); + SessionTokenDto anotherSessionToken = db.users().insertSessionToken(anotherUser); + + underTest.deleteByUuid(dbSession, sessionToken1.getUuid()); + + assertThat(underTest.selectByUuid(dbSession, sessionToken1.getUuid())).isNotPresent(); + assertThat(underTest.selectByUuid(dbSession, sessionToken2.getUuid())).isPresent(); + assertThat(underTest.selectByUuid(dbSession, anotherSessionToken.getUuid())).isPresent(); + } + + @Test + public void deleteByUser() { + UserDto user = db.users().insertUser(); + SessionTokenDto sessionToken = db.users().insertSessionToken(user); + // Creation another session token linked on another user, it should not be removed + UserDto anotherUser = db.users().insertUser(); + SessionTokenDto anotherSessionToken = db.users().insertSessionToken(anotherUser); + + underTest.deleteByUser(dbSession, user); + + assertThat(underTest.selectByUuid(dbSession, sessionToken.getUuid())).isNotPresent(); + assertThat(underTest.selectByUuid(dbSession, anotherSessionToken.getUuid())).isPresent(); + } + + @Test + public void deleteExpired() { + UserDto user = db.users().insertUser(); + SessionTokenDto expiredSessionToken1 = db.users().insertSessionToken(user, st -> st.setExpirationDate(NOW - 1_000_000_000L)); + SessionTokenDto expiredSessionToken2 = db.users().insertSessionToken(user, st -> st.setExpirationDate(NOW - 1_000_000_000L)); + SessionTokenDto validSessionToken = db.users().insertSessionToken(user); + + int result = underTest.deleteExpired(dbSession); + + assertThat(underTest.selectByUuid(dbSession, expiredSessionToken1.getUuid())).isNotPresent(); + assertThat(underTest.selectByUuid(dbSession, expiredSessionToken2.getUuid())).isNotPresent(); + assertThat(underTest.selectByUuid(dbSession, validSessionToken.getUuid())).isPresent(); + assertThat(result).isEqualTo(2); + } +} diff --git a/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java b/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java index 93bc6d09710..079e15fe109 100644 --- a/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java +++ b/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java @@ -41,6 +41,7 @@ import org.sonar.db.permission.UserPermissionDto; import static com.google.common.base.Preconditions.checkArgument; import static java.lang.String.format; import static java.util.Arrays.stream; +import static org.apache.commons.lang.math.RandomUtils.nextLong; import static org.sonar.db.permission.OrganizationPermission.ADMINISTER; public class UserDbTester { @@ -378,6 +379,8 @@ public class UserDbTester { .collect(MoreCollectors.toList()); } + // USER TOKEN + @SafeVarargs public final UserTokenDto insertToken(UserDto user, Consumer<UserTokenDto>... populators) { UserTokenDto dto = UserTokenTesting.newUserToken().setUserUuid(user.getUuid()); @@ -387,4 +390,17 @@ public class UserDbTester { return dto; } + // SESSION TOKENS + + @SafeVarargs + public final SessionTokenDto insertSessionToken(UserDto user, Consumer<SessionTokenDto>... populators) { + SessionTokenDto dto = new SessionTokenDto() + .setUserUuid(user.getUuid()) + .setExpirationDate(nextLong()); + stream(populators).forEach(p -> p.accept(dto)); + db.getDbClient().sessionTokensDao().insert(db.getSession(), dto); + db.commit(); + return dto; + } + } diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/def/VarcharColumnDef.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/def/VarcharColumnDef.java index a4f62523460..41d45667c74 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/def/VarcharColumnDef.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/def/VarcharColumnDef.java @@ -43,6 +43,13 @@ public class VarcharColumnDef extends AbstractColumnDef { public static final int UUID_VARCHAR_SIZE = 50; public static final int UUID_SIZE = 40; + /** + * UUID length of the USERS table is not using the standard UUID length. + * The reason of this is because when the UUID column was introduced in the USERS table, existing rows were fed with the login, which has a length of 255. + * @see <a https://jira.sonarsource.com/browse/SONAR-10597>SONAR-10597</a> + */ + public static final int USER_UUID_SIZE = 255; + private final int columnSize; private final boolean ignoreOracleUnit; diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v00/CreateInitialSchema.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v00/CreateInitialSchema.java index c22374418c1..2f8b95aa499 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v00/CreateInitialSchema.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v00/CreateInitialSchema.java @@ -44,6 +44,7 @@ import static org.sonar.server.platform.db.migration.def.IntegerColumnDef.newInt import static org.sonar.server.platform.db.migration.def.TimestampColumnDef.newTimestampColumnDefBuilder; import static org.sonar.server.platform.db.migration.def.TinyIntColumnDef.newTinyIntColumnDefBuilder; import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.MAX_SIZE; +import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.USER_UUID_SIZE; import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE; import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder; import static org.sonar.server.platform.db.migration.sql.CreateTableBuilder.PRIMARY_KEY_PREFIX; @@ -241,7 +242,7 @@ public class CreateInitialSchema extends DdlChange { .addColumn(mainIsLastKeyCol) .addColumn(isLastCol) .addColumn(isLastKeyCol) - .addColumn(newLenientVarcharBuilder("submitter_uuid").setLimit(255).setIsNullable(true).build()) + .addColumn(newLenientVarcharBuilder("submitter_uuid").setLimit(USER_UUID_SIZE).setIsNullable(true).build()) .addColumn(newBigIntegerColumnDefBuilder().setColumnName("submitted_at").setIsNullable(false).build()) .addColumn(newBigIntegerColumnDefBuilder().setColumnName("started_at").setIsNullable(true).build()) .addColumn(newBigIntegerColumnDefBuilder().setColumnName("executed_at").setIsNullable(true).build()) @@ -277,7 +278,7 @@ public class CreateInitialSchema extends DdlChange { .addColumn(mainComponentUuidCol) .addColumn(componentUuidCol) .addColumn(newLenientVarcharBuilder("status").setLimit(15).setIsNullable(true).build()) - .addColumn(newLenientVarcharBuilder("submitter_uuid").setLimit(255).setIsNullable(true).build()) + .addColumn(newLenientVarcharBuilder("submitter_uuid").setLimit(USER_UUID_SIZE).setIsNullable(true).build()) .addColumn(newBigIntegerColumnDefBuilder().setColumnName("started_at").setIsNullable(true).build()) .addColumn(newVarcharColumnBuilder("worker_uuid").setLimit(UUID_SIZE).setIsNullable(true).build()) .addColumn(newIntegerColumnDefBuilder().setColumnName("execution_count").setIsNullable(false).build()) @@ -558,7 +559,7 @@ public class CreateInitialSchema extends DdlChange { .addPkColumn(newBigIntegerColumnDefBuilder().setColumnName("id").setIsNullable(false).build(), AUTO_INCREMENT) .addColumn(keeCol) .addColumn(issueKeyCol) - .addColumn(newLenientVarcharBuilder("user_login").setLimit(255).build()) + .addColumn(newLenientVarcharBuilder("user_login").setLimit(USER_UUID_SIZE).build()) .addColumn(newLenientVarcharBuilder("change_type").setLimit(20).build()) .addColumn(newClobColumnDefBuilder().setColumnName("change_data").build()) .addColumn(NULLABLE_TECHNICAL_CREATED_AT_COL) @@ -570,7 +571,7 @@ public class CreateInitialSchema extends DdlChange { } private void createIssues(Context context) { - VarcharColumnDef assigneeCol = newLenientVarcharBuilder("assignee").setLimit(255).build(); + VarcharColumnDef assigneeCol = newLenientVarcharBuilder("assignee").setLimit(USER_UUID_SIZE).build(); VarcharColumnDef componentUuidCol = newLenientVarcharBuilder(COMPONENT_UUID_COL_NAME).setLimit(50).build(); BigIntegerColumnDef issueCreationDateCol = newBigIntegerColumnDefBuilder().setColumnName("issue_creation_date").build(); VarcharColumnDef keeCol = newLenientVarcharBuilder("kee").setLimit(50).setIsNullable(false).build(); @@ -592,7 +593,7 @@ public class CreateInitialSchema extends DdlChange { .addColumn(newLenientVarcharBuilder("status").setLimit(20).build()) .addColumn(resolutionCol) .addColumn(newLenientVarcharBuilder("checksum").setLimit(1000).build()) - .addColumn(newLenientVarcharBuilder("reporter").setLimit(255).build()) + .addColumn(newLenientVarcharBuilder("reporter").setLimit(USER_UUID_SIZE).build()) .addColumn(assigneeCol) .addColumn(newLenientVarcharBuilder("author_login").setLimit(255).build()) .addColumn(newLenientVarcharBuilder("action_plan_key").setLimit(50).build()) @@ -651,7 +652,7 @@ public class CreateInitialSchema extends DdlChange { .addColumn(newIntegerColumnDefBuilder().setColumnName(METRIC_ID_COL_NAME).setIsNullable(false).build()) .addColumn(newDecimalColumnDefBuilder().setColumnName("value").setPrecision(38).setScale(20).build()) .addColumn(newLenientVarcharBuilder("text_value").setLimit(MAX_SIZE).build()) - .addColumn(newLenientVarcharBuilder(USER_UUID_COL_NAME).setLimit(255).build()) + .addColumn(newLenientVarcharBuilder(USER_UUID_COL_NAME).setLimit(USER_UUID_SIZE).build()) .addColumn(newLenientVarcharBuilder(DESCRIPTION_COL_NAME).setLimit(MAX_SIZE).build()) .addColumn(NULLABLE_TECHNICAL_CREATED_AT_COL) .addColumn(NULLABLE_TECHNICAL_UPDATED_AT_COL) @@ -737,7 +738,7 @@ public class CreateInitialSchema extends DdlChange { .addColumn(almAppInstallUuidCol) .addColumn(newVarcharColumnBuilder("alm_id").setIsNullable(false).setLimit(UUID_SIZE).build()) .addColumn(newVarcharColumnBuilder("url").setIsNullable(false).setLimit(2000).build()) - .addColumn(newVarcharColumnBuilder(USER_UUID_COL_NAME).setIsNullable(false).setLimit(255).build()) + .addColumn(newVarcharColumnBuilder(USER_UUID_COL_NAME).setIsNullable(false).setLimit(USER_UUID_SIZE).build()) .addColumn(newBooleanColumnDefBuilder().setColumnName("members_sync_enabled").setIsNullable(true).build()) .addColumn(TECHNICAL_CREATED_AT_COL) .build()); @@ -1049,7 +1050,7 @@ public class CreateInitialSchema extends DdlChange { .addPkColumn(newLenientVarcharBuilder("kee").setLimit(UUID_SIZE).setIsNullable(false).build()) .addColumn(rulesProfileUuidCol) .addColumn(newLenientVarcharBuilder("change_type").setLimit(20).setIsNullable(false).build()) - .addColumn(newLenientVarcharBuilder(USER_UUID_COL_NAME).setLimit(255).setIsNullable(true).build()) + .addColumn(newLenientVarcharBuilder(USER_UUID_COL_NAME).setLimit(USER_UUID_SIZE).setIsNullable(true).build()) .addColumn(newClobColumnDefBuilder().setColumnName("change_data").setIsNullable(true).build()) .addColumn(TECHNICAL_CREATED_AT_COL) .build()); @@ -1164,7 +1165,7 @@ public class CreateInitialSchema extends DdlChange { .addPkColumn(newIntegerColumnDefBuilder().setColumnName("rule_id").setIsNullable(false).build()) .addPkColumn(newVarcharColumnBuilder(ORGANIZATION_UUID_COL_NAME).setLimit(UUID_SIZE).setIsNullable(false).build()) .addColumn(newClobColumnDefBuilder().setColumnName("note_data").setIsNullable(true).build()) - .addColumn(newVarcharColumnBuilder("note_user_uuid").setLimit(255).setIsNullable(true).build()) + .addColumn(newVarcharColumnBuilder("note_user_uuid").setLimit(USER_UUID_SIZE).setIsNullable(true).build()) .addColumn(newBigIntegerColumnDefBuilder().setColumnName("note_created_at").setIsNullable(true).build()) .addColumn(newBigIntegerColumnDefBuilder().setColumnName("note_updated_at").setIsNullable(true).build()) .addColumn(newVarcharColumnBuilder("remediation_function").setLimit(20).setIsNullable(true).build()) @@ -1253,7 +1254,7 @@ public class CreateInitialSchema extends DdlChange { private void createUserProperties(Context context) { String tableName = "user_properties"; - VarcharColumnDef userUuidCol = newVarcharColumnBuilder(USER_UUID_COL_NAME).setLimit(255).setIsNullable(false).build(); + VarcharColumnDef userUuidCol = newVarcharColumnBuilder(USER_UUID_COL_NAME).setLimit(USER_UUID_SIZE).setIsNullable(false).build(); VarcharColumnDef keyCol = newVarcharColumnBuilder("kee").setLimit(100).setIsNullable(false).build(); context.execute(newTableBuilder(tableName) .addPkColumn(newVarcharColumnBuilder("uuid").setLimit(UUID_SIZE).setIsNullable(false).build()) @@ -1284,7 +1285,7 @@ public class CreateInitialSchema extends DdlChange { private void createUserTokens(Context context) { String tableName = "user_tokens"; - VarcharColumnDef userUuidCol = newVarcharColumnBuilder(USER_UUID_COL_NAME).setLimit(255).setIsNullable(false).build(); + VarcharColumnDef userUuidCol = newVarcharColumnBuilder(USER_UUID_COL_NAME).setLimit(USER_UUID_SIZE).setIsNullable(false).build(); VarcharColumnDef nameCol = newVarcharColumnBuilder("name").setLimit(100).setIsNullable(false).build(); VarcharColumnDef tokenHashCol = newVarcharColumnBuilder("token_hash").setLimit(255).setIsNullable(false).build(); context.execute( @@ -1302,7 +1303,7 @@ public class CreateInitialSchema extends DdlChange { private void createUsers(Context context) { String tableName = "users"; - VarcharColumnDef uuidCol = newVarcharColumnBuilder("uuid").setLimit(255).setIsNullable(false).build(); + VarcharColumnDef uuidCol = newVarcharColumnBuilder("uuid").setLimit(USER_UUID_SIZE).setIsNullable(false).build(); VarcharColumnDef loginCol = newLenientVarcharBuilder("login").setLimit(255).setIsNullable(false).build(); BigIntegerColumnDef updatedAtCol = NULLABLE_TECHNICAL_UPDATED_AT_COL; VarcharColumnDef externalLoginCol = newLenientVarcharBuilder("external_login").setLimit(255).setIsNullable(false).build(); diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v83/DbVersion83.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v83/DbVersion83.java index 000f8bcf457..35918f7e5bb 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v83/DbVersion83.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v83/DbVersion83.java @@ -47,7 +47,6 @@ public class DbVersion83 implements DbVersion { .add(3309, "Migrate 'resource_id' to 'component_uuid' in 'user_roles'", MigrateResourceIdToUuidInUserRoles.class) .add(3310, "Remove column 'resource_id' in 'user_roles'", DropResourceIdFromUserRolesTable.class) .add(3311, "Remove column 'id' in 'components'", DropIdFromComponentsTable.class) - ; } } diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/CreateSessionTokensTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/CreateSessionTokensTable.java new file mode 100644 index 00000000000..e4abbad3ae3 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/CreateSessionTokensTable.java @@ -0,0 +1,76 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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.platform.db.migration.version.v84; + +import java.sql.SQLException; +import org.sonar.db.Database; +import org.sonar.server.platform.db.migration.def.VarcharColumnDef; +import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder; +import org.sonar.server.platform.db.migration.sql.CreateTableBuilder; +import org.sonar.server.platform.db.migration.step.DdlChange; + +import static org.sonar.server.platform.db.migration.def.BigIntegerColumnDef.newBigIntegerColumnDefBuilder; +import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.USER_UUID_SIZE; +import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE; + +public class CreateSessionTokensTable extends DdlChange { + + private static final String TABLE_NAME = "session_tokens"; + private static final VarcharColumnDef USER_UUID_COLUMN = VarcharColumnDef.newVarcharColumnDefBuilder() + .setColumnName("user_uuid") + .setLimit(USER_UUID_SIZE) + .setIsNullable(false) + .build(); + + public CreateSessionTokensTable(Database db) { + super(db); + } + + @Override + public void execute(Context context) throws SQLException { + context.execute(new CreateTableBuilder(getDialect(), TABLE_NAME) + .addPkColumn(VarcharColumnDef.newVarcharColumnDefBuilder() + .setColumnName("uuid") + .setLimit(UUID_SIZE) + .setIsNullable(false) + .build()) + .addColumn(USER_UUID_COLUMN) + .addColumn(newBigIntegerColumnDefBuilder() + .setColumnName("expiration_date") + .setIsNullable(false) + .build()) + .addColumn(newBigIntegerColumnDefBuilder() + .setColumnName("created_at") + .setIsNullable(false) + .build()) + .addColumn(newBigIntegerColumnDefBuilder() + .setColumnName("updated_at") + .setIsNullable(false) + .build()) + .build()); + + context.execute(new CreateIndexBuilder() + .setTable(TABLE_NAME) + .setName("session_tokens_user_uuid") + .addColumn(USER_UUID_COLUMN) + .setUnique(false) + .build()); + } +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/DbVersion84.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/DbVersion84.java index 72702ae7791..1d4eac7aed2 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/DbVersion84.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/DbVersion84.java @@ -780,8 +780,8 @@ public class DbVersion84 implements DbVersion { .add(3710, "Add primary key on 'UUID' column of 'RULES' table", AddPrimaryKeyOnUuidColumnOfRulesTable.class) .add(3711, "Drop column 'ID' of 'RULES' table", DropIdColumnOfRulesTable.class) - .add(3800, "Remove favourites for components with qualifiers 'DIR', 'FIL', 'UTS'", RemoveFilesFavouritesFromProperties.class); - + .add(3800, "Remove favourites for components with qualifiers 'DIR', 'FIL', 'UTS'", RemoveFilesFavouritesFromProperties.class) + .add(3801, "Create 'SESSION_TOKENS' table", CreateSessionTokensTable.class) ; } } diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v84/CreateSessionTokensTableTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v84/CreateSessionTokensTableTest.java new file mode 100644 index 00000000000..c66d83b9f0b --- /dev/null +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v84/CreateSessionTokensTableTest.java @@ -0,0 +1,59 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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.platform.db.migration.version.v84; + +import java.sql.SQLException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.db.CoreDbTester; + +import static java.sql.Types.BIGINT; +import static java.sql.Types.VARCHAR; + +public class CreateSessionTokensTableTest { + + private static final String TABLE_NAME = "session_tokens"; + + @Rule + public CoreDbTester dbTester = CoreDbTester.createEmpty(); + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private CreateSessionTokensTable underTest = new CreateSessionTokensTable(dbTester.database()); + + @Test + public void table_has_been_created() throws SQLException { + underTest.execute(); + + dbTester.assertTableExists(TABLE_NAME); + dbTester.assertPrimaryKey(TABLE_NAME, "pk_session_tokens", "uuid"); + dbTester.assertIndex(TABLE_NAME, "session_tokens_user_uuid", "user_uuid"); + + dbTester.assertColumnDefinition(TABLE_NAME, "uuid", VARCHAR, 40, false); + dbTester.assertColumnDefinition(TABLE_NAME, "user_uuid", VARCHAR, 255, false); + dbTester.assertColumnDefinition(TABLE_NAME, "expiration_date", BIGINT, 20, false); + dbTester.assertColumnDefinition(TABLE_NAME, "updated_at", BIGINT, 20, false); + dbTester.assertColumnDefinition(TABLE_NAME, "created_at", BIGINT, 20, false); + } + +} diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java index 35e49f833e0..c2619579720 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java @@ -21,30 +21,34 @@ package org.sonar.server.authentication; import org.sonar.core.platform.Module; import org.sonar.server.authentication.event.AuthenticationEventImpl; +import org.sonar.server.authentication.purge.SessionTokensCleaner; +import org.sonar.server.authentication.purge.SessionTokensCleanerExecutorServiceImpl; public class AuthenticationModule extends Module { @Override protected void configureModule() { add( AuthenticationEventImpl.class, - InitFilter.class, - OAuth2CallbackFilter.class, - IdentityProviderRepository.class, BaseContextFactory.class, - OAuth2ContextFactory.class, - UserRegistrarImpl.class, - OAuthCsrfVerifier.class, - UserSessionInitializer.class, - JwtSerializer.class, - JwtHttpHandler.class, - JwtCsrfVerifier.class, - OAuth2AuthenticationParametersImpl.class, + BasicAuthentication.class, CredentialsAuthentication.class, - CredentialsLocalAuthentication.class, CredentialsExternalAuthentication.class, - BasicAuthentication.class, + CredentialsLocalAuthentication.class, HttpHeadersAuthentication.class, + IdentityProviderRepository.class, + InitFilter.class, + JwtCsrfVerifier.class, + JwtHttpHandler.class, + JwtSerializer.class, + OAuth2AuthenticationParametersImpl.class, + OAuth2CallbackFilter.class, + OAuth2ContextFactory.class, + OAuthCsrfVerifier.class, RequestAuthenticatorImpl.class, - UserLastConnectionDatesUpdaterImpl.class); + SessionTokensCleaner.class, + SessionTokensCleanerExecutorServiceImpl.class, + UserLastConnectionDatesUpdaterImpl.class, + UserRegistrarImpl.class, + UserSessionInitializer.class); } } diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java index a46245a0ab0..699c771d901 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java @@ -34,6 +34,7 @@ import org.sonar.api.server.ServerSide; import org.sonar.api.utils.System2; import org.sonar.db.DbClient; import org.sonar.db.DbSession; +import org.sonar.db.user.SessionTokenDto; import org.sonar.db.user.UserDto; import static com.google.common.base.Preconditions.checkArgument; @@ -79,10 +80,13 @@ public class JwtHttpHandler { public void generateToken(UserDto user, Map<String, Object> properties, HttpServletRequest request, HttpServletResponse response) { String csrfState = jwtCsrfVerifier.generateState(request, response, sessionTimeoutInSeconds); + long expirationTime = system2.now() + sessionTimeoutInSeconds * 1000L; + SessionTokenDto sessionToken = createSessionToken(user, expirationTime); String token = jwtSerializer.encode(new JwtSerializer.JwtSession( user.getUuid(), - sessionTimeoutInSeconds, + sessionToken.getUuid(), + expirationTime, ImmutableMap.<String, Object>builder() .putAll(properties) .put(LAST_REFRESH_TIME_PARAM, system2.now()) @@ -91,16 +95,24 @@ public class JwtHttpHandler { response.addCookie(createCookie(request, JWT_COOKIE, token, sessionTimeoutInSeconds)); } + private SessionTokenDto createSessionToken(UserDto user, long expirationTime) { + try (DbSession dbSession = dbClient.openSession(false)) { + SessionTokenDto sessionToken = new SessionTokenDto() + .setUserUuid(user.getUuid()) + .setExpirationDate(expirationTime); + dbClient.sessionTokensDao().insert(dbSession, sessionToken); + dbSession.commit(); + return sessionToken; + } + } + public void generateToken(UserDto user, HttpServletRequest request, HttpServletResponse response) { generateToken(user, Collections.emptyMap(), request, response); } public Optional<UserDto> validateToken(HttpServletRequest request, HttpServletResponse response) { Optional<Token> token = getToken(request, response); - if (token.isPresent()) { - return Optional.of(token.get().getUserDto()); - } - return Optional.empty(); + return token.map(Token::getUserDto); } public Optional<Token> getToken(HttpServletRequest request, HttpServletResponse response) { @@ -108,7 +120,9 @@ public class JwtHttpHandler { if (!encodedToken.isPresent()) { return Optional.empty(); } - return validateToken(encodedToken.get(), request, response); + try (DbSession dbSession = dbClient.openSession(false)) { + return validateToken(dbSession, encodedToken.get(), request, response); + } } private static Optional<String> getTokenFromCookie(HttpServletRequest request) { @@ -124,24 +138,32 @@ public class JwtHttpHandler { return Optional.of(token); } - private Optional<Token> validateToken(String tokenEncoded, HttpServletRequest request, HttpServletResponse response) { + private Optional<Token> validateToken(DbSession dbSession, String tokenEncoded, HttpServletRequest request, HttpServletResponse response) { Optional<Claims> claims = jwtSerializer.decode(tokenEncoded); if (!claims.isPresent()) { return Optional.empty(); } + Claims token = claims.get(); + Optional<SessionTokenDto> sessionToken = dbClient.sessionTokensDao().selectByUuid(dbSession, token.getId()); + if (!sessionToken.isPresent()) { + return Optional.empty(); + } + // Check on expiration is already done when decoding the JWT token, but here is done a double check with the expiration date from DB. Date now = new Date(system2.now()); - Claims token = claims.get(); + if (now.getTime() > sessionToken.get().getExpirationDate()) { + return Optional.empty(); + } if (now.after(addSeconds(token.getIssuedAt(), SESSION_DISCONNECT_IN_SECONDS))) { return Optional.empty(); } jwtCsrfVerifier.verifyState(request, (String) token.get(CSRF_JWT_PARAM), token.getSubject()); if (now.after(addSeconds(getLastRefreshDate(token), SESSION_REFRESH_IN_SECONDS))) { - refreshToken(token, request, response); + refreshToken(dbSession, sessionToken.get(), token, request, response); } - Optional<UserDto> user = selectUserFromUuid(token.getSubject()); + Optional<UserDto> user = selectUserFromUuid(dbSession, token.getSubject()); return user.map(userDto -> new Token(userDto, claims.get())); } @@ -151,26 +173,44 @@ public class JwtHttpHandler { return new Date(lastFreshTime); } - private void refreshToken(Claims token, HttpServletRequest request, HttpServletResponse response) { - String refreshToken = jwtSerializer.refresh(token, sessionTimeoutInSeconds); + private void refreshToken(DbSession dbSession, SessionTokenDto tokenFromDb, Claims tokenFromCookie, HttpServletRequest request, HttpServletResponse response) { + long expirationTime = system2.now() + sessionTimeoutInSeconds * 1000L; + String refreshToken = jwtSerializer.refresh(tokenFromCookie, expirationTime); response.addCookie(createCookie(request, JWT_COOKIE, refreshToken, sessionTimeoutInSeconds)); - jwtCsrfVerifier.refreshState(request, response, (String) token.get(CSRF_JWT_PARAM), sessionTimeoutInSeconds); + jwtCsrfVerifier.refreshState(request, response, (String) tokenFromCookie.get(CSRF_JWT_PARAM), sessionTimeoutInSeconds); + + dbClient.sessionTokensDao().update(dbSession, tokenFromDb.setExpirationDate(expirationTime)); + dbSession.commit(); } public void removeToken(HttpServletRequest request, HttpServletResponse response) { + removeSessionToken(request); response.addCookie(createCookie(request, JWT_COOKIE, null, 0)); jwtCsrfVerifier.removeState(request, response); } + private void removeSessionToken(HttpServletRequest request) { + Optional<Cookie> jwtCookie = findCookie(JWT_COOKIE, request); + if (!jwtCookie.isPresent()) { + return; + } + Optional<Claims> claims = jwtSerializer.decode(jwtCookie.get().getValue()); + if (!claims.isPresent()) { + return; + } + try (DbSession dbSession = dbClient.openSession(false)) { + dbClient.sessionTokensDao().deleteByUuid(dbSession, claims.get().getId()); + dbSession.commit(); + } + } + private static Cookie createCookie(HttpServletRequest request, String name, @Nullable String value, int expirationInSeconds) { return newCookieBuilder(request).setName(name).setValue(value).setHttpOnly(true).setExpiry(expirationInSeconds).build(); } - private Optional<UserDto> selectUserFromUuid(String userUuid) { - try (DbSession dbSession = dbClient.openSession(false)) { - UserDto user = dbClient.userDao().selectByUuid(dbSession, userUuid); - return Optional.ofNullable(user != null && user.isActive() ? user : null); - } + private Optional<UserDto> selectUserFromUuid(DbSession dbSession, String userUuid) { + UserDto user = dbClient.userDao().selectByUuid(dbSession, userUuid); + return Optional.ofNullable(user != null && user.isActive() ? user : null); } private static int getSessionTimeoutInSeconds(Configuration config) { diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtSerializer.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtSerializer.java index cfd06edf931..3aba1994598 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtSerializer.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtSerializer.java @@ -25,7 +25,7 @@ import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.security.SignatureException; import java.util.Base64; import java.util.Collections; import java.util.Date; @@ -38,7 +38,6 @@ import org.sonar.api.Startable; import org.sonar.api.config.Configuration; import org.sonar.api.server.ServerSide; import org.sonar.api.utils.System2; -import org.sonar.core.util.UuidFactory; import org.sonar.server.authentication.event.AuthenticationEvent.Source; import org.sonar.server.authentication.event.AuthenticationException; @@ -57,14 +56,12 @@ public class JwtSerializer implements Startable { private final Configuration config; private final System2 system2; - private final UuidFactory uuidFactory; private SecretKey secretKey; - public JwtSerializer(Configuration config, System2 system2, UuidFactory uuidFactory) { + public JwtSerializer(Configuration config, System2 system2) { this.config = config; this.system2 = system2; - this.uuidFactory = uuidFactory; } @VisibleForTesting @@ -75,22 +72,19 @@ public class JwtSerializer implements Startable { @Override public void start() { Optional<String> encodedKey = config.get(AUTH_JWT_SECRET.getKey()); - if (encodedKey.isPresent()) { - this.secretKey = decodeSecretKeyProperty(encodedKey.get()); - } else { - this.secretKey = generateSecretKey(); - } + this.secretKey = encodedKey + .map(JwtSerializer::decodeSecretKeyProperty) + .orElseGet(JwtSerializer::generateSecretKey); } String encode(JwtSession jwtSession) { checkIsStarted(); - long now = system2.now(); JwtBuilder jwtBuilder = Jwts.builder() - .setId(uuidFactory.create()) + .setId(jwtSession.getSessionTokenUuid()) .setSubject(jwtSession.getUserLogin()) - .setIssuedAt(new Date(now)) - .setExpiration(new Date(now + jwtSession.getExpirationTimeInSeconds() * 1000)) - .signWith(SIGNATURE_ALGORITHM, secretKey); + .setIssuedAt(new Date(system2.now())) + .setExpiration(new Date(jwtSession.getExpirationTime())) + .signWith(secretKey, SIGNATURE_ALGORITHM); for (Map.Entry<String, Object> entry : jwtSession.getProperties().entrySet()) { jwtBuilder.claim(entry.getKey(), entry.getValue()); } @@ -101,9 +95,10 @@ public class JwtSerializer implements Startable { checkIsStarted(); Claims claims = null; try { - claims = Jwts.parser() + claims = (Claims) Jwts.parserBuilder() .setSigningKey(secretKey) - .parseClaimsJws(token) + .build() + .parse(token) .getBody(); requireNonNull(claims.getId(), "Token id hasn't been found"); requireNonNull(claims.getSubject(), "Token subject hasn't been found"); @@ -121,15 +116,14 @@ public class JwtSerializer implements Startable { } } - String refresh(Claims token, int expirationTimeInSeconds) { + String refresh(Claims token, long expirationTime) { checkIsStarted(); - long now = system2.now(); JwtBuilder jwtBuilder = Jwts.builder(); for (Map.Entry<String, Object> entry : token.entrySet()) { jwtBuilder.claim(entry.getKey(), entry.getValue()); } - jwtBuilder.setExpiration(new Date(now + expirationTimeInSeconds * 1_000L)) - .signWith(SIGNATURE_ALGORITHM, secretKey); + jwtBuilder.setExpiration(new Date(expirationTime)) + .signWith(secretKey, SIGNATURE_ALGORITHM); return jwtBuilder.compact(); } @@ -155,16 +149,18 @@ public class JwtSerializer implements Startable { static class JwtSession { private final String userLogin; - private final long expirationTimeInSeconds; + private final String sessionTokenUuid; + private final long expirationTime; private final Map<String, Object> properties; - JwtSession(String userLogin, long expirationTimeInSeconds) { - this(userLogin, expirationTimeInSeconds, Collections.emptyMap()); + JwtSession(String userLogin, String sessionTokenUuid, long expirationTime) { + this(userLogin, sessionTokenUuid, expirationTime, Collections.emptyMap()); } - JwtSession(String userLogin, long expirationTimeInSeconds, Map<String, Object> properties) { + JwtSession(String userLogin, String sessionTokenUuid, long expirationTime, Map<String, Object> properties) { this.userLogin = requireNonNull(userLogin, "User login cannot be null"); - this.expirationTimeInSeconds = expirationTimeInSeconds; + this.sessionTokenUuid = requireNonNull(sessionTokenUuid, "Session token UUID cannot be null"); + this.expirationTime = expirationTime; this.properties = properties; } @@ -172,8 +168,12 @@ public class JwtSerializer implements Startable { return userLogin; } - long getExpirationTimeInSeconds() { - return expirationTimeInSeconds; + String getSessionTokenUuid() { + return sessionTokenUuid; + } + + long getExpirationTime() { + return expirationTime; } Map<String, Object> getProperties() { diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleaner.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleaner.java new file mode 100644 index 00000000000..d5764bf3fd8 --- /dev/null +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleaner.java @@ -0,0 +1,74 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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.purge; + +import java.util.concurrent.TimeUnit; +import org.sonar.api.Startable; +import org.sonar.api.config.Configuration; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.server.util.GlobalLockManager; + +public class SessionTokensCleaner implements Startable { + + private static final Logger LOG = Loggers.get(SessionTokensCleaner.class); + + private static final String PURGE_DELAY_CONFIGURATION = "sonar.authentication.session.tokens.purge.delay"; + private static final long DEFAULT_PURGE_DELAY_IN_SECONDS = 24 * 60 * 60L; + private static final String LOCK_NAME = "SessionCleaner"; + + private final SessionTokensCleanerExecutorService executorService; + private final DbClient dbClient; + private final Configuration configuration; + private final GlobalLockManager lockManager; + + public SessionTokensCleaner(SessionTokensCleanerExecutorService executorService, DbClient dbClient, Configuration configuration, GlobalLockManager lockManager) { + this.executorService = executorService; + this.dbClient = dbClient; + this.configuration = configuration; + this.lockManager = lockManager; + } + + @Override + public void start() { + this.executorService.scheduleAtFixedRate(this::executePurge, 0, configuration.getLong(PURGE_DELAY_CONFIGURATION).orElse(DEFAULT_PURGE_DELAY_IN_SECONDS), TimeUnit.SECONDS); + } + + private void executePurge() { + if (!lockManager.tryLock(LOCK_NAME)) { + return; + } + LOG.debug("Start of cleaning expired session tokens"); + try (DbSession dbSession = dbClient.openSession(false)) { + int deletedSessionTokens = dbClient.sessionTokensDao().deleteExpired(dbSession); + dbSession.commit(); + LOG.info("Purge of expired session tokens has removed {} elements", deletedSessionTokens); + } + } + + @Override + public void stop() { + // nothing to do + } + +} diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorService.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorService.java new file mode 100644 index 00000000000..551363c6944 --- /dev/null +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorService.java @@ -0,0 +1,27 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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.purge; + +import java.util.concurrent.ScheduledExecutorService; +import org.sonar.api.server.ServerSide; + +@ServerSide +public interface SessionTokensCleanerExecutorService extends ScheduledExecutorService { +} diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorServiceImpl.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorServiceImpl.java new file mode 100644 index 00000000000..3a0bbadb7a6 --- /dev/null +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorServiceImpl.java @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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.purge; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import org.sonar.server.util.AbstractStoppableScheduledExecutorServiceImpl; + +import static java.lang.Thread.MIN_PRIORITY; + +public class SessionTokensCleanerExecutorServiceImpl + extends AbstractStoppableScheduledExecutorServiceImpl<ScheduledExecutorService> + implements SessionTokensCleanerExecutorService { + + public SessionTokensCleanerExecutorServiceImpl() { + super( + Executors.newSingleThreadScheduledExecutor(r -> { + Thread thread = Executors.defaultThreadFactory().newThread(r); + thread.setName("SessionTokensCleaner-%d"); + thread.setPriority(MIN_PRIORITY); + thread.setDaemon(false); + return thread; + })); + } +} diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/AuthenticationModuleTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/AuthenticationModuleTest.java index 34f83b73dd7..d3dccc5a21c 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/AuthenticationModuleTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/AuthenticationModuleTest.java @@ -31,7 +31,7 @@ public class AuthenticationModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new AuthenticationModule().configure(container); - assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 20); + assertThat(container.size()).isGreaterThan(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER); } } diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java index f267d9400d4..0e6430c1ae8 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java @@ -22,6 +22,7 @@ package org.sonar.server.authentication; import io.jsonwebtoken.Claims; import io.jsonwebtoken.impl.DefaultClaims; import java.util.Date; +import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; import javax.servlet.http.Cookie; @@ -38,18 +39,21 @@ import org.sonar.api.utils.System2; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.DbTester; +import org.sonar.db.user.SessionTokenDto; import org.sonar.db.user.UserDto; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import static org.sonar.db.user.UserTesting.newUserDto; @@ -63,9 +67,10 @@ public class JwtHttpHandlerTest { private static final long SIX_MINUTES_AGO = NOW - 6 * 60 * 1000L; private static final long TEN_DAYS_AGO = NOW - 10 * 24 * 60 * 60 * 1000L; + private static final long IN_FIVE_MINUTES = NOW + 5 * 60 * 1000L; + @Rule public ExpectedException expectedException = ExpectedException.none(); - @Rule public DbTester db = DbTester.create(); @@ -102,6 +107,7 @@ public class JwtHttpHandlerTest { verify(jwtSerializer).encode(jwtArgumentCaptor.capture()); verifyToken(jwtArgumentCaptor.getValue(), user, 3 * 24 * 60 * 60, NOW); + verifySessionTokenInDb(jwtArgumentCaptor.getValue()); } @Test @@ -142,7 +148,7 @@ public class JwtHttpHandlerTest { settings.setProperty("sonar.web.sessionTimeoutInMinutes", 15); underTest.generateToken(user, request, response); verify(jwtSerializer, times(2)).encode(jwtArgumentCaptor.capture()); - verifyToken(jwtArgumentCaptor.getAllValues().get(0), user,firstSessionTimeoutInMinutes * 60, NOW); + verifyToken(jwtArgumentCaptor.getAllValues().get(0), user, firstSessionTimeoutInMinutes * 60, NOW); verifyToken(jwtArgumentCaptor.getAllValues().get(1), user, firstSessionTimeoutInMinutes * 60, NOW); } @@ -180,7 +186,8 @@ public class JwtHttpHandlerTest { public void validate_token() { UserDto user = db.users().insertUser(); addJwtCookie(); - Claims claims = createToken(user.getUuid(), NOW); + SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES)); + Claims claims = createToken(sessionToken, NOW); when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); assertThat(underTest.validateToken(request, response).isPresent()).isTrue(); @@ -193,13 +200,17 @@ public class JwtHttpHandlerTest { UserDto user = db.users().insertUser(); addJwtCookie(); // Token was created 10 days ago and refreshed 6 minutes ago - Claims claims = createToken(user.getUuid(), TEN_DAYS_AGO); + SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES)); + Claims claims = createToken(sessionToken, TEN_DAYS_AGO); claims.put("lastRefreshTime", SIX_MINUTES_AGO); when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); assertThat(underTest.validateToken(request, response).isPresent()).isTrue(); - verify(jwtSerializer).refresh(any(Claims.class), eq(3 * 24 * 60 * 60)); + verify(jwtSerializer).refresh(any(Claims.class), eq(NOW + 3 * 24 * 60 * 60 * 1000L)); + assertThat(dbClient.sessionTokensDao().selectByUuid(dbSession, sessionToken.getUuid()).get().getExpirationDate()) + .isNotEqualTo(IN_FIVE_MINUTES) + .isEqualTo(NOW + 3 * 24 * 60 * 60 * 1000L); } @Test @@ -207,7 +218,8 @@ public class JwtHttpHandlerTest { UserDto user = db.users().insertUser(); addJwtCookie(); // Token was created 10 days ago and refreshed 4 minutes ago - Claims claims = createToken(user.getUuid(), TEN_DAYS_AGO); + SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES)); + Claims claims = createToken(sessionToken, TEN_DAYS_AGO); claims.put("lastRefreshTime", FOUR_MINUTES_AGO); when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); @@ -221,8 +233,8 @@ public class JwtHttpHandlerTest { UserDto user = db.users().insertUser(); addJwtCookie(); // Token was created 4 months ago, refreshed 4 minutes ago, and it expired in 5 minutes - Claims claims = createToken(user.getUuid(), NOW - (4L * 30 * 24 * 60 * 60 * 1000)); - claims.setExpiration(new Date(NOW + 5 * 60 * 1000)); + SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES)); + Claims claims = createToken(sessionToken, NOW - (4L * 30 * 24 * 60 * 60 * 1000)); claims.put("lastRefreshTime", FOUR_MINUTES_AGO); when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); @@ -233,8 +245,8 @@ public class JwtHttpHandlerTest { public void validate_token_does_not_refresh_session_when_user_is_disabled() { addJwtCookie(); UserDto user = addUser(false); - - Claims claims = createToken(user.getLogin(), NOW); + SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES)); + Claims claims = createToken(sessionToken, NOW); when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); assertThat(underTest.validateToken(request, response).isPresent()).isFalse(); @@ -253,7 +265,7 @@ public class JwtHttpHandlerTest { public void validate_token_does_nothing_when_no_jwt_cookie() { underTest.validateToken(request, response); - verifyZeroInteractions(httpSession, jwtSerializer); + verifyNoInteractions(httpSession, jwtSerializer); assertThat(underTest.validateToken(request, response).isPresent()).isFalse(); } @@ -263,7 +275,7 @@ public class JwtHttpHandlerTest { underTest.validateToken(request, response); - verifyZeroInteractions(httpSession, jwtSerializer); + verifyNoInteractions(httpSession, jwtSerializer); assertThat(underTest.validateToken(request, response).isPresent()).isFalse(); } @@ -271,7 +283,8 @@ public class JwtHttpHandlerTest { public void validate_token_verify_csrf_state() { UserDto user = db.users().insertUser(); addJwtCookie(); - Claims claims = createToken(user.getUuid(), NOW); + SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES)); + Claims claims = createToken(sessionToken, NOW); claims.put("xsrfToken", CSRF_STATE); when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); @@ -281,31 +294,95 @@ public class JwtHttpHandlerTest { } @Test - public void validate_token_refresh_state_when_refreshing_token() { + public void validate_token_does_nothing_when_no_session_token_in_db() { UserDto user = db.users().insertUser(); addJwtCookie(); + // No SessionToken in DB + Claims claims = createToken("ABCD", user.getUuid(), NOW, IN_FIVE_MINUTES); + claims.put("lastRefreshTime", SIX_MINUTES_AGO); + when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); + + underTest.validateToken(request, response); + + assertThat(underTest.validateToken(request, response).isPresent()).isFalse(); + } + @Test + public void validate_token_does_nothing_when_expiration_date_from_session_token_is_expired() { + UserDto user = db.users().insertUser(); + addJwtCookie(); + // In SessionToken, the expiration date is expired... + SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(FOUR_MINUTES_AGO)); + // ...whereas in the cookie, the expiration date is not expired + Claims claims = createToken(sessionToken.getUuid(), user.getUuid(), NOW, IN_FIVE_MINUTES); + claims.put("lastRefreshTime", SIX_MINUTES_AGO); + when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); + + underTest.validateToken(request, response); + + assertThat(underTest.validateToken(request, response).isPresent()).isFalse(); + } + + @Test + public void validate_token_refresh_state_when_refreshing_token() { + UserDto user = db.users().insertUser(); + addJwtCookie(); // Token was created 10 days ago and refreshed 6 minutes ago - Claims claims = createToken(user.getUuid(), TEN_DAYS_AGO); + SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES)); + Claims claims = createToken(sessionToken, TEN_DAYS_AGO); claims.put("xsrfToken", "CSRF_STATE"); when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); underTest.validateToken(request, response); - verify(jwtSerializer).refresh(any(Claims.class), anyInt()); + verify(jwtSerializer).refresh(any(Claims.class), anyLong()); verify(jwtCsrfVerifier).refreshState(request, response, "CSRF_STATE", 3 * 24 * 60 * 60); } @Test public void remove_token() { + addJwtCookie(); + UserDto user = db.users().insertUser(); + SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES)); + Claims claims = createToken(sessionToken, TEN_DAYS_AGO); + claims.put("lastRefreshTime", FOUR_MINUTES_AGO); + when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims)); + + underTest.removeToken(request, response); + + verifyCookie(findCookie("JWT-SESSION").get(), null, 0); + verify(jwtCsrfVerifier).removeState(request, response); + assertThat(dbClient.sessionTokensDao().selectByUuid(dbSession, sessionToken.getUuid())).isNotPresent(); + } + + @Test + public void does_not_remove_token_from_db_when_no_jwt_token_in_cookie() { + addJwtCookie(); + UserDto user = db.users().insertUser(); + SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES)); + when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.empty()); + underTest.removeToken(request, response); verifyCookie(findCookie("JWT-SESSION").get(), null, 0); verify(jwtCsrfVerifier).removeState(request, response); + assertThat(dbClient.sessionTokensDao().selectByUuid(dbSession, sessionToken.getUuid())).isPresent(); } - private void verifyToken(JwtSerializer.JwtSession token, UserDto user, int expectedExpirationTime, long expectedRefreshTime) { - assertThat(token.getExpirationTimeInSeconds()).isEqualTo(expectedExpirationTime); + @Test + public void does_not_remove_token_from_db_when_no_cookie() { + UserDto user = db.users().insertUser(); + SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES)); + + underTest.removeToken(request, response); + + verifyCookie(findCookie("JWT-SESSION").get(), null, 0); + verify(jwtCsrfVerifier).removeState(request, response); + assertThat(dbClient.sessionTokensDao().selectByUuid(dbSession, sessionToken.getUuid())).isPresent(); + } + + private void verifyToken(JwtSerializer.JwtSession token, UserDto user, long expectedExpirationDuration, long expectedRefreshTime) { + assertThat(token.getExpirationTime()).isEqualTo(NOW + expectedExpirationDuration * 1000L); assertThat(token.getUserLogin()).isEqualTo(user.getUuid()); assertThat(token.getProperties().get("lastRefreshTime")).isEqualTo(expectedRefreshTime); } @@ -325,6 +402,17 @@ public class JwtHttpHandlerTest { assertThat(cookie.getValue()).isEqualTo(value); } + private void verifySessionTokenInDb(JwtSerializer.JwtSession jwtSession) { + Map<String, Object> map = db.selectFirst(dbSession, "select st.uuid as \"uuid\", " + + "st.user_uuid as \"userUuid\", " + + "st.expiration_date as \"expirationDate\" " + + "from session_tokens st "); + assertThat(map) + .contains( + entry("uuid", jwtSession.getSessionTokenUuid()), + entry("expirationDate", jwtSession.getExpirationTime())); + } + private UserDto addUser(boolean active) { UserDto user = newUserDto() .setActive(active); @@ -339,14 +427,13 @@ public class JwtHttpHandlerTest { return cookie; } - private Claims createToken(String userUuid, long createdAt) { - // Expired in 5 minutes by default - return createToken(userUuid, createdAt, NOW + 5 * 60 * 1000); + private Claims createToken(SessionTokenDto sessionToken, long createdAt) { + return createToken(sessionToken.getUuid(), sessionToken.getUserUuid(), createdAt, sessionToken.getExpirationDate()); } - private Claims createToken(String userUuid, long createdAt, long expiredAt) { + private Claims createToken(String uuid, String userUuid, long createdAt, long expiredAt) { DefaultClaims claims = new DefaultClaims(); - claims.setId("ID"); + claims.setId(uuid); claims.setSubject(userUuid); claims.setIssuedAt(new Date(createdAt)); claims.setExpiration(new Date(expiredAt)); diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtSerializerTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtSerializerTest.java index d5451459a11..e94b9fb8ddc 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtSerializerTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtSerializerTest.java @@ -22,7 +22,6 @@ package org.sonar.server.authentication; import com.google.common.collect.ImmutableMap; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.impl.DefaultClaims; import java.util.Base64; import java.util.Date; @@ -35,11 +34,12 @@ import org.junit.rules.ExpectedException; import org.sonar.api.config.internal.MapSettings; import org.sonar.api.utils.DateUtils; import org.sonar.api.utils.System2; -import org.sonar.core.util.UuidFactory; -import org.sonar.core.util.UuidFactoryImpl; import org.sonar.server.authentication.JwtSerializer.JwtSession; import org.sonar.server.authentication.event.AuthenticationEvent.Source; +import static io.jsonwebtoken.SignatureAlgorithm.HS256; +import static org.apache.commons.lang.time.DateUtils.addMinutes; +import static org.apache.commons.lang.time.DateUtils.addYears; import static org.assertj.core.api.Assertions.assertThat; import static org.sonar.server.authentication.event.AuthenticationExceptionMatcher.authenticationException; @@ -47,21 +47,21 @@ public class JwtSerializerTest { private static final String A_SECRET_KEY = "HrPSavOYLNNrwTY+SOqpChr7OwvbR/zbDLdVXRN0+Eg="; private static final String USER_LOGIN = "john"; + private static final String SESSION_TOKEN_UUID = "ABCD"; @Rule public ExpectedException expectedException = ExpectedException.none(); private MapSettings settings = new MapSettings(); private System2 system2 = System2.INSTANCE; - private UuidFactory uuidFactory = UuidFactoryImpl.INSTANCE; - private JwtSerializer underTest = new JwtSerializer(settings.asConfig(), system2, uuidFactory); + private JwtSerializer underTest = new JwtSerializer(settings.asConfig(), system2); @Test public void generate_token() { setSecretKey(A_SECRET_KEY); underTest.start(); - String token = underTest.encode(new JwtSession(USER_LOGIN, 10)); + String token = underTest.encode(new JwtSession(USER_LOGIN, SESSION_TOKEN_UUID, addMinutes(new Date(), 20).getTime())); assertThat(token).isNotEmpty(); } @@ -71,29 +71,27 @@ public class JwtSerializerTest { setSecretKey(A_SECRET_KEY); underTest.start(); - Date now = new Date(); - long expirationTimeInSeconds = 10L; - String token = underTest.encode(new JwtSession(USER_LOGIN, expirationTimeInSeconds)); + String token = underTest.encode(new JwtSession(USER_LOGIN, SESSION_TOKEN_UUID, addMinutes(new Date(), 20).getTime())); assertThat(token).isNotEmpty(); Claims claims = underTest.decode(token).get(); - assertThat(claims.getExpiration().getTime()).isGreaterThanOrEqualTo(now.getTime() + expirationTimeInSeconds * 1000L - 1000L); + assertThat(claims.getExpiration().getTime()) + .isGreaterThanOrEqualTo(addMinutes(new Date(), 19).getTime()); } @Test public void generate_token_with_big_expiration_date() { setSecretKey(A_SECRET_KEY); underTest.start(); - Date now = new Date(); - long oneYearInSeconds = 12 * 30 * 24 * 60 * 60L; - String token = underTest.encode(new JwtSession(USER_LOGIN, oneYearInSeconds)); + long oneYearLater = addYears(new Date(), 1).getTime(); + String token = underTest.encode(new JwtSession(USER_LOGIN, SESSION_TOKEN_UUID, oneYearLater)); assertThat(token).isNotEmpty(); Claims claims = underTest.decode(token).get(); // Check expiration date it set to one year in the future - assertThat(claims.getExpiration().getTime()).isGreaterThanOrEqualTo(now.getTime() + oneYearInSeconds * 1000L - 1000L); + assertThat(claims.getExpiration().getTime()).isGreaterThanOrEqualTo(oneYearLater - 1000L); } @Test @@ -101,7 +99,7 @@ public class JwtSerializerTest { setSecretKey(A_SECRET_KEY); underTest.start(); - String token = underTest.encode(new JwtSession(USER_LOGIN, 10, ImmutableMap.of("custom", "property"))); + String token = underTest.encode(new JwtSession(USER_LOGIN, SESSION_TOKEN_UUID, addMinutes(new Date(), 20).getTime(), ImmutableMap.of("custom", "property"))); assertThat(token).isNotEmpty(); Claims claims = underTest.decode(token).get(); @@ -112,17 +110,16 @@ public class JwtSerializerTest { public void decode_token() { setSecretKey(A_SECRET_KEY); underTest.start(); - Date now = new Date(); - String token = underTest.encode(new JwtSession(USER_LOGIN, 20 * 60)); + String token = underTest.encode(new JwtSession(USER_LOGIN, SESSION_TOKEN_UUID, addMinutes(new Date(), 20).getTime())); Claims claims = underTest.decode(token).get(); - assertThat(claims.getId()).isNotEmpty(); + assertThat(claims.getId()).isEqualTo(SESSION_TOKEN_UUID); assertThat(claims.getSubject()).isEqualTo(USER_LOGIN); assertThat(claims.getExpiration()).isNotNull(); assertThat(claims.getIssuedAt()).isNotNull(); // Check expiration date it set to more than 19 minutes in the future - assertThat(claims.getExpiration()).isAfterOrEqualsTo(new Date(now.getTime() + 19 * 60 * 1000)); + assertThat(claims.getExpiration()).isAfterOrEqualTo(addMinutes(new Date(), 19)); } @Test @@ -133,8 +130,8 @@ public class JwtSerializerTest { String token = Jwts.builder() .setId("123") .setIssuedAt(new Date(system2.now())) - .setExpiration(new Date(system2.now())) - .signWith(SignatureAlgorithm.HS256, decodeSecretKey(A_SECRET_KEY)) + .setExpiration(addMinutes(new Date(), -20)) + .signWith(decodeSecretKey(A_SECRET_KEY), HS256) .compact(); assertThat(underTest.decode(token)).isEmpty(); @@ -149,8 +146,8 @@ public class JwtSerializerTest { .setId("123") .setSubject(USER_LOGIN) .setIssuedAt(new Date(system2.now())) - .setExpiration(new Date(system2.now() + 20 * 60 * 1000)) - .signWith(SignatureAlgorithm.HS256, decodeSecretKey("LyWgHktP0FuHB2K+kMs3KWMCJyFHVZDdDSqpIxAMVaQ=")) + .setExpiration(addMinutes(new Date(), 20)) + .signWith(decodeSecretKey("LyWgHktP0FuHB2K+kMs3KWMCJyFHVZDdDSqpIxAMVaQ="), HS256) .compact(); assertThat(underTest.decode(token)).isEmpty(); @@ -165,8 +162,8 @@ public class JwtSerializerTest { .setSubject(USER_LOGIN) .setIssuer("sonarqube") .setIssuedAt(new Date(system2.now())) - .setExpiration(new Date(system2.now() + 20 * 60 * 1000)) - .signWith(SignatureAlgorithm.HS256, decodeSecretKey(A_SECRET_KEY)) + .setExpiration(addMinutes(new Date(), 20)) + .signWith(decodeSecretKey(A_SECRET_KEY), HS256) .compact(); expectedException.expect(authenticationException().from(Source.jwt()).withLogin(USER_LOGIN).andNoPublicMessage()); @@ -183,8 +180,8 @@ public class JwtSerializerTest { .setId("123") .setIssuer("sonarqube") .setIssuedAt(new Date(system2.now())) - .setExpiration(new Date(system2.now() + 20 * 60 * 1000)) - .signWith(SignatureAlgorithm.HS256, decodeSecretKey(A_SECRET_KEY)) + .setExpiration(addMinutes(new Date(), 20)) + .signWith(HS256, decodeSecretKey(A_SECRET_KEY)) .compact(); expectedException.expect(authenticationException().from(Source.jwt()).withoutLogin().andNoPublicMessage()); @@ -202,7 +199,7 @@ public class JwtSerializerTest { .setIssuer("sonarqube") .setSubject(USER_LOGIN) .setIssuedAt(new Date(system2.now())) - .signWith(SignatureAlgorithm.HS256, decodeSecretKey(A_SECRET_KEY)) + .signWith(decodeSecretKey(A_SECRET_KEY), HS256) .compact(); expectedException.expect(authenticationException().from(Source.jwt()).withLogin(USER_LOGIN).andNoPublicMessage()); @@ -218,8 +215,8 @@ public class JwtSerializerTest { String token = Jwts.builder() .setId("123") .setSubject(USER_LOGIN) - .setExpiration(new Date(system2.now() + 20 * 60 * 1000)) - .signWith(SignatureAlgorithm.HS256, decodeSecretKey(A_SECRET_KEY)) + .setExpiration(addMinutes(new Date(), 20)) + .signWith(decodeSecretKey(A_SECRET_KEY), HS256) .compact(); expectedException.expect(authenticationException().from(Source.jwt()).withLogin(USER_LOGIN).andNoPublicMessage()); @@ -234,7 +231,7 @@ public class JwtSerializerTest { underTest.start(); assertThat(underTest.getSecretKey()).isNotNull(); - assertThat(underTest.getSecretKey().getAlgorithm()).isEqualTo(SignatureAlgorithm.HS256.getJcaName()); + assertThat(underTest.getSecretKey().getAlgorithm()).isEqualTo(HS256.getJcaName()); } @Test @@ -254,7 +251,7 @@ public class JwtSerializerTest { Date now = new Date(); Date createdAt = DateUtils.parseDate("2016-01-01"); // Expired in 10 minutes - Date expiredAt = new Date(now.getTime() + 10 * 60 * 1000); + Date expiredAt = addMinutes(new Date(), 10); Claims token = new DefaultClaims() .setId("id") .setSubject("subject") @@ -264,7 +261,7 @@ public class JwtSerializerTest { token.put("key", "value"); // Refresh the token with a higher expiration time - String encodedToken = underTest.refresh(token, 20 * 60); + String encodedToken = underTest.refresh(token, addMinutes(new Date(), 20).getTime()); Claims result = underTest.decode(encodedToken).get(); assertThat(result.getId()).isEqualTo("id"); @@ -274,17 +271,17 @@ public class JwtSerializerTest { assertThat(result.get("key")).isEqualTo("value"); // Expiration date has been changed assertThat(result.getExpiration()).isNotEqualTo(expiredAt) - .isAfterOrEqualsTo(new Date(now.getTime() + 19 * 1000)); + .isAfterOrEqualTo(addMinutes(new Date(), 19)); } @Test public void refresh_token_generate_a_new_hash() { setSecretKey(A_SECRET_KEY); underTest.start(); - String token = underTest.encode(new JwtSession(USER_LOGIN, 30)); + String token = underTest.encode(new JwtSession(USER_LOGIN, SESSION_TOKEN_UUID, addMinutes(new Date(), 20).getTime())); Optional<Claims> claims = underTest.decode(token); - String newToken = underTest.refresh(claims.get(), 45); + String newToken = underTest.refresh(claims.get(), addMinutes(new Date(), 45).getTime()); assertThat(newToken).isNotEqualTo(token); } @@ -294,7 +291,7 @@ public class JwtSerializerTest { expectedException.expect(NullPointerException.class); expectedException.expectMessage("org.sonar.server.authentication.JwtSerializer not started"); - underTest.encode(new JwtSession(USER_LOGIN, 10)); + underTest.encode(new JwtSession(USER_LOGIN, SESSION_TOKEN_UUID, addMinutes(new Date(), 10).getTime())); } @Test @@ -310,12 +307,12 @@ public class JwtSerializerTest { expectedException.expect(NullPointerException.class); expectedException.expectMessage("org.sonar.server.authentication.JwtSerializer not started"); - underTest.refresh(new DefaultClaims(), 10); + underTest.refresh(new DefaultClaims(), addMinutes(new Date(), 10).getTime()); } private SecretKey decodeSecretKey(String encodedKey) { byte[] decodedKey = Base64.getDecoder().decode(encodedKey); - return new SecretKeySpec(decodedKey, 0, decodedKey.length, SignatureAlgorithm.HS256.getJcaName()); + return new SecretKeySpec(decodedKey, 0, decodedKey.length, HS256.getJcaName()); } private void setSecretKey(String s) { diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/purge/SessionTokensCleanerTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/purge/SessionTokensCleanerTest.java new file mode 100644 index 00000000000..08a28f7f5df --- /dev/null +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/purge/SessionTokensCleanerTest.java @@ -0,0 +1,127 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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.purge; + +import java.util.concurrent.Callable; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.config.Configuration; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.impl.utils.TestSystem2; +import org.sonar.api.utils.log.LogAndArguments; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.db.DbTester; +import org.sonar.db.user.SessionTokenDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.util.AbstractStoppableExecutorService; +import org.sonar.server.util.GlobalLockManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class SessionTokensCleanerTest { + + private static final long NOW = 1_000_000_000L; + + private TestSystem2 system2 = new TestSystem2().setNow(NOW); + @Rule + public DbTester db = DbTester.create(system2); + @Rule + public LogTester logTester = new LogTester(); + + private GlobalLockManager lockManager = mock(GlobalLockManager.class); + + private final MapSettings settings = new MapSettings(); + private final Configuration configuration = settings.asConfig(); + + private SyncSessionTokensCleanerExecutorService executorService = new SyncSessionTokensCleanerExecutorService(); + + private SessionTokensCleaner underTest = new SessionTokensCleaner(executorService, db.getDbClient(), configuration, lockManager); + + @Test + public void purge_expired_session_tokens() { + when(lockManager.tryLock(anyString())).thenReturn(true); + UserDto user = db.users().insertUser(); + SessionTokenDto validSessionToken = db.users().insertSessionToken(user); + SessionTokenDto expiredSessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(NOW - 1_000_000L)); + underTest.start(); + + executorService.runCommand(); + + assertThat(db.getDbClient().sessionTokensDao().selectByUuid(db.getSession(), validSessionToken.getUuid())).isPresent(); + assertThat(db.getDbClient().sessionTokensDao().selectByUuid(db.getSession(), expiredSessionToken.getUuid())).isNotPresent(); + assertThat(logTester.getLogs(LoggerLevel.INFO)) + .extracting(LogAndArguments::getFormattedMsg) + .containsOnly("Purge of expired session tokens has removed 1 elements"); + } + + @Test + public void do_not_execute_purge_when_fail_to_get_lock() { + when(lockManager.tryLock(anyString())).thenReturn(false); + SessionTokenDto expiredSessionToken = db.users().insertSessionToken(db.users().insertUser(), st -> st.setExpirationDate(NOW - 1_000_000L)); + underTest.start(); + + executorService.runCommand(); + + assertThat(db.getDbClient().sessionTokensDao().selectByUuid(db.getSession(), expiredSessionToken.getUuid())).isPresent(); + } + + private static class SyncSessionTokensCleanerExecutorService extends AbstractStoppableExecutorService<ScheduledExecutorService> implements SessionTokensCleanerExecutorService { + + private Runnable command; + + public SyncSessionTokensCleanerExecutorService() { + super(null); + } + + public void runCommand() { + command.run(); + } + + @Override + public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { + this.command = command; + return null; + } + + @Override + public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) { + return null; + } + + @Override + public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) { + return null; + } + + @Override + public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { + return null; + } + + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/authentication/ws/LogoutAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/authentication/ws/LogoutAction.java index 936e1d64baf..6470e483807 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/authentication/ws/LogoutAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/authentication/ws/LogoutAction.java @@ -87,7 +87,7 @@ public class LogoutAction extends ServletFilter implements AuthenticationWsActio private void generateAuthenticationEvent(HttpServletRequest request, HttpServletResponse response) { try { Optional<JwtHttpHandler.Token> token = jwtHttpHandler.getToken(request, response); - String userLogin = token.isPresent() ? token.get().getUserDto().getLogin() : null; + String userLogin = token.map(value -> value.getUserDto().getLogin()).orElse(null); authenticationEvent.logoutSuccess(request, userLogin); } catch (AuthenticationException e) { authenticationEvent.logoutFailure(request, e.getMessage()); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DeactivateAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DeactivateAction.java index a5da21e08a4..ac5b47ecbfe 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DeactivateAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DeactivateAction.java @@ -108,19 +108,14 @@ public class DeactivateAction implements UsersWsAction { dbClient.organizationMemberDao().deleteByUserUuid(dbSession, userUuid); dbClient.userPropertiesDao().deleteByUser(dbSession, user); dbClient.almPatDao().deleteByUser(dbSession, user); - deactivateUser(dbSession, user); + dbClient.sessionTokensDao().deleteByUser(dbSession, user); + dbClient.userDao().deactivateUser(dbSession, user); userIndexer.commitAndIndex(dbSession, user); - - LOGGER.debug("Deactivate user: {}; by admin: {}", login, userSession.isSystemAdministrator()); } writeResponse(response, login); } - private void deactivateUser(DbSession dbSession, UserDto user) { - dbClient.userDao().deactivateUser(dbSession, user); - } - private void writeResponse(Response response, String login) { try (DbSession dbSession = dbClient.openSession(false)) { UserDto user = dbClient.userDao().selectByLogin(dbSession, login); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/authentication/ws/LogoutActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/authentication/ws/LogoutActionTest.java index 0cf8985de48..84861bc4607 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/authentication/ws/LogoutActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/authentication/ws/LogoutActionTest.java @@ -40,7 +40,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import static org.sonar.db.user.UserTesting.newUserDto; import static org.sonar.server.authentication.event.AuthenticationEvent.Source.sso; @@ -91,34 +91,34 @@ public class LogoutActionTest { underTest.doFilter(request, response, chain); - verifyZeroInteractions(jwtHttpHandler, chain); + verifyNoInteractions(jwtHttpHandler, chain); verify(response).setStatus(400); } @Test - public void logout_logged_user() throws Exception { + public void logout_logged_user() { setUser(USER); executeRequest(); verify(jwtHttpHandler).removeToken(request, response); - verifyZeroInteractions(chain); + verifyNoInteractions(chain); verify(authenticationEvent).logoutSuccess(request, "john"); } @Test - public void logout_unlogged_user() throws Exception { + public void logout_unlogged_user() { setNoUser(); executeRequest(); verify(jwtHttpHandler).removeToken(request, response); - verifyZeroInteractions(chain); + verifyNoInteractions(chain); verify(authenticationEvent).logoutSuccess(request, null); } @Test - public void generate_auth_event_on_failure() throws Exception { + public void generate_auth_event_on_failure() { setUser(USER); AuthenticationException exception = AuthenticationException.newBuilder().setMessage("error!").setSource(sso()).build(); doThrow(exception).when(jwtHttpHandler).getToken(any(HttpServletRequest.class), any(HttpServletResponse.class)); @@ -127,7 +127,7 @@ public class LogoutActionTest { verify(authenticationEvent).logoutFailure(request, "error!"); verify(jwtHttpHandler).removeToken(any(HttpServletRequest.class), any(HttpServletResponse.class)); - verifyZeroInteractions(chain); + verifyNoInteractions(chain); } private void executeRequest() { diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java index 010afb4a438..caaecbb3ac1 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java @@ -38,6 +38,7 @@ import org.sonar.db.property.PropertyDto; import org.sonar.db.property.PropertyQuery; import org.sonar.db.qualityprofile.QProfileDto; import org.sonar.db.user.GroupDto; +import org.sonar.db.user.SessionTokenDto; import org.sonar.db.user.UserDto; import org.sonar.server.es.EsTester; import org.sonar.server.exceptions.BadRequestException; @@ -260,6 +261,22 @@ public class DeactivateActionTest { } @Test + public void deactivate_user_deletes_his_session_tokens() { + logInAsSystemAdministrator(); + UserDto user = db.users().insertUser(); + SessionTokenDto sessionToken1 = db.users().insertSessionToken(user); + SessionTokenDto sessionToken2 =db.users().insertSessionToken(user); + UserDto anotherUser = db.users().insertUser(); + SessionTokenDto sessionToken3 =db.users().insertSessionToken(anotherUser); + + deactivate(user.getLogin()); + + assertThat(db.getDbClient().sessionTokensDao().selectByUuid(dbSession, sessionToken1.getUuid())).isNotPresent(); + assertThat(db.getDbClient().sessionTokensDao().selectByUuid(dbSession, sessionToken2.getUuid())).isNotPresent(); + assertThat(db.getDbClient().sessionTokensDao().selectByUuid(dbSession, sessionToken3.getUuid())).isPresent(); + } + + @Test public void user_cannot_deactivate_itself_on_sonarqube() { UserDto user = db.users().insertUser(); userSession.logIn(user.getLogin()).setSystemAdministrator(); |