From 71a95f62be1ac96228cf9de09d14b833d18bc037 Mon Sep 17 00:00:00 2001 From: Steve Marion Date: Tue, 11 Apr 2023 15:12:21 +0200 Subject: [PATCH] [SONAR-18964] add sonarLintLastConnectionDate parameter to api/users/search response. Add lastConnectedAfter, lastConnectedBefore, slLastConnectedAfter, and slLastConnectedBefore request parameters to api/users/search to allow filter by SonarLint and SonarQube last connection time. --- .../java/org/sonar/db/user/UserQuery.java | 77 +++++++++- .../org/sonar/db/user/UserMapper.xml | 12 ++ .../java/org/sonar/db/user/UserDbTester.java | 6 + .../sonar/server/user/ws/SearchActionIT.java | 56 +++++++- .../sonar/server/user/ws/SearchAction.java | 132 ++++++++++++++++-- .../sonar/server/user/ws/search-example.json | 4 +- sonar-ws/src/main/protobuf/ws-users.proto | 1 + 7 files changed, 272 insertions(+), 16 deletions(-) diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserQuery.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserQuery.java index 43d326ec43c..54f8e0945cc 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserQuery.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserQuery.java @@ -19,6 +19,8 @@ */ package org.sonar.db.user; +import java.time.OffsetDateTime; +import java.time.temporal.ChronoUnit; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.apache.commons.lang.StringUtils; @@ -27,11 +29,37 @@ public class UserQuery { private final String searchText; private final Boolean isActive; private final String isManagedSqlClause; + private final Long lastConnectionDateFrom; + private final Long lastConnectionDateTo; + private final Long sonarLintLastConnectionDateFrom; + private final Long sonarLintLastConnectionDateTo; - public UserQuery(@Nullable String searchText, @Nullable Boolean isActive, @Nullable String isManagedSqlClause) { + public UserQuery(@Nullable String searchText, @Nullable Boolean isActive, @Nullable String isManagedSqlClause, + @Nullable OffsetDateTime lastConnectionDateFrom, @Nullable OffsetDateTime lastConnectionDateTo, + @Nullable OffsetDateTime sonarLintLastConnectionDateFrom, @Nullable OffsetDateTime sonarLintLastConnectionDateTo) { this.searchText = searchTextToSearchTextSql(searchText); this.isActive = isActive; this.isManagedSqlClause = isManagedSqlClause; + this.lastConnectionDateFrom = parseDateToLong(lastConnectionDateFrom); + this.lastConnectionDateTo = formatDateToInput(lastConnectionDateTo); + this.sonarLintLastConnectionDateFrom = parseDateToLong(sonarLintLastConnectionDateFrom); + this.sonarLintLastConnectionDateTo = formatDateToInput(sonarLintLastConnectionDateTo); + } + + private static Long formatDateToInput(@Nullable OffsetDateTime dateTo) { + if(dateTo == null) { + return null; + } else { + // add 1 second to include all timestamp at the second precision. + return dateTo.toInstant().plus(1, ChronoUnit.SECONDS).toEpochMilli(); + } + } + private static Long parseDateToLong(@Nullable OffsetDateTime date) { + if(date == null) { + return null; + } else { + return date.toInstant().toEpochMilli(); + } } private static String searchTextToSearchTextSql(@Nullable String text) { @@ -59,6 +87,24 @@ public class UserQuery { return isManagedSqlClause; } + @CheckForNull + public Long getLastConnectionDateFrom() { + return lastConnectionDateFrom; + } + + @CheckForNull + public Long getLastConnectionDateTo() { + return lastConnectionDateTo; + } + @CheckForNull + public Long getSonarLintLastConnectionDateFrom() { + return sonarLintLastConnectionDateFrom; + } + @CheckForNull + public Long getSonarLintLastConnectionDateTo() { + return sonarLintLastConnectionDateTo; + } + public static UserQueryBuilder builder() { return new UserQueryBuilder(); } @@ -67,6 +113,11 @@ public class UserQuery { private String searchText = null; private Boolean isActive = null; private String isManagedSqlClause = null; + private OffsetDateTime lastConnectionDateFrom = null; + private OffsetDateTime lastConnectionDateTo = null; + private OffsetDateTime sonarLintLastConnectionDateFrom = null; + private OffsetDateTime sonarLintLastConnectionDateTo = null; + private UserQueryBuilder() { } @@ -86,8 +137,30 @@ public class UserQuery { return this; } + public UserQueryBuilder lastConnectionDateFrom(@Nullable OffsetDateTime lastConnectionDateFrom) { + this.lastConnectionDateFrom = lastConnectionDateFrom; + return this; + } + + public UserQueryBuilder lastConnectionDateTo(@Nullable OffsetDateTime lastConnectionDateTo) { + this.lastConnectionDateTo = lastConnectionDateTo; + return this; + } + + public UserQueryBuilder sonarLintLastConnectionDateFrom(@Nullable OffsetDateTime sonarLintLastConnectionDateFrom) { + this.sonarLintLastConnectionDateFrom = sonarLintLastConnectionDateFrom; + return this; + } + + public UserQueryBuilder sonarLintLastConnectionDateTo(@Nullable OffsetDateTime sonarLintLastConnectionDateTo) { + this.sonarLintLastConnectionDateTo = sonarLintLastConnectionDateTo; + return this; + } + public UserQuery build() { - return new UserQuery(searchText, isActive, isManagedSqlClause); + return new UserQuery( + searchText, isActive, isManagedSqlClause, lastConnectionDateFrom, lastConnectionDateTo, + sonarLintLastConnectionDateFrom, sonarLintLastConnectionDateTo); } } } diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml index 4042833f532..791f63c990c 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml @@ -184,6 +184,18 @@ AND ${query.isManagedSqlClause} + + AND u.last_connection_date >= #{query.lastConnectionDateFrom, jdbcType=BIGINT} + + + AND u.last_connection_date < #{query.lastConnectionDateTo, jdbcType=BIGINT} + + + AND u.last_sonarlint_connection >= #{query.sonarLintLastConnectionDateFrom, jdbcType=BIGINT} + + + AND u.last_sonarlint_connection < #{query.sonarLintLastConnectionDateTo, jdbcType=BIGINT} + 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 846b20a7c9c..c6f99bd5e7f 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 @@ -115,6 +115,12 @@ public class UserDbTester { return user; } + public UserDto updateSonarLintLastConnectionDate(UserDto user, long sonarLintLastConnectionDate) { + db.getDbClient().userDao().update(db.getSession(), user.setLastSonarlintConnectionDate(sonarLintLastConnectionDate)); + db.getSession().commit(); + return user; + } + public Optional selectUserByLogin(String login) { return Optional.ofNullable(dbClient.userDao().selectByLogin(db.getSession(), login)); } diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/SearchActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/SearchActionIT.java index 6d3bb32da1b..7ce5deac990 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/SearchActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/SearchActionIT.java @@ -19,12 +19,15 @@ */ package org.sonar.server.user.ws; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Set; import java.util.stream.IntStream; import org.junit.Rule; import org.junit.Test; import org.sonar.api.server.ws.WebService; import org.sonar.api.server.ws.WebService.Param; +import org.sonar.api.utils.DateUtils; import org.sonar.core.util.UuidFactory; import org.sonar.db.DbTester; import org.sonar.db.scim.ScimUserDao; @@ -452,11 +455,15 @@ public class SearchActionIT { .setScmAccounts(emptyList()) .setExternalLogin("fmallet") .setExternalIdentityProvider("sonarqube")); + long lastConnection = DateUtils.parseOffsetDateTime("2019-03-27T09:51:50+0100").toInstant().toEpochMilli(); + fmallet = db.users().updateLastConnectionDate(fmallet, lastConnection); + fmallet = db.users().updateSonarLintLastConnectionDate(fmallet, lastConnection); UserDto simon = db.users().insertUser(u -> u.setLogin("sbrandhof").setName("Simon").setEmail("s.brandhof@company.tld") .setLocal(false) .setExternalLogin("sbrandhof@ldap.com") .setExternalIdentityProvider("sonarqube") .setScmAccounts(asList("simon.brandhof", "s.brandhof@company.tld"))); + mockUsersAsManaged(simon.getUuid()); GroupDto sonarUsers = db.users().insertGroup("sonar-users"); @@ -481,7 +488,54 @@ public class SearchActionIT { assertThat(action).isNotNull(); assertThat(action.isPost()).isFalse(); assertThat(action.responseExampleAsString()).isNotEmpty(); - assertThat(action.params()).hasSize(5); + assertThat(action.params()).hasSize(9); + } + + @Test + public void search_whenFilteringConnectionDate_shouldApplyFilter() { + userSession.logIn().setSystemAdministrator(); + final Instant lastConnection = Instant.now(); + UserDto user = db.users().insertUser(u -> u + .setLogin("user-%_%-login") + .setName("user-name") + .setEmail("user@mail.com") + .setLocal(true) + .setScmAccounts(singletonList("user1"))); + user = db.users().updateLastConnectionDate(user, lastConnection.toEpochMilli()); + user = db.users().updateSonarLintLastConnectionDate(user, lastConnection.toEpochMilli()); + + assertThat(ws.newRequest() + .setParam("q", "user-%_%-") + .executeProtobuf(SearchWsResponse.class).getUsersList()) + .extracting(User::getLogin) + .containsExactlyInAnyOrder(user.getLogin()); + + assertUserWithFilter("lastConnectedAfter", lastConnection.minus(1, ChronoUnit.DAYS), user.getLogin(), true); + assertUserWithFilter("lastConnectedAfter", lastConnection.plus(1, ChronoUnit.DAYS), user.getLogin(), false); + assertUserWithFilter("lastConnectedBefore", lastConnection.minus(1, ChronoUnit.DAYS), user.getLogin(), false); + assertUserWithFilter("lastConnectedBefore", lastConnection.plus(1, ChronoUnit.DAYS), user.getLogin(), true); + + assertUserWithFilter("slLastConnectedAfter", lastConnection.minus(1, ChronoUnit.DAYS), user.getLogin(), true); + assertUserWithFilter("slLastConnectedAfter", lastConnection.plus(1, ChronoUnit.DAYS), user.getLogin(), false); + assertUserWithFilter("slLastConnectedBefore", lastConnection.minus(1, ChronoUnit.DAYS), user.getLogin(), false); + assertUserWithFilter("slLastConnectedBefore", lastConnection.plus(1, ChronoUnit.DAYS), user.getLogin(), true); + + assertUserWithFilter("slLastConnectedAfter", lastConnection, user.getLogin(), true); + assertUserWithFilter("slLastConnectedBefore", lastConnection, user.getLogin(), true); + } + + private void assertUserWithFilter(String field, Instant filterValue, String userLogin, boolean isExpectedToBeThere) { + var assertion = assertThat(ws.newRequest() + .setParam("q", "user-%_%-") + .setParam(field, DateUtils.formatDateTime(filterValue.toEpochMilli())) + .executeProtobuf(SearchWsResponse.class).getUsersList()); + if (isExpectedToBeThere) { + assertion + .extracting(User::getLogin) + .containsExactlyInAnyOrder(userLogin); + } else { + assertion.isEmpty(); + } } private void mockUsersAsManaged(String... userUuids) { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/SearchAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/SearchAction.java index 51a9561443e..358efd5315e 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/SearchAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/SearchAction.java @@ -20,6 +20,7 @@ package org.sonar.server.user.ws; import com.google.common.collect.Multimap; +import java.time.OffsetDateTime; import java.util.Collection; import java.util.List; import java.util.Map; @@ -33,6 +34,8 @@ import org.sonar.api.server.ws.Change; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; +import org.sonar.api.utils.DateUtils; +import org.sonar.api.utils.MessageException; import org.sonar.api.utils.Paging; import org.sonar.db.DbClient; import org.sonar.db.DbSession; @@ -40,6 +43,7 @@ import org.sonar.db.user.UserDto; import org.sonar.db.user.UserQuery; import org.sonar.server.es.SearchOptions; import org.sonar.server.exceptions.BadRequestException; +import org.sonar.server.exceptions.ServerException; import org.sonar.server.issue.AvatarResolver; import org.sonar.server.management.ManagedInstanceService; import org.sonar.server.user.UserSession; @@ -49,13 +53,12 @@ import org.sonarqube.ws.Users.SearchWsResponse; import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Strings.emptyToNull; -import static java.util.Comparator.comparing; import static java.lang.Boolean.TRUE; +import static java.util.Comparator.comparing; import static java.util.Optional.ofNullable; import static org.sonar.api.server.ws.WebService.Param.PAGE; import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE; import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY; -import static org.sonar.api.utils.DateUtils.formatDateTime; import static org.sonar.api.utils.Paging.forPageIndex; import static org.sonar.core.util.stream.MoreCollectors.toList; import static org.sonar.server.ws.WsUtils.writeProtobuf; @@ -67,8 +70,13 @@ import static org.sonarqube.ws.Users.SearchWsResponse.newBuilder; public class SearchAction implements UsersWsAction { private static final String DEACTIVATED_PARAM = "deactivated"; private static final String MANAGED_PARAM = "managed"; - private static final int MAX_PAGE_SIZE = 500; + + private static final int MAX_PAGE_SIZE = 500; + private static final String LAST_CONNECTION_DATE_FROM = "lastConnectedAfter"; + private static final String LAST_CONNECTION_DATE_TO = "lastConnectedBefore"; + private static final String SONAR_LINT_LAST_CONNECTION_DATE_FROM = "slLastConnectedAfter"; + private static final String SONAR_LINT_LAST_CONNECTION_DATE_TO = "slLastConnectedBefore"; private final UserSession userSession; private final DbClient dbClient; private final AvatarResolver avatarResolver; @@ -93,11 +101,17 @@ public class SearchAction implements UsersWsAction { "
  • 'externalProvider'
  • " + "
  • 'groups'
  • " + "
  • 'lastConnectionDate'
  • " + + "
  • 'sonarLintLastConnectionDate'
  • " + "
  • 'tokensCount'
  • " + "" + "Field 'lastConnectionDate' is only updated every hour, so it may not be accurate, for instance when a user authenticates many times in less than one hour.") .setSince("3.6") .setChangelog( + new Change("10.1", "New optional parameters " + SONAR_LINT_LAST_CONNECTION_DATE_FROM + + " and " + SONAR_LINT_LAST_CONNECTION_DATE_TO + " to filter users by SonarLint last connection date"), + new Change("10.1", "New optional parameters " + LAST_CONNECTION_DATE_FROM + + " and " + LAST_CONNECTION_DATE_TO + " to filter users by SonarQube last connection date"), + new Change("10.1", "New field 'sonarLintLastConnectionDate' is added to response"), new Change("10.0", "'q' parameter values is now always performing a case insensitive match"), new Change("10.0", "New parameter 'managed' to optionally search by managed status"), new Change("10.0", "Response includes 'managed' field."), @@ -112,6 +126,7 @@ public class SearchAction implements UsersWsAction { action.addPagingParams(50, SearchOptions.MAX_PAGE_SIZE); + final String dateExample = "2020-01-01T00:00:00+0100"; action.createParam(TEXT_QUERY) .setMinimumLength(2) .setDescription("Filter on login, name and email.
    " + @@ -128,6 +143,38 @@ public class SearchAction implements UsersWsAction { .setRequired(false) .setDefaultValue(null) .setBooleanPossibleValues(); + action.createParam(LAST_CONNECTION_DATE_FROM) + .setSince("10.1") + .setDescription(""" + Filter the users based on the last connection date field. Only users who interacted with this instance at or after the date will be returned. + The format must be ISO 8601 datetime format (YYYY-MM-DDThh:mm:ss±hhmm)""") + .setRequired(false) + .setDefaultValue(null) + .setExampleValue(dateExample); + action.createParam(LAST_CONNECTION_DATE_TO) + .setSince("10.1") + .setDescription(""" + Filter the users based on the last connection date field. Only users who interacted with this instance at or before the date will be returned. + The format must be ISO 8601 datetime format (YYYY-MM-DDThh:mm:ss±hhmm)""") + .setRequired(false) + .setDefaultValue(null) + .setExampleValue(dateExample); + action.createParam(SONAR_LINT_LAST_CONNECTION_DATE_FROM) + .setSince("10.1") + .setDescription(""" + Filter the users based on the sonar lint last connection date field. Only users who interacted with this instance using SonarLint at or after the date will be returned. + The format must be ISO 8601 datetime format (YYYY-MM-DDThh:mm:ss±hhmm)""") + .setRequired(false) + .setDefaultValue(null) + .setExampleValue(dateExample); + action.createParam(SONAR_LINT_LAST_CONNECTION_DATE_TO) + .setSince("10.1") + .setDescription(""" + Filter the users based on the sonar lint last connection date field. Only users who interacted with this instance using SonarLint at or before the date will be returned. + The format must be ISO 8601 datetime format (YYYY-MM-DDThh:mm:ss±hhmm)""") + .setRequired(false) + .setDefaultValue(null) + .setExampleValue(dateExample); } @Override @@ -156,20 +203,22 @@ public class SearchAction implements UsersWsAction { } private UserQuery buildUserQuery(SearchRequest request) { + UserQuery.UserQueryBuilder builder = UserQuery.builder(); + request.getLastConnectionDateFrom().ifPresent(builder::lastConnectionDateFrom); + request.getLastConnectionDateTo().ifPresent(builder::lastConnectionDateTo); + request.getSonarLintLastConnectionDateFrom().ifPresent(builder::sonarLintLastConnectionDateFrom); + request.getSonarLintLastConnectionDateTo().ifPresent(builder::sonarLintLastConnectionDateTo); + if (managedInstanceService.isInstanceExternallyManaged()) { String managedInstanceSql = Optional.ofNullable(request.isManaged()) .map(managedInstanceService::getManagedUsersSqlFilter) .orElse(null); - return UserQuery.builder() - .isActive(!request.isDeactivated()) - .searchText(request.getQuery()) - .isManagedClause(managedInstanceSql) - .build(); - } - if (request.isManaged() != null) { + builder.isManagedClause(managedInstanceSql); + } else if (request.isManaged() != null) { throw BadRequestException.create("The 'managed' parameter is only available for managed instances."); } - return UserQuery.builder() + + return builder .isActive(!request.isDeactivated()) .searchText(request.getQuery()) .build(); @@ -215,7 +264,9 @@ public class SearchAction implements UsersWsAction { } ofNullable(user.getExternalLogin()).ifPresent(userBuilder::setExternalIdentity); ofNullable(tokensCount).ifPresent(userBuilder::setTokensCount); - ofNullable(user.getLastConnectionDate()).ifPresent(date -> userBuilder.setLastConnectionDate(formatDateTime(date))); + ofNullable(user.getLastConnectionDate()).map(DateUtils::formatDateTime).ifPresent(userBuilder::setLastConnectionDate); + ofNullable(user.getLastSonarlintConnectionDate()) + .map(DateUtils::formatDateTime).ifPresent(userBuilder::setSonarLintLastConnectionDate); userBuilder.setManaged(TRUE.equals(managed)); } return userBuilder.build(); @@ -228,6 +279,10 @@ public class SearchAction implements UsersWsAction { .setQuery(request.param(TEXT_QUERY)) .setDeactivated(request.mandatoryParamAsBoolean(DEACTIVATED_PARAM)) .setManaged(request.paramAsBoolean(MANAGED_PARAM)) + .setLastConnectionDateFrom(request.param(LAST_CONNECTION_DATE_FROM)) + .setLastConnectionDateTo(request.param(LAST_CONNECTION_DATE_TO)) + .setSonarLintLastConnectionDateFrom(request.param(SONAR_LINT_LAST_CONNECTION_DATE_FROM)) + .setSonarLintLastConnectionDateTo(request.param(SONAR_LINT_LAST_CONNECTION_DATE_TO)) .setPage(request.mandatoryParamAsInt(PAGE)) .setPageSize(pageSize) .build(); @@ -239,6 +294,10 @@ public class SearchAction implements UsersWsAction { private final String query; private final boolean deactivated; private final Boolean managed; + private final OffsetDateTime lastConnectionDateFrom; + private final OffsetDateTime lastConnectionDateTo; + private final OffsetDateTime sonarLintLastConnectionDateFrom; + private final OffsetDateTime sonarLintLastConnectionDateTo; private SearchRequest(Builder builder) { this.page = builder.page; @@ -246,6 +305,14 @@ public class SearchAction implements UsersWsAction { this.query = builder.query; this.deactivated = builder.deactivated; this.managed = builder.managed; + try { + this.lastConnectionDateFrom = Optional.ofNullable(builder.lastConnectionDateFrom).map(DateUtils::parseOffsetDateTime).orElse(null); + this.lastConnectionDateTo = Optional.ofNullable(builder.lastConnectionDateTo).map(DateUtils::parseOffsetDateTime).orElse(null); + this.sonarLintLastConnectionDateFrom = Optional.ofNullable(builder.sonarLintLastConnectionDateFrom).map(DateUtils::parseOffsetDateTime).orElse(null); + this.sonarLintLastConnectionDateTo = Optional.ofNullable(builder.sonarLintLastConnectionDateTo).map(DateUtils::parseOffsetDateTime).orElse(null); + } catch (MessageException me) { + throw new ServerException(400, me.getMessage()); + } } public Integer getPage() { @@ -270,6 +337,22 @@ public class SearchAction implements UsersWsAction { return managed; } + public Optional getLastConnectionDateFrom() { + return Optional.ofNullable(lastConnectionDateFrom); + } + + public Optional getLastConnectionDateTo() { + return Optional.ofNullable(lastConnectionDateTo); + } + + public Optional getSonarLintLastConnectionDateFrom() { + return Optional.ofNullable(sonarLintLastConnectionDateFrom); + } + + public Optional getSonarLintLastConnectionDateTo() { + return Optional.ofNullable(sonarLintLastConnectionDateTo); + } + public static Builder builder() { return new Builder(); } @@ -281,6 +364,11 @@ public class SearchAction implements UsersWsAction { private String query; private boolean deactivated; private Boolean managed; + private String lastConnectionDateFrom; + private String lastConnectionDateTo; + private String sonarLintLastConnectionDateFrom; + private String sonarLintLastConnectionDateTo; + private Builder() { // enforce factory method use @@ -311,6 +399,26 @@ public class SearchAction implements UsersWsAction { return this; } + public Builder setLastConnectionDateFrom(@Nullable String lastConnectionDateFrom) { + this.lastConnectionDateFrom = lastConnectionDateFrom; + return this; + } + + public Builder setLastConnectionDateTo(@Nullable String lastConnectionDateTo) { + this.lastConnectionDateTo = lastConnectionDateTo; + return this; + } + + public Builder setSonarLintLastConnectionDateFrom(@Nullable String sonarLintLastConnectionDateFrom) { + this.sonarLintLastConnectionDateFrom = sonarLintLastConnectionDateFrom; + return this; + } + + public Builder setSonarLintLastConnectionDateTo(@Nullable String sonarLintLastConnectionDateTo) { + this.sonarLintLastConnectionDateTo = sonarLintLastConnectionDateTo; + return this; + } + public SearchRequest build() { return new SearchRequest(this); } diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/user/ws/search-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/user/ws/search-example.json index 84e141b731f..110b0fb8377 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/user/ws/search-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/user/ws/search-example.json @@ -19,7 +19,9 @@ "externalIdentity": "fmallet", "externalProvider": "sonarqube", "avatar": "2f9dff586d3f74f825b059e3798a3bbb", - "managed": false + "lastConnectionDate": "2019-03-27T09:51:50+0100", + "managed": false, + "sonarLintLastConnectionDate": "2019-03-27T09:51:50+0100" }, { "login": "sbrandhof", diff --git a/sonar-ws/src/main/protobuf/ws-users.proto b/sonar-ws/src/main/protobuf/ws-users.proto index d8ba5a43868..ed8742bed9f 100644 --- a/sonar-ws/src/main/protobuf/ws-users.proto +++ b/sonar-ws/src/main/protobuf/ws-users.proto @@ -45,6 +45,7 @@ message SearchWsResponse { optional string avatar = 11; optional string lastConnectionDate = 12; optional bool managed = 13; + optional string sonarLintLastConnectionDate = 14; } message Groups { -- 2.39.5