Selaa lähdekoodia

[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.

tags/10.1.0.73491
Steve Marion 1 vuosi sitten
vanhempi
commit
71a95f62be

+ 75
- 2
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserQuery.java Näytä tiedosto

@@ -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);
}
}
}

+ 12
- 0
server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml Näytä tiedosto

@@ -184,6 +184,18 @@
<if test="query.isManagedSqlClause != null">
AND ${query.isManagedSqlClause}
</if>
<if test="query.lastConnectionDateFrom != null">
AND u.last_connection_date &gt;= #{query.lastConnectionDateFrom, jdbcType=BIGINT}
</if>
<if test="query.lastConnectionDateTo != null">
AND u.last_connection_date &lt; #{query.lastConnectionDateTo, jdbcType=BIGINT}
</if>
<if test="query.sonarLintLastConnectionDateFrom != null">
AND u.last_sonarlint_connection &gt;= #{query.sonarLintLastConnectionDateFrom, jdbcType=BIGINT}
</if>
<if test="query.sonarLintLastConnectionDateTo != null">
AND u.last_sonarlint_connection &lt; #{query.sonarLintLastConnectionDateTo, jdbcType=BIGINT}
</if>
</where>
</sql>


+ 6
- 0
server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java Näytä tiedosto

@@ -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<UserDto> selectUserByLogin(String login) {
return Optional.ofNullable(dbClient.userDao().selectByLogin(db.getSession(), login));
}

+ 55
- 1
server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/SearchActionIT.java Näytä tiedosto

@@ -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) {

+ 120
- 12
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/SearchAction.java Näytä tiedosto

@@ -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 {
" <li>'externalProvider'</li>" +
" <li>'groups'</li>" +
" <li>'lastConnectionDate'</li>" +
" <li>'sonarLintLastConnectionDate'</li>" +
" <li>'tokensCount'</li>" +
"</ul>" +
"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.<br />" +
@@ -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<OffsetDateTime> getLastConnectionDateFrom() {
return Optional.ofNullable(lastConnectionDateFrom);
}

public Optional<OffsetDateTime> getLastConnectionDateTo() {
return Optional.ofNullable(lastConnectionDateTo);
}

public Optional<OffsetDateTime> getSonarLintLastConnectionDateFrom() {
return Optional.ofNullable(sonarLintLastConnectionDateFrom);
}

public Optional<OffsetDateTime> 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);
}

+ 3
- 1
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/user/ws/search-example.json Näytä tiedosto

@@ -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",

+ 1
- 0
sonar-ws/src/main/protobuf/ws-users.proto Näytä tiedosto

@@ -45,6 +45,7 @@ message SearchWsResponse {
optional string avatar = 11;
optional string lastConnectionDate = 12;
optional bool managed = 13;
optional string sonarLintLastConnectionDate = 14;
}

message Groups {

Loading…
Peruuta
Tallenna