@@ -43,7 +43,7 @@ import static org.sonar.db.DatabaseUtils.executeLargeInputsWithoutOutput; | |||
import static org.sonar.db.user.UserDto.SCM_ACCOUNTS_SEPARATOR; | |||
public class UserDao implements Dao { | |||
private final static long WEEK_IN_MS = 7L * 24L * 3_600L * 1_000L; | |||
private final System2 system2; | |||
private final UuidFactory uuidFactory; | |||
@@ -112,6 +112,10 @@ public class UserDao implements Dao { | |||
return dto; | |||
} | |||
public void updateSonarlintLastConnectionDate(DbSession session, String login) { | |||
mapper(session).updateSonarlintLastConnectionDate(login, system2.now()); | |||
} | |||
public void setRoot(DbSession session, String login, boolean root) { | |||
mapper(session).setRoot(login, root, system2.now()); | |||
} | |||
@@ -171,6 +175,11 @@ public class UserDao implements Dao { | |||
return mapper(dbSession).selectByExternalLoginAndIdentityProvider(externalLogin, externalIdentityProvider); | |||
} | |||
public long countSonarlintWeeklyUsers(DbSession dbSession) { | |||
long threshold = system2.now() - WEEK_IN_MS; | |||
return mapper(dbSession).countActiveSonarlintUsers(threshold); | |||
} | |||
public void scrollByUuids(DbSession dbSession, Collection<String> uuids, Consumer<UserDto> consumer) { | |||
UserMapper mapper = mapper(dbSession); | |||
@@ -63,6 +63,13 @@ public class UserDto { | |||
@Nullable | |||
private Long lastConnectionDate; | |||
/** | |||
* Date of the last time sonarlint connected to sonarqube WSs with this user's authentication. | |||
* Can be null when user has never been authenticated, or has not been authenticated since the creation of the column in SonarQube 8.8. | |||
*/ | |||
@Nullable | |||
private Long lastSonarlintConnectionDate; | |||
private Long createdAt; | |||
private Long updatedAt; | |||
@@ -281,6 +288,17 @@ public class UserDto { | |||
return this; | |||
} | |||
@CheckForNull | |||
public Long getLastSonarlintConnectionDate() { | |||
return lastSonarlintConnectionDate; | |||
} | |||
public UserDto setLastSonarlintConnectionDate(@Nullable Long lastSonarlintConnectionDate) { | |||
this.lastSonarlintConnectionDate = lastSonarlintConnectionDate; | |||
return this; | |||
} | |||
public Long getCreatedAt() { | |||
return createdAt; | |||
} |
@@ -66,6 +66,8 @@ public interface UserMapper { | |||
void scrollAll(ResultHandler<UserDto> handler); | |||
void updateSonarlintLastConnectionDate(@Param("login") String login, @Param("now") long now); | |||
/** | |||
* Count actives users which are root and which login is not the specified one. | |||
*/ | |||
@@ -82,4 +84,6 @@ public interface UserMapper { | |||
void clearHomepages(@Param("homepageType") String type, @Param("homepageParameter") String value, @Param("now") long now); | |||
void clearHomepage(@Param("login") String login, @Param("now") long now); | |||
long countActiveSonarlintUsers(@Param("sinceDate") long sinceDate); | |||
} |
@@ -23,6 +23,7 @@ | |||
u.homepage_type as "homepageType", | |||
u.homepage_parameter as "homepageParameter", | |||
u.last_connection_date as "lastConnectionDate", | |||
u.last_sonarlint_connection as "lastSonarlintConnectionDate", | |||
u.created_at as "createdAt", | |||
u.updated_at as "updatedAt" | |||
</sql> | |||
@@ -166,9 +167,17 @@ | |||
salt = null, | |||
crypted_password = null, | |||
last_connection_date = null, | |||
last_sonarlint_connection = null, | |||
updated_at = #{now, jdbcType=BIGINT} | |||
</sql> | |||
<update id="updateSonarlintLastConnectionDate" parameterType="map"> | |||
update users set | |||
last_sonarlint_connection = #{now, jdbcType=BIGINT} | |||
where | |||
login = #{login, jdbcType=VARCHAR} | |||
</update> | |||
<update id="deactivateUser" parameterType="map"> | |||
update users set | |||
<include refid="deactivateUserUpdatedFields"/> | |||
@@ -221,6 +230,7 @@ | |||
hash_method, | |||
is_root, | |||
onboarded, | |||
last_sonarlint_connection, | |||
reset_password, | |||
homepage_type, | |||
homepage_parameter, | |||
@@ -242,6 +252,7 @@ | |||
#{user.hashMethod,jdbcType=VARCHAR}, | |||
#{user.root,jdbcType=BOOLEAN}, | |||
#{user.onboarded,jdbcType=BOOLEAN}, | |||
#{user.lastSonarlintConnectionDate,jdbcType=BIGINT}, | |||
#{user.resetPassword,jdbcType=BOOLEAN}, | |||
#{user.homepageType,jdbcType=VARCHAR}, | |||
#{user.homepageParameter,jdbcType=VARCHAR}, | |||
@@ -269,9 +280,15 @@ | |||
homepage_type = #{user.homepageType, jdbcType=VARCHAR}, | |||
homepage_parameter = #{user.homepageParameter, jdbcType=VARCHAR}, | |||
last_connection_date = #{user.lastConnectionDate,jdbcType=BIGINT}, | |||
last_sonarlint_connection = #{user.lastSonarlintConnectionDate,jdbcType=BIGINT}, | |||
updated_at = #{user.updatedAt,jdbcType=BIGINT} | |||
where | |||
uuid = #{user.uuid, jdbcType=VARCHAR} | |||
</update> | |||
<select id="countActiveSonarlintUsers" parameterType="map" resultType="long"> | |||
select count(login) from users | |||
where last_sonarlint_connection > #{sinceDate,jdbcType=BIGINT} | |||
</select> | |||
</mapper> |
@@ -914,7 +914,8 @@ CREATE TABLE "USERS"( | |||
"LAST_CONNECTION_DATE" BIGINT, | |||
"CREATED_AT" BIGINT, | |||
"UPDATED_AT" BIGINT, | |||
"RESET_PASSWORD" BOOLEAN NOT NULL | |||
"RESET_PASSWORD" BOOLEAN NOT NULL, | |||
"LAST_SONARLINT_CONNECTION" BIGINT | |||
); | |||
ALTER TABLE "USERS" ADD CONSTRAINT "PK_USERS" PRIMARY KEY("UUID"); | |||
CREATE UNIQUE INDEX "USERS_LOGIN" ON "USERS"("LOGIN"); |
@@ -510,6 +510,25 @@ public class UserDaoTest { | |||
assertThat(untouchedUserReloaded.getHomepageParameter()).isEqualTo(untouchedUser.getHomepageParameter()); | |||
} | |||
@Test | |||
public void update_last_sonarlint_connection_date() { | |||
UserDto user = db.users().insertUser(); | |||
assertThat(user.getLastSonarlintConnectionDate()).isNull(); | |||
underTest.updateSonarlintLastConnectionDate(db.getSession(), user.getLogin()); | |||
assertThat(underTest.selectByLogin(db.getSession(), user.getLogin()).getLastSonarlintConnectionDate()).isEqualTo(NOW); | |||
} | |||
@Test | |||
public void count_sonarlint_weekly_users() { | |||
UserDto user1 = db.users().insertUser(c -> c.setLastSonarlintConnectionDate(NOW - 100_000)); | |||
UserDto user2 = db.users().insertUser(c -> c.setLastSonarlintConnectionDate(NOW)); | |||
// these don't count | |||
UserDto user3 = db.users().insertUser(c -> c.setLastSonarlintConnectionDate(NOW - 1_000_000_000)); | |||
UserDto user4 = db.users().insertUser(); | |||
assertThat(underTest.countSonarlintWeeklyUsers(db.getSession())).isEqualTo(2); | |||
} | |||
@Test | |||
public void clean_user_homepage() { | |||
@@ -628,7 +647,7 @@ public class UserDaoTest { | |||
.extracting(UserDto::getUuid).containsExactlyInAnyOrder(user1.getUuid()); | |||
assertThat(underTest.selectByExternalIdsAndIdentityProvider(session, | |||
asList(user1.getExternalId(), user2.getExternalId(), user3.getExternalId(), disableUser.getExternalId()), "github")) | |||
.extracting(UserDto::getUuid).containsExactlyInAnyOrder(user1.getUuid(), user2.getUuid(), disableUser.getUuid()); | |||
.extracting(UserDto::getUuid).containsExactlyInAnyOrder(user1.getUuid(), user2.getUuid(), disableUser.getUuid()); | |||
assertThat(underTest.selectByExternalIdsAndIdentityProvider(session, singletonList("unknown"), "github")).isEmpty(); | |||
assertThat(underTest.selectByExternalIdsAndIdentityProvider(session, singletonList(user1.getExternalId()), "unknown")).isEmpty(); | |||
} |
@@ -35,6 +35,7 @@ import org.sonar.server.platform.db.migration.version.v84.DbVersion84; | |||
import org.sonar.server.platform.db.migration.version.v85.DbVersion85; | |||
import org.sonar.server.platform.db.migration.version.v86.DbVersion86; | |||
import org.sonar.server.platform.db.migration.version.v87.DbVersion87; | |||
import org.sonar.server.platform.db.migration.version.v88.DbVersion88; | |||
public class MigrationConfigurationModule extends Module { | |||
@Override | |||
@@ -50,6 +51,7 @@ public class MigrationConfigurationModule extends Module { | |||
DbVersion85.class, | |||
DbVersion86.class, | |||
DbVersion87.class, | |||
DbVersion88.class, | |||
// migration steps | |||
MigrationStepRegistryImpl.class, |
@@ -0,0 +1,39 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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.v88; | |||
import java.sql.SQLException; | |||
import org.sonar.db.Database; | |||
import org.sonar.server.platform.db.migration.def.BigIntegerColumnDef; | |||
import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder; | |||
import org.sonar.server.platform.db.migration.step.DdlChange; | |||
public class AddLastSonarlintConnectionToUsers extends DdlChange { | |||
public AddLastSonarlintConnectionToUsers(Database db) { | |||
super(db); | |||
} | |||
@Override | |||
public void execute(Context context) throws SQLException { | |||
context.execute(new AddColumnsBuilder(getDialect(), "users") | |||
.addColumn(BigIntegerColumnDef.newBigIntegerColumnDefBuilder().setColumnName("last_sonarlint_connection").setIsNullable(true).build()) | |||
.build()); | |||
} | |||
} |
@@ -0,0 +1,33 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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.v88; | |||
import org.sonar.server.platform.db.migration.step.MigrationStepRegistry; | |||
import org.sonar.server.platform.db.migration.version.DbVersion; | |||
public class DbVersion88 implements DbVersion { | |||
@Override | |||
public void addSteps(MigrationStepRegistry registry) { | |||
registry | |||
.add(4300, "Add 'last_sonarlint_connection' to 'users", AddLastSonarlintConnectionToUsers.class) | |||
; | |||
} | |||
} |
@@ -30,7 +30,7 @@ public class DbVersion87Test { | |||
private final DbVersion underTest = new DbVersion87(); | |||
@Test | |||
public void migrationNumber_starts_at_4100() { | |||
public void migrationNumber_starts_at_4200() { | |||
verifyMinimumMigrationNumber(underTest, 4200); | |||
} | |||
@@ -0,0 +1,43 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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.v88; | |||
import java.sql.SQLException; | |||
import java.sql.Types; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.db.CoreDbTester; | |||
import org.sonar.server.platform.db.migration.step.DdlChange; | |||
public class AddLastSonarlintConnectionToUsersTest { | |||
private static final String TABLE_NAME = "users"; | |||
private static final String COLUMN_NAME = "last_sonarlint_connection"; | |||
@Rule | |||
public CoreDbTester db = CoreDbTester.createForSchema(AddLastSonarlintConnectionToUsersTest.class, "schema.sql"); | |||
private final DdlChange underTest = new AddLastSonarlintConnectionToUsers(db.database()); | |||
@Test | |||
public void add_column() throws SQLException { | |||
db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME); | |||
underTest.execute(); | |||
db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, Types.BIGINT, null, true); | |||
} | |||
} |
@@ -0,0 +1,42 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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.v88; | |||
import org.junit.Test; | |||
import org.sonar.server.platform.db.migration.version.DbVersion; | |||
import static org.sonar.server.platform.db.migration.version.DbVersionTestUtils.verifyMigrationNotEmpty; | |||
import static org.sonar.server.platform.db.migration.version.DbVersionTestUtils.verifyMinimumMigrationNumber; | |||
public class DbVersion88Test { | |||
private final DbVersion underTest = new DbVersion88(); | |||
@Test | |||
public void migrationNumber_starts_at_4300() { | |||
verifyMinimumMigrationNumber(underTest, 4300); | |||
} | |||
@Test | |||
public void verify_migration_count() { | |||
verifyMigrationNotEmpty(underTest); | |||
} | |||
} |
@@ -0,0 +1,28 @@ | |||
CREATE TABLE "USERS"( | |||
"UUID" VARCHAR(255) NOT NULL, | |||
"LOGIN" VARCHAR(255) NOT NULL, | |||
"NAME" VARCHAR(200), | |||
"EMAIL" VARCHAR(100), | |||
"CRYPTED_PASSWORD" VARCHAR(100), | |||
"SALT" VARCHAR(40), | |||
"HASH_METHOD" VARCHAR(10), | |||
"ACTIVE" BOOLEAN DEFAULT TRUE, | |||
"SCM_ACCOUNTS" VARCHAR(4000), | |||
"EXTERNAL_LOGIN" VARCHAR(255) NOT NULL, | |||
"EXTERNAL_IDENTITY_PROVIDER" VARCHAR(100) NOT NULL, | |||
"EXTERNAL_ID" VARCHAR(255) NOT NULL, | |||
"IS_ROOT" BOOLEAN NOT NULL, | |||
"USER_LOCAL" BOOLEAN, | |||
"ONBOARDED" BOOLEAN NOT NULL, | |||
"HOMEPAGE_TYPE" VARCHAR(40), | |||
"HOMEPAGE_PARAMETER" VARCHAR(40), | |||
"LAST_CONNECTION_DATE" BIGINT, | |||
"CREATED_AT" BIGINT, | |||
"UPDATED_AT" BIGINT, | |||
"RESET_PASSWORD" BOOLEAN NOT NULL | |||
); | |||
ALTER TABLE "USERS" ADD CONSTRAINT "PK_USERS" PRIMARY KEY("UUID"); | |||
CREATE UNIQUE INDEX "USERS_LOGIN" ON "USERS"("LOGIN"); | |||
CREATE INDEX "USERS_UPDATED_AT" ON "USERS"("UPDATED_AT"); | |||
CREATE UNIQUE INDEX "UNIQ_EXTERNAL_ID" ON "USERS"("EXTERNAL_IDENTITY_PROVIDER", "EXTERNAL_ID"); | |||
CREATE UNIQUE INDEX "UNIQ_EXTERNAL_LOGIN" ON "USERS"("EXTERNAL_IDENTITY_PROVIDER", "EXTERNAL_LOGIN"); |
@@ -52,6 +52,7 @@ public class TelemetryData { | |||
private final Boolean hasUnanalyzedC; | |||
private final Boolean hasUnanalyzedCpp; | |||
private final List<String> customSecurityConfigs; | |||
private final long sonarlintWeeklyUsers; | |||
private TelemetryData(Builder builder) { | |||
serverId = builder.serverId; | |||
@@ -62,6 +63,7 @@ public class TelemetryData { | |||
projectCount = builder.projectMeasuresStatistics.getProjectCount(); | |||
usingBranches = builder.usingBranches; | |||
database = builder.database; | |||
sonarlintWeeklyUsers = builder.sonarlintWeeklyUsers; | |||
projectCountByLanguage = builder.projectMeasuresStatistics.getProjectCountByLanguage(); | |||
almIntegrationCountByAlm = builder.almIntegrationCountByAlm; | |||
nclocByLanguage = builder.projectMeasuresStatistics.getNclocByLanguage(); | |||
@@ -93,6 +95,10 @@ public class TelemetryData { | |||
return ncloc; | |||
} | |||
public long sonarlintWeeklyUsers() { | |||
return sonarlintWeeklyUsers; | |||
} | |||
public long getUserCount() { | |||
return userCount; | |||
} | |||
@@ -169,6 +175,7 @@ public class TelemetryData { | |||
private String serverId; | |||
private String version; | |||
private long userCount; | |||
private long sonarlintWeeklyUsers; | |||
private Map<String, String> plugins; | |||
private Database database; | |||
private ProjectMeasuresStatistics projectMeasuresStatistics; | |||
@@ -201,6 +208,11 @@ public class TelemetryData { | |||
return this; | |||
} | |||
Builder setSonarlintWeeklyUsers(long sonarlintWeeklyUsers) { | |||
this.sonarlintWeeklyUsers = sonarlintWeeklyUsers; | |||
return this; | |||
} | |||
Builder setServerId(String serverId) { | |||
this.serverId = serverId; | |||
return this; |
@@ -105,6 +105,7 @@ public class TelemetryDataJsonWriter { | |||
}); | |||
json.endArray(); | |||
json.prop("sonarlintWeeklyUsers", statistics.sonarlintWeeklyUsers()); | |||
if (statistics.getInstallationDate() != null) { | |||
json.prop("installationDate", statistics.getInstallationDate()); | |||
} |
@@ -59,6 +59,7 @@ public class TelemetryDataJsonWriterTest { | |||
.setNcloc(42L) | |||
.setExternalAuthenticationProviders(asList("github", "gitlab")) | |||
.setProjectCountByScm(Collections.emptyMap()) | |||
.setSonarlintWeeklyUsers(10) | |||
.setDatabase(new TelemetryData.Database("H2", "11")) | |||
.setUsingBranches(true); | |||
@@ -96,6 +97,15 @@ public class TelemetryDataJsonWriterTest { | |||
assertJson(json).isSimilarTo("{ \"externalAuthProviders\": [ \"github\", \"gitlab\" ] }"); | |||
} | |||
@Test | |||
public void write_sonarlint_weekly_users() { | |||
TelemetryData data = SOME_TELEMETRY_DATA.build(); | |||
String json = writeTelemetryData(data); | |||
assertJson(json).isSimilarTo("{ \"sonarlintWeeklyUsers\": 10 }"); | |||
} | |||
@Test | |||
@UseDataProvider("allEditions") | |||
public void writes_edition_if_non_null(EditionProvider.Edition edition) { |
@@ -75,6 +75,12 @@ public abstract class AbstractUserSession implements UserSession { | |||
} | |||
} | |||
@Override | |||
@CheckForNull | |||
public Long getLastSonarlintConnectionDate() { | |||
return null; | |||
} | |||
@Override | |||
public final boolean hasPermission(GlobalPermission permission) { | |||
return isRoot() || hasPermissionImpl(permission); |
@@ -55,7 +55,7 @@ public class ServerUserSession extends AbstractUserSession { | |||
private Set<GlobalPermission> permissions; | |||
private Map<String, Set<String>> permissionsByProjectUuid; | |||
ServerUserSession(DbClient dbClient, @Nullable UserDto userDto) { | |||
public ServerUserSession(DbClient dbClient, @Nullable UserDto userDto) { | |||
this.dbClient = dbClient; | |||
this.userDto = userDto; | |||
} | |||
@@ -69,6 +69,12 @@ public class ServerUserSession extends AbstractUserSession { | |||
} | |||
} | |||
@Override | |||
@CheckForNull | |||
public Long getLastSonarlintConnectionDate() { | |||
return userDto == null ? null : userDto.getLastSonarlintConnectionDate(); | |||
} | |||
@Override | |||
@CheckForNull | |||
public String getLogin() { |
@@ -56,6 +56,12 @@ public class ThreadLocalUserSession implements UserSession { | |||
return DELEGATE.get() != null; | |||
} | |||
@Override | |||
@CheckForNull | |||
public Long getLastSonarlintConnectionDate() { | |||
return get().getLastSonarlintConnectionDate(); | |||
} | |||
@Override | |||
@CheckForNull | |||
public String getLogin() { |
@@ -56,6 +56,9 @@ public interface UserSession { | |||
@CheckForNull | |||
String getName(); | |||
@CheckForNull | |||
Long getLastSonarlintConnectionDate(); | |||
/** | |||
* The groups that the logged-in user is member of. An empty | |||
* collection is returned if {@link #isLoggedIn()} is {@code false}. |
@@ -99,6 +99,12 @@ public class ServerUserSessionTest { | |||
assertThat(newUserSession(user).getGroups()).extracting(GroupDto::getUuid).containsOnly(group1.getUuid(), group2.getUuid()); | |||
} | |||
@Test | |||
public void getLastSonarlintConnectionDate() { | |||
UserDto user = db.users().insertUser(p -> p.setLastSonarlintConnectionDate(1000L)); | |||
assertThat(newUserSession(user).getLastSonarlintConnectionDate()).isEqualTo(1000L); | |||
} | |||
@Test | |||
public void getGroups_keeps_groups_in_cache() { | |||
UserDto user = db.users().insertUser(); |
@@ -53,11 +53,13 @@ public class ThreadLocalUserSessionTest { | |||
MockUserSession expected = new MockUserSession("karadoc") | |||
.setUuid("karadoc-uuid") | |||
.setResetPassword(true) | |||
.setLastSonarlintConnectionDate(1000L) | |||
.setGroups(group); | |||
threadLocalUserSession.set(expected); | |||
UserSession session = threadLocalUserSession.get(); | |||
assertThat(session).isSameAs(expected); | |||
assertThat(threadLocalUserSession.getLastSonarlintConnectionDate()).isEqualTo(1000L); | |||
assertThat(threadLocalUserSession.getLogin()).isEqualTo("karadoc"); | |||
assertThat(threadLocalUserSession.getUuid()).isEqualTo("karadoc-uuid"); | |||
assertThat(threadLocalUserSession.isLoggedIn()).isTrue(); |
@@ -23,6 +23,8 @@ import java.util.ArrayList; | |||
import java.util.Collection; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import javax.annotation.CheckForNull; | |||
import org.jetbrains.annotations.Nullable; | |||
import org.sonar.db.user.GroupDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.user.AbstractUserSession; | |||
@@ -41,6 +43,7 @@ public class MockUserSession extends AbstractMockUserSession<MockUserSession> { | |||
private List<GroupDto> groups = new ArrayList<>(); | |||
private UserSession.IdentityProvider identityProvider; | |||
private UserSession.ExternalIdentity externalIdentity; | |||
private Long lastSonarlintConnectionDate; | |||
public MockUserSession(String login) { | |||
super(MockUserSession.class); | |||
@@ -62,6 +65,17 @@ public class MockUserSession extends AbstractMockUserSession<MockUserSession> { | |||
this.externalIdentity = identity.getExternalIdentity(); | |||
} | |||
@CheckForNull | |||
@Override | |||
public Long getLastSonarlintConnectionDate() { | |||
return lastSonarlintConnectionDate; | |||
} | |||
public MockUserSession setLastSonarlintConnectionDate(@Nullable Long lastSonarlintConnectionDate) { | |||
this.lastSonarlintConnectionDate = lastSonarlintConnectionDate; | |||
return this; | |||
} | |||
@Override | |||
public boolean isLoggedIn() { | |||
return true; |
@@ -278,6 +278,12 @@ public class UserSessionRule implements TestRule, UserSession { | |||
return currentUserSession.getName(); | |||
} | |||
@Override | |||
@CheckForNull | |||
public Long getLastSonarlintConnectionDate() { | |||
return currentUserSession.getLastSonarlintConnectionDate(); | |||
} | |||
@Override | |||
public Collection<GroupDto> getGroups() { | |||
return currentUserSession.getGroups(); |
@@ -144,6 +144,7 @@ public class TelemetryDataLoaderImpl implements TelemetryDataLoader { | |||
.stream() | |||
.collect(Collectors.toMap(ProjectCountPerAnalysisPropertyValue::getPropertyValue, ProjectCountPerAnalysisPropertyValue::getCount)); | |||
data.setProjectCountByScm(projectCountPerScmDetected); | |||
data.setSonarlintWeeklyUsers(dbClient.userDao().countSonarlintWeeklyUsers(dbSession)); | |||
} | |||
setSecurityCustomConfigIfPresent(data); | |||
@@ -153,7 +154,6 @@ public class TelemetryDataLoaderImpl implements TelemetryDataLoader { | |||
Optional<String> installationVersionProperty = internalProperties.read(InternalProperties.INSTALLATION_VERSION); | |||
data.setInstallationVersion(installationVersionProperty.orElse(null)); | |||
data.setInDocker(dockerSupport.isRunningInDocker()); | |||
return data.build(); | |||
} | |||
@@ -73,8 +73,9 @@ public class ClusterSystemInfoWriterTest { | |||
+ "\"Application Nodes\":[{\"Name\":\"appNodes\",\"\":{\"name\":\"appNodes\"}}]," | |||
+ "\"Search Nodes\":[{\"Name\":\"searchNodes\",\"\":{\"name\":\"searchNodes\"}}]," | |||
+ "\"Statistics\":{\"id\":\"\",\"version\":\"\",\"database\":{\"name\":\"\",\"version\":\"\"},\"plugins\":[]," | |||
+ "\"userCount\":0,\"projectCount\":0,\"usingBranches\":false,\"ncloc\":0,\"projectCountByLanguage\":[]," + | |||
"\"nclocByLanguage\":[],\"almIntegrationCount\":[],\"externalAuthProviders\":[],\"projectCountByScm\":[],\"installationDate\":0,\"installationVersion\":\"\",\"docker\":false}}"); | |||
+ "\"userCount\":0,\"projectCount\":0,\"usingBranches\":false,\"ncloc\":0,\"projectCountByLanguage\":[]," | |||
+ "\"nclocByLanguage\":[],\"almIntegrationCount\":[],\"externalAuthProviders\":[],\"projectCountByScm\":[],\"sonarlintWeeklyUsers\":0,\"installationDate\":0," | |||
+ "\"installationVersion\":\"\",\"docker\":false}}"); | |||
} | |||
private static NodeInfo createNodeInfo(String name) { |
@@ -80,7 +80,8 @@ public class StandaloneSystemInfoWriterTest { | |||
// response does not contain empty "Section Three" | |||
assertThat(writer).hasToString("{\"Health\":\"GREEN\",\"Health Causes\":[],\"Section One\":{\"foo\":\"bar\"},\"Section Two\":{\"one\":1,\"two\":2}," + | |||
"\"Statistics\":{\"id\":\"\",\"version\":\"\",\"database\":{\"name\":\"\",\"version\":\"\"},\"plugins\":[],\"userCount\":0,\"projectCount\":0,\"usingBranches\":false," + | |||
"\"ncloc\":0,\"projectCountByLanguage\":[],\"nclocByLanguage\":[],\"almIntegrationCount\":[],\"externalAuthProviders\":[],\"projectCountByScm\":[],\"installationDate\":0,\"installationVersion\":\"\",\"docker\":false}}"); | |||
"\"ncloc\":0,\"projectCountByLanguage\":[],\"nclocByLanguage\":[],\"almIntegrationCount\":[],\"externalAuthProviders\":[],\"projectCountByScm\":[]," | |||
+ "\"sonarlintWeeklyUsers\":0,\"installationDate\":0,\"installationVersion\":\"\",\"docker\":false}}"); | |||
} | |||
private void logInAsSystemAdministrator() { |
@@ -66,15 +66,17 @@ import static org.sonar.server.metric.UnanalyzedLanguageMetrics.UNANALYZED_CPP_K | |||
import static org.sonar.server.metric.UnanalyzedLanguageMetrics.UNANALYZED_C_KEY; | |||
public class TelemetryDataLoaderImplTest { | |||
private final static Long NOW = 100_000_000L; | |||
private final TestSystem2 system2 = new TestSystem2().setNow(NOW); | |||
@Rule | |||
public DbTester db = DbTester.create(); | |||
public DbTester db = DbTester.create(system2); | |||
@Rule | |||
public EsTester es = EsTester.create(); | |||
private final FakeServer server = new FakeServer(); | |||
private final PluginRepository pluginRepository = mock(PluginRepository.class); | |||
private final Configuration configuration = mock(Configuration.class); | |||
private final TestSystem2 system2 = new TestSystem2().setNow(System.currentTimeMillis()); | |||
private final PlatformEditionProvider editionProvider = mock(PlatformEditionProvider.class); | |||
private final DockerSupport dockerSupport = mock(DockerSupport.class); | |||
private final InternalProperties internalProperties = spy(new MapInternalProperties()); | |||
@@ -201,6 +203,18 @@ public class TelemetryDataLoaderImplTest { | |||
assertThat(data.getLicenseType()).isEmpty(); | |||
} | |||
@Test | |||
public void data_contains_weekly_count_sonarlint_users() { | |||
db.users().insertUser(c -> c.setLastSonarlintConnectionDate(NOW - 100_000L)); | |||
db.users().insertUser(c -> c.setLastSonarlintConnectionDate(NOW)); | |||
// these don't count | |||
db.users().insertUser(c -> c.setLastSonarlintConnectionDate(NOW - 1_000_000_000L)); | |||
db.users().insertUser(); | |||
TelemetryData data = communityUnderTest.load(); | |||
assertThat(data.sonarlintWeeklyUsers()).isEqualTo(2L); | |||
} | |||
@Test | |||
public void data_has_license_type_on_commercial_edition_if_no_license() { | |||
String licenseType = randomAlphabetic(12); |
@@ -133,6 +133,7 @@ import org.sonar.server.platform.ClusterVerification; | |||
import org.sonar.server.platform.PersistentSettings; | |||
import org.sonar.server.platform.SystemInfoWriterModule; | |||
import org.sonar.server.platform.WebCoreExtensionsInstaller; | |||
import org.sonar.server.platform.web.SonarLintConnectionFilter; | |||
import org.sonar.server.platform.web.WebServiceFilter; | |||
import org.sonar.server.platform.web.WebServiceReroutingFilter; | |||
import org.sonar.server.platform.web.requestid.HttpRequestIdModule; | |||
@@ -338,6 +339,7 @@ public class PlatformLevel4 extends PlatformLevel { | |||
// web services | |||
WebServiceEngine.class, | |||
WebServicesWsModule.class, | |||
SonarLintConnectionFilter.class, | |||
WebServiceFilter.class, | |||
WebServiceReroutingFilter.class, | |||
@@ -0,0 +1,94 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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.web; | |||
import java.io.IOException; | |||
import java.util.Locale; | |||
import java.util.Optional; | |||
import javax.servlet.FilterChain; | |||
import javax.servlet.FilterConfig; | |||
import javax.servlet.ServletException; | |||
import javax.servlet.http.HttpServletRequest; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.api.web.ServletFilter; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonar.server.ws.ServletRequest; | |||
public class SonarLintConnectionFilter extends ServletFilter { | |||
private static final UrlPattern URL_PATTERN = UrlPattern.builder() | |||
.includes("/api/*") | |||
.build(); | |||
private final DbClient dbClient; | |||
private final UserSession userSession; | |||
private final System2 system2; | |||
public SonarLintConnectionFilter(DbClient dbClient, UserSession userSession, System2 system2) { | |||
this.dbClient = dbClient; | |||
this.userSession = userSession; | |||
this.system2 = system2; | |||
} | |||
@Override | |||
public UrlPattern doGetPattern() { | |||
return URL_PATTERN; | |||
} | |||
@Override | |||
public void doFilter(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { | |||
HttpServletRequest request = (HttpServletRequest) servletRequest; | |||
ServletRequest wsRequest = new ServletRequest(request); | |||
Optional<String> agent = wsRequest.header("User-Agent"); | |||
if (agent.isPresent() && agent.get().toLowerCase(Locale.ENGLISH).contains("sonarlint")) { | |||
update(); | |||
} | |||
chain.doFilter(servletRequest, servletResponse); | |||
} | |||
@Override | |||
public void init(FilterConfig filterConfig) { | |||
// Nothing to do | |||
} | |||
@Override | |||
public void destroy() { | |||
// Nothing to do | |||
} | |||
public void update() { | |||
if (shouldUpdate()) { | |||
try (DbSession session = dbClient.openSession(false)) { | |||
dbClient.userDao().updateSonarlintLastConnectionDate(session, userSession.getLogin()); | |||
session.commit(); | |||
} | |||
} | |||
} | |||
private boolean shouldUpdate() { | |||
if (!userSession.isLoggedIn()) { | |||
return false; | |||
} | |||
long now = system2.now(); | |||
Long lastUpdate = userSession.getLastSonarlintConnectionDate(); | |||
return (lastUpdate == null || lastUpdate < now - 3_600_000L); | |||
} | |||
} |
@@ -26,7 +26,6 @@ import javax.servlet.FilterChain; | |||
import javax.servlet.FilterConfig; | |||
import javax.servlet.http.HttpServletRequest; | |||
import javax.servlet.http.HttpServletResponse; | |||
import org.sonar.api.SonarRuntime; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.api.web.ServletFilter; | |||
import org.sonar.core.util.stream.MoreCollectors; |
@@ -0,0 +1,122 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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.web; | |||
import java.io.IOException; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import javax.servlet.FilterChain; | |||
import javax.servlet.ServletException; | |||
import javax.servlet.ServletResponse; | |||
import javax.servlet.http.HttpServletRequest; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.api.impl.utils.TestSystem2; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.user.ServerUserSession; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.ArgumentMatchers.any; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.verify; | |||
import static org.mockito.Mockito.when; | |||
public class SonarLintConnectionFilterTest { | |||
private static final String LOGIN = "user1"; | |||
private final TestSystem2 system2 = new TestSystem2(); | |||
@Rule | |||
public DbTester dbTester = DbTester.create(system2); | |||
@Test | |||
public void update() throws IOException, ServletException { | |||
system2.setNow(10_000_000L); | |||
addUser(LOGIN, 1_000_000L); | |||
runFilter(LOGIN, "SonarLint for IntelliJ"); | |||
assertThat(getLastUpdate(LOGIN)).isEqualTo(10_000_000L); | |||
} | |||
@Test | |||
public void update_first_time() throws IOException, ServletException { | |||
system2.setNow(10_000_000L); | |||
addUser(LOGIN, null); | |||
runFilter(LOGIN, "SonarLint for IntelliJ"); | |||
assertThat(getLastUpdate(LOGIN)).isEqualTo(10_000_000L); | |||
} | |||
@Test | |||
public void only_applies_to_api() { | |||
SonarLintConnectionFilter underTest = new SonarLintConnectionFilter(dbTester.getDbClient(), mock(ServerUserSession.class), system2); | |||
assertThat(underTest.doGetPattern().matches("/api/test")).isTrue(); | |||
assertThat(underTest.doGetPattern().matches("/test")).isFalse(); | |||
} | |||
@Test | |||
public void do_nothing_if_no_sonarlint_agent() throws IOException, ServletException { | |||
system2.setNow(10_000L); | |||
addUser(LOGIN, 1_000L); | |||
runFilter(LOGIN, "unknown"); | |||
runFilter(LOGIN, null); | |||
assertThat(getLastUpdate(LOGIN)).isEqualTo(1_000L); | |||
} | |||
@Test | |||
public void do_nothing_if_not_logged_in() throws IOException, ServletException { | |||
system2.setNow(10_000_000L); | |||
addUser("invalid", 1_000_000L); | |||
runFilter(LOGIN, "SonarLint for IntelliJ"); | |||
assertThat(getLastUpdate("invalid")).isEqualTo(1_000_000L); | |||
} | |||
@Test | |||
public void only_update_if_not_updated_within_1h() throws IOException, ServletException { | |||
system2.setNow(2_000_000L); | |||
addUser(LOGIN, 1_000_000L); | |||
runFilter(LOGIN, "SonarLint for IntelliJ"); | |||
assertThat(getLastUpdate(LOGIN)).isEqualTo(1_000_000L); | |||
} | |||
private void addUser(String login, @Nullable Long lastUpdate) { | |||
dbTester.users().insertUser(u -> u.setLogin(login).setLastSonarlintConnectionDate(lastUpdate)); | |||
} | |||
@CheckForNull | |||
private Long getLastUpdate(String login) { | |||
return dbTester.getDbClient().userDao().selectByLogin(dbTester.getSession(), login).getLastSonarlintConnectionDate(); | |||
} | |||
private void runFilter(String loggedInUser, @Nullable String agent) throws IOException, ServletException { | |||
UserDto user = dbTester.getDbClient().userDao().selectByLogin(dbTester.getSession(), loggedInUser); | |||
ServerUserSession session = new ServerUserSession(dbTester.getDbClient(), user); | |||
SonarLintConnectionFilter underTest = new SonarLintConnectionFilter(dbTester.getDbClient(), session, system2); | |||
HttpServletRequest request = mock(HttpServletRequest.class); | |||
when(request.getHeader("User-Agent")).thenReturn(agent); | |||
FilterChain chain = mock(FilterChain.class); | |||
underTest.doFilter(request, mock(ServletResponse.class), chain); | |||
verify(chain).doFilter(any(), any()); | |||
} | |||
} |