@@ -36,6 +36,7 @@ public class UserQuery { | |||
private final Long lastConnectionDateTo; | |||
private final Long sonarLintLastConnectionDateFrom; | |||
private final Long sonarLintLastConnectionDateTo; | |||
private final String externalLogin; | |||
private final Set<String> userUuids; | |||
private UserQuery(UserQuery userQuery, Collection<String> userUuids) { | |||
@@ -46,12 +47,14 @@ public class UserQuery { | |||
this.lastConnectionDateTo = userQuery.getLastConnectionDateTo(); | |||
this.sonarLintLastConnectionDateTo = userQuery.getSonarLintLastConnectionDateTo(); | |||
this.sonarLintLastConnectionDateFrom = userQuery.getSonarLintLastConnectionDateFrom(); | |||
this.externalLogin = userQuery.externalLogin; | |||
this.userUuids = new HashSet<>(userUuids); | |||
} | |||
private UserQuery(@Nullable String searchText, @Nullable Boolean isActive, @Nullable String isManagedSqlClause, | |||
@Nullable OffsetDateTime lastConnectionDateFrom, @Nullable OffsetDateTime lastConnectionDateTo, | |||
@Nullable OffsetDateTime sonarLintLastConnectionDateFrom, @Nullable OffsetDateTime sonarLintLastConnectionDateTo, @Nullable Set<String> userUuids) { | |||
@Nullable OffsetDateTime sonarLintLastConnectionDateFrom, @Nullable OffsetDateTime sonarLintLastConnectionDateTo, @Nullable String externalLogin, | |||
@Nullable Set<String> userUuids) { | |||
this.searchText = searchTextToSearchTextSql(searchText); | |||
this.isActive = isActive; | |||
this.isManagedSqlClause = isManagedSqlClause; | |||
@@ -59,6 +62,7 @@ public class UserQuery { | |||
this.lastConnectionDateTo = formatDateToInput(lastConnectionDateTo); | |||
this.sonarLintLastConnectionDateFrom = parseDateToLong(sonarLintLastConnectionDateFrom); | |||
this.sonarLintLastConnectionDateTo = formatDateToInput(sonarLintLastConnectionDateTo); | |||
this.externalLogin = externalLogin; | |||
this.userUuids = userUuids; | |||
} | |||
@@ -128,6 +132,11 @@ public class UserQuery { | |||
return sonarLintLastConnectionDateTo; | |||
} | |||
@CheckForNull | |||
public String getExternalLogin() { | |||
return externalLogin; | |||
} | |||
@CheckForNull | |||
public Set<String> getUserUuids() { | |||
return userUuids; | |||
@@ -145,6 +154,7 @@ public class UserQuery { | |||
private OffsetDateTime lastConnectionDateTo = null; | |||
private OffsetDateTime sonarLintLastConnectionDateFrom = null; | |||
private OffsetDateTime sonarLintLastConnectionDateTo = null; | |||
private String externalLogin = null; | |||
private Set<String> userUuids = null; | |||
private UserQueryBuilder() { | |||
@@ -185,6 +195,11 @@ public class UserQuery { | |||
return this; | |||
} | |||
public UserQueryBuilder externalLogin(@Nullable String externalLogin) { | |||
this.externalLogin = externalLogin; | |||
return this; | |||
} | |||
public UserQueryBuilder userUuids(@Nullable Set<String> userUuids) { | |||
this.userUuids = userUuids; | |||
return this; | |||
@@ -193,7 +208,7 @@ public class UserQuery { | |||
public UserQuery build() { | |||
return new UserQuery( | |||
searchText, isActive, isManagedSqlClause, lastConnectionDateFrom, lastConnectionDateTo, | |||
sonarLintLastConnectionDateFrom, sonarLintLastConnectionDateTo, userUuids); | |||
sonarLintLastConnectionDateFrom, sonarLintLastConnectionDateTo, externalLogin, userUuids); | |||
} | |||
} | |||
} |
@@ -169,6 +169,9 @@ | |||
<if test="query.sonarLintLastConnectionDateTo != null"> | |||
AND (u.last_sonarlint_connection is null or u.last_sonarlint_connection < #{query.sonarLintLastConnectionDateTo, jdbcType=BIGINT}) | |||
</if> | |||
<if test="query.externalLogin != null"> | |||
AND (u.external_login = #{query.externalLogin, jdbcType=VARCHAR}) | |||
</if> | |||
</where> | |||
</sql> | |||
@@ -53,6 +53,7 @@ public class UserQueryTest { | |||
.lastConnectionDateTo(OffsetDateTime.now().plus(1, ChronoUnit.DECADES)) | |||
.sonarLintLastConnectionDateFrom(OffsetDateTime.now().plus(2, ChronoUnit.DAYS)) | |||
.sonarLintLastConnectionDateTo(OffsetDateTime.now().minus(2, ChronoUnit.DECADES)) | |||
.externalLogin("externalLogin") | |||
.build(); | |||
} | |||
} |
@@ -233,6 +233,34 @@ public class UserServiceIT { | |||
}); | |||
} | |||
@Test | |||
public void search_whenFilteringByExternalLoginAndMatchFound_returnsTheCorrectResult() { | |||
prepareUsersWithExternalLogin(); | |||
SearchResults<UserInformation> users = userService.findUsers(UsersSearchRequest.builder().setPage(1).setPageSize(50).setExternalLogin("user1").build()); | |||
assertThat(users.searchResults()) | |||
.extracting(r -> r.userDto().getExternalLogin()) | |||
.containsExactly("user1"); | |||
} | |||
@Test | |||
public void search_whenFilteringByExternalLoginAndNoMatchFound_returnsNoResult() { | |||
prepareUsersWithExternalLogin(); | |||
SearchResults<UserInformation> users = userService.findUsers(UsersSearchRequest.builder().setPage(1).setPageSize(50).setExternalLogin("nomatch").build()); | |||
assertThat(users.searchResults()) | |||
.extracting(r -> r.userDto().getExternalLogin()) | |||
.isEmpty(); | |||
} | |||
private void prepareUsersWithExternalLogin() { | |||
db.users().insertUser(user -> user.setExternalLogin("user1")); | |||
db.users().insertUser(user -> user.setExternalLogin("USER1")); | |||
db.users().insertUser(user -> user.setExternalLogin("user1-oldaccount")); | |||
} | |||
@Test | |||
public void return_scm_accounts() { | |||
UserDto user = db.users().insertUser(u -> u.setScmAccounts(asList("john1", "john2"))); |
@@ -92,6 +92,7 @@ public class UserService { | |||
request.getLastConnectionDateTo().ifPresent(builder::lastConnectionDateTo); | |||
request.getSonarLintLastConnectionDateFrom().ifPresent(builder::sonarLintLastConnectionDateFrom); | |||
request.getSonarLintLastConnectionDateTo().ifPresent(builder::sonarLintLastConnectionDateTo); | |||
request.getExternalLogin().ifPresent(builder::externalLogin); | |||
if (managedInstanceService.isInstanceExternallyManaged()) { | |||
String managedInstanceSql = Optional.ofNullable(request.isManaged()) |
@@ -37,6 +37,7 @@ public class UsersSearchRequest { | |||
private final OffsetDateTime lastConnectionDateTo; | |||
private final OffsetDateTime sonarLintLastConnectionDateFrom; | |||
private final OffsetDateTime sonarLintLastConnectionDateTo; | |||
private final String externalLogin; | |||
private UsersSearchRequest(Builder builder) { | |||
this.page = builder.page; | |||
@@ -44,6 +45,7 @@ public class UsersSearchRequest { | |||
this.query = builder.query; | |||
this.deactivated = builder.deactivated; | |||
this.managed = builder.managed; | |||
this.externalLogin = builder.externalLogin; | |||
try { | |||
this.lastConnectionDateFrom = Optional.ofNullable(builder.lastConnectionDateFrom).map(DateUtils::parseOffsetDateTime).orElse(null); | |||
this.lastConnectionDateTo = Optional.ofNullable(builder.lastConnectionDateTo).map(DateUtils::parseOffsetDateTime).orElse(null); | |||
@@ -92,6 +94,10 @@ public class UsersSearchRequest { | |||
return Optional.ofNullable(sonarLintLastConnectionDateTo); | |||
} | |||
public Optional<String> getExternalLogin() { | |||
return Optional.ofNullable(externalLogin); | |||
} | |||
public static Builder builder() { | |||
return new Builder(); | |||
} | |||
@@ -106,6 +112,7 @@ public class UsersSearchRequest { | |||
private String lastConnectionDateTo; | |||
private String sonarLintLastConnectionDateFrom; | |||
private String sonarLintLastConnectionDateTo; | |||
private String externalLogin; | |||
private Builder() { | |||
// enforce factory method use | |||
@@ -156,6 +163,11 @@ public class UsersSearchRequest { | |||
return this; | |||
} | |||
public Builder setExternalLogin(@Nullable String externalLogin) { | |||
this.externalLogin = externalLogin; | |||
return this; | |||
} | |||
public UsersSearchRequest build() { | |||
return new UsersSearchRequest(this); | |||
} |
@@ -65,6 +65,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
import static org.sonar.api.utils.DateUtils.formatDateTime; | |||
import static org.sonar.server.user.ws.SearchAction.EXTERNAL_IDENTITY; | |||
import static org.sonar.test.JsonAssert.assertJson; | |||
public class SearchActionIT { | |||
@@ -505,7 +506,7 @@ public class SearchActionIT { | |||
assertThat(action).isNotNull(); | |||
assertThat(action.isPost()).isFalse(); | |||
assertThat(action.responseExampleAsString()).isNotEmpty(); | |||
assertThat(action.params()).hasSize(9); | |||
assertThat(action.params()).hasSize(10); | |||
} | |||
@Test | |||
@@ -569,13 +570,6 @@ public class SearchActionIT { | |||
.forEach(SearchActionIT::assertForbiddenException); | |||
} | |||
private static void assertForbiddenException(TestRequest testRequest) { | |||
assertThatThrownBy(() -> testRequest.executeProtobuf(SearchWsResponse.class)) | |||
.asInstanceOf(InstanceOfAssertFactories.type(ServerException.class)) | |||
.extracting(ServerException::httpCode) | |||
.isEqualTo(403); | |||
} | |||
private void assertUserWithFilter(String field, Instant filterValue, String userLogin, boolean isExpectedToBeThere) { | |||
var assertion = assertThat(ws.newRequest() | |||
.setParam("q", "user-%_%-") | |||
@@ -601,4 +595,55 @@ public class SearchActionIT { | |||
); | |||
} | |||
@Test | |||
public void search_whenFilteringOnExternalIdentityAndNotAdmin_shouldThrow() { | |||
userSession.logIn(); | |||
TestRequest testRequest = ws.newRequest() | |||
.setParam(EXTERNAL_IDENTITY, "login"); | |||
assertForbiddenException(testRequest); | |||
} | |||
private static void assertForbiddenException(TestRequest testRequest) { | |||
assertThatThrownBy(() -> testRequest.executeProtobuf(SearchWsResponse.class)) | |||
.asInstanceOf(InstanceOfAssertFactories.type(ServerException.class)) | |||
.extracting(ServerException::httpCode) | |||
.isEqualTo(403); | |||
} | |||
@Test | |||
public void search_whenFilteringOnExternalIdentityAndMatch_shouldReturnMatchingUser() { | |||
userSession.logIn().setSystemAdministrator(); | |||
prepareUsersWithExternalLogin(); | |||
TestRequest testRequest = ws.newRequest() | |||
.setParam(EXTERNAL_IDENTITY, "user1"); | |||
assertThat(testRequest.executeProtobuf(SearchWsResponse.class).getUsersList()) | |||
.extracting(User::getExternalIdentity) | |||
.containsExactly("user1"); | |||
} | |||
@Test | |||
public void search_whenFilteringOnExternalIdentityAndNoMatch_shouldReturnMatchingUser() { | |||
userSession.logIn().setSystemAdministrator(); | |||
prepareUsersWithExternalLogin(); | |||
TestRequest testRequest = ws.newRequest() | |||
.setParam(EXTERNAL_IDENTITY, "nomatch"); | |||
assertThat(testRequest.executeProtobuf(SearchWsResponse.class).getUsersList()) | |||
.extracting(User::getExternalIdentity) | |||
.isEmpty(); | |||
} | |||
private void prepareUsersWithExternalLogin() { | |||
db.users().insertUser(user -> user.setExternalLogin("user1")); | |||
db.users().insertUser(user -> user.setExternalLogin("USER1")); | |||
db.users().insertUser(user -> user.setExternalLogin("user1-oldaccount")); | |||
} | |||
} |
@@ -42,16 +42,17 @@ import static org.sonar.server.common.PaginationInformation.forPageIndex; | |||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | |||
public class SearchAction implements UsersWsAction { | |||
private static final int MAX_PAGE_SIZE = 500; | |||
private static final String DEACTIVATED_PARAM = "deactivated"; | |||
private static final String MANAGED_PARAM = "managed"; | |||
private static final int MAX_PAGE_SIZE = 500; | |||
static final String LAST_CONNECTION_DATE_FROM = "lastConnectedAfter"; | |||
static final String LAST_CONNECTION_DATE_TO = "lastConnectedBefore"; | |||
static final String SONAR_LINT_LAST_CONNECTION_DATE_FROM = "slLastConnectedAfter"; | |||
static final String SONAR_LINT_LAST_CONNECTION_DATE_TO = "slLastConnectedBefore"; | |||
private final UserSession userSession; | |||
static final String EXTERNAL_IDENTITY = "externalIdentity"; | |||
private final UserSession userSession; | |||
private final UserService userService; | |||
private final SearchWsReponseGenerator searchWsReponseGenerator; | |||
@@ -79,6 +80,7 @@ public class SearchAction implements UsersWsAction { | |||
"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.3", "New optional parameters " + EXTERNAL_IDENTITY + " to find a user by its IdP login"), | |||
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. Only available with Administer System permission."), | |||
new Change("10.1", "New optional parameters " + LAST_CONNECTION_DATE_FROM + | |||
@@ -136,7 +138,7 @@ public class SearchAction implements UsersWsAction { | |||
action.createParam(SONAR_LINT_LAST_CONNECTION_DATE_FROM) | |||
.setSince("10.1") | |||
.setDescription(""" | |||
Filter the users based on the sonar lint last connection date field | |||
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) | |||
@@ -151,6 +153,12 @@ public class SearchAction implements UsersWsAction { | |||
.setRequired(false) | |||
.setDefaultValue(null) | |||
.setExampleValue(dateExample); | |||
action.createParam(EXTERNAL_IDENTITY) | |||
.setSince("10.3") | |||
.setDescription(""" | |||
Find a user by its external identity (ie. its login in the Identity Provider). | |||
This is case sensitive and only available with Administer System permission. | |||
"""); | |||
} | |||
@Override | |||
@@ -166,6 +174,7 @@ public class SearchAction implements UsersWsAction { | |||
throwIfParameterValuePresent(request, LAST_CONNECTION_DATE_TO); | |||
throwIfParameterValuePresent(request, SONAR_LINT_LAST_CONNECTION_DATE_FROM); | |||
throwIfParameterValuePresent(request, SONAR_LINT_LAST_CONNECTION_DATE_TO); | |||
throwIfParameterValuePresent(request, EXTERNAL_IDENTITY); | |||
} | |||
} | |||
@@ -176,7 +185,7 @@ public class SearchAction implements UsersWsAction { | |||
return searchWsReponseGenerator.toUsersForResponse(userSearchResults.searchResults(), paging); | |||
} | |||
private UsersSearchRequest toSearchRequest(Request request) { | |||
private static UsersSearchRequest toSearchRequest(Request request) { | |||
int pageSize = request.mandatoryParamAsInt(PAGE_SIZE); | |||
checkArgument(pageSize <= MAX_PAGE_SIZE, "The '%s' parameter must be less than %s", PAGE_SIZE, MAX_PAGE_SIZE); | |||
return UsersSearchRequest.builder() | |||
@@ -187,6 +196,7 @@ public class SearchAction implements UsersWsAction { | |||
.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)) | |||
.setExternalLogin(request.param(EXTERNAL_IDENTITY)) | |||
.setPage(request.mandatoryParamAsInt(PAGE)) | |||
.setPageSize(pageSize) | |||
.build(); |