Преглед изворни кода

SONAR-7254 Feed user.local when creating/updating/reactivating a user

tags/5.5-M11
Julien Lancelot пре 8 година
родитељ
комит
d26a5ac773

+ 31
- 22
server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java Прегледај датотеку

@@ -21,7 +21,6 @@ package org.sonar.server.user;

import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import java.net.HttpURLConnection;
import java.security.SecureRandom;
@@ -48,9 +47,8 @@ import org.sonar.server.exceptions.ServerException;
import org.sonar.server.user.index.UserIndexer;
import org.sonar.server.util.Validation;

import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.Lists.newArrayList;
import static java.util.Arrays.asList;
import static org.sonar.api.CoreProperties.CORE_AUTHENTICATOR_LOCAL_USERS;

@ServerSide
public class UserUpdater {
@@ -72,17 +70,13 @@ public class UserUpdater {
private final DbClient dbClient;
private final UserIndexer userIndexer;
private final System2 system2;
private final SecurityRealmFactory realmFactory;
private final List<String> technicalUsers;

public UserUpdater(NewUserNotifier newUserNotifier, Settings settings, DbClient dbClient, UserIndexer userIndexer, System2 system2, SecurityRealmFactory realmFactory) {
public UserUpdater(NewUserNotifier newUserNotifier, Settings settings, DbClient dbClient, UserIndexer userIndexer, System2 system2) {
this.newUserNotifier = newUserNotifier;
this.settings = settings;
this.dbClient = dbClient;
this.userIndexer = userIndexer;
this.system2 = system2;
this.realmFactory = realmFactory;
this.technicalUsers = asList(settings.getStringArray(CORE_AUTHENTICATOR_LOCAL_USERS));
}

/**
@@ -126,6 +120,8 @@ public class UserUpdater {
if (newUser.password() != null) {
updateUser.setPassword(newUser.password());
}
// Hack to allow to change the password of the user
existingUser.setLocal(true);
updateUserDto(dbSession, updateUser, existingUser);
updateUser(dbSession, existingUser);
addDefaultGroup(dbSession, existingUser);
@@ -230,14 +226,15 @@ public class UserUpdater {
userDto.setEmail(email);
}

String password = updateUser.password();
if (updateUser.isPasswordChanged()) {
validatePasswords(password, messages);
checkPasswordChangeAllowed(updateUser.login(), messages);
if (Strings.isNullOrEmpty(password)) {
userDto.setSalt(null);
userDto.setCryptedPassword(null);
} else {
if (isNewExternalIdentityNotEqualsToSonaQube(updateUser)) {
setExternalIdentity(userDto, updateUser.externalIdentity());
userDto.setSalt(null);
userDto.setCryptedPassword(null);
} else {
String password = updateUser.password();
if (updateUser.isPasswordChanged()) {
validatePasswords(password, messages);
checkPasswordChangeAllowed(userDto, messages);
setEncryptedPassWord(password, userDto);
}
}
@@ -252,7 +249,9 @@ public class UserUpdater {
}
}

setExternalIdentity(userDto, updateUser.externalIdentity());
if (updateUser.isExternalIdentityChanged()) {
setExternalIdentity(userDto, updateUser.externalIdentity());
}

if (!messages.isEmpty()) {
throw new BadRequestException(messages);
@@ -263,21 +262,23 @@ public class UserUpdater {
if (externalIdentity == null) {
dto.setExternalIdentity(dto.getLogin());
dto.setExternalIdentityProvider(SQ_AUTHORITY);
dto.setLocal(true);
} else {
dto.setExternalIdentity(externalIdentity.getId());
dto.setExternalIdentityProvider(externalIdentity.getProvider());
dto.setLocal(false);
}
}

private static void checkNotEmptyParam(@Nullable String value, String param, List<Message> messages) {
if (Strings.isNullOrEmpty(value)) {
if (isNullOrEmpty(value)) {
messages.add(Message.of(Validation.CANT_BE_EMPTY_MESSAGE, param));
}
}

private static String validateLoginFormat(@Nullable String login, List<Message> messages) {
checkNotEmptyParam(login, LOGIN_PARAM, messages);
if (!Strings.isNullOrEmpty(login)) {
if (!isNullOrEmpty(login)) {
if (login.length() < LOGIN_MIN_LENGTH) {
messages.add(Message.of(Validation.IS_TOO_SHORT_MESSAGE, LOGIN_PARAM, LOGIN_MIN_LENGTH));
} else if (login.length() > LOGIN_MAX_LENGTH) {
@@ -302,14 +303,22 @@ public class UserUpdater {
}
}

private void checkPasswordChangeAllowed(String login, List<Message> messages) {
if (realmFactory.hasExternalAuthentication() && !technicalUsers.contains(login)) {
private static void checkPasswordChangeAllowed(UserDto userDto, List<Message> messages) {
if (!userDto.isLocal()) {
messages.add(Message.of("user.password_cant_be_changed_on_external_auth"));
}
}

private static boolean isNewExternalIdentityNotEqualsToSonaQube(UpdateUser updateUser) {
ExternalIdentity externalIdentity = updateUser.externalIdentity();
if (updateUser.isExternalIdentityChanged() && externalIdentity != null) {
return !externalIdentity.getProvider().equals(SQ_AUTHORITY);
}
return false;
}

private static void validatePasswords(@Nullable String password, List<Message> messages) {
if (password != null && password.length() == 0) {
if (password == null || password.length() == 0) {
messages.add(Message.of(Validation.CANT_BE_EMPTY_MESSAGE, PASSWORD_PARAM));
}
}

+ 92
- 63
server/sonar-server/src/test/java/org/sonar/server/user/UserUpdaterTest.java Прегледај датотеку

@@ -35,12 +35,10 @@ import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
import org.sonar.db.user.GroupDao;
import org.sonar.db.user.GroupDto;
import org.sonar.db.user.GroupMembershipDao;
import org.sonar.db.user.GroupMembershipQuery;
import org.sonar.db.user.UserDao;
import org.sonar.db.user.UserDto;
import org.sonar.db.user.UserGroupDao;
import org.sonar.db.user.UserTokenDao;
import org.sonar.db.user.UserTesting;
import org.sonar.server.es.EsTester;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.Message;
@@ -56,31 +54,36 @@ import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.sonar.api.CoreProperties.CORE_AUTHENTICATOR_LOCAL_USERS;
import static org.sonar.api.CoreProperties.CORE_DEFAULT_GROUP;

import static org.sonar.db.user.UserTesting.newDisabledUser;
import static org.sonar.db.user.UserTesting.newUserDto;

public class UserUpdaterTest {

static final long NOW = 1418215735482L;
static final long PAST = 1000000000000L;

static final String DEFAULT_LOGIN = "marius";
static final String TECHNICAL_USER = "tech_user";
@ClassRule
public static EsTester es = new EsTester().addDefinitions(new UserIndexDefinition(new Settings()));
@Rule
public DbTester db = DbTester.create(System2.INSTANCE);

System2 system2 = mock(System2.class);

NewUserNotifier newUserNotifier = mock(NewUserNotifier.class);
@Rule
public DbTester db = DbTester.create(system2);

DbClient dbClient = db.getDbClient();

SecurityRealmFactory realmFactory = mock(SecurityRealmFactory.class);
NewUserNotifier newUserNotifier = mock(NewUserNotifier.class);

ArgumentCaptor<NewUserHandler.Context> newUserHandler = ArgumentCaptor.forClass(NewUserHandler.Context.class);

Settings settings;
UserDao userDao;
GroupDao groupDao;
GroupMembershipFinder groupMembershipFinder;
DbSession session;
Settings settings = new Settings();
UserDao userDao = dbClient.userDao();
GroupDao groupDao = dbClient.groupDao();
GroupMembershipFinder groupMembershipFinder = new GroupMembershipFinder(userDao, dbClient.groupMembershipDao());
DbSession session = db.getSession();
UserIndexer userIndexer;

UserUpdater userUpdater;
@@ -88,24 +91,16 @@ public class UserUpdaterTest {
@Before
public void setUp() {
es.truncateIndices();
settings = new Settings();
settings.setProperty(CORE_AUTHENTICATOR_LOCAL_USERS, TECHNICAL_USER);
session = db.getSession();
userDao = new UserDao(db.myBatis(), system2);
groupDao = new GroupDao(system2);
UserGroupDao userGroupDao = new UserGroupDao();
GroupMembershipDao groupMembershipDao = new GroupMembershipDao(db.myBatis());
groupMembershipFinder = new GroupMembershipFinder(userDao, groupMembershipDao);

DbClient dbClient = new DbClient(db.database(), db.myBatis(), userDao, groupDao, userGroupDao, new UserTokenDao());

userIndexer = (UserIndexer) new UserIndexer(dbClient, es.client()).setEnabled(true);
userUpdater = new UserUpdater(newUserNotifier, settings, dbClient,
userIndexer, system2, realmFactory);
userIndexer, system2);

when(system2.now()).thenReturn(NOW);
}

@Test
public void create_user() {
when(system2.now()).thenReturn(1418215735482L);
createDefaultGroup();

boolean result = userUpdater.create(NewUser.create()
@@ -122,6 +117,7 @@ public class UserUpdaterTest {
assertThat(dto.getEmail()).isEqualTo("user@mail.com");
assertThat(dto.getScmAccountsAsList()).containsOnly("u1", "u_1", "User 1");
assertThat(dto.isActive()).isTrue();
assertThat(dto.isLocal()).isTrue();

assertThat(dto.getSalt()).isNotNull();
assertThat(dto.getCryptedPassword()).isNotNull();
@@ -150,6 +146,7 @@ public class UserUpdaterTest {
UserDto dto = userDao.selectByLogin(session, "user");
assertThat(dto.getExternalIdentity()).isEqualTo("user");
assertThat(dto.getExternalIdentityProvider()).isEqualTo("sonarqube");
assertThat(dto.isLocal()).isTrue();
}

@Test
@@ -165,6 +162,7 @@ public class UserUpdaterTest {
UserDto dto = userDao.selectByLogin(session, "ABCD");
assertThat(dto.getExternalIdentity()).isEqualTo("user");
assertThat(dto.getExternalIdentityProvider()).isEqualTo("github");
assertThat(dto.isLocal()).isFalse();
}

@Test
@@ -486,8 +484,11 @@ public class UserUpdaterTest {

@Test
public void reactivate_user_when_creating_user_with_existing_login() {
db.prepareDbUnit(getClass(), "reactivate_user.xml");
when(system2.now()).thenReturn(1418215735486L);
addUser(newDisabledUser(DEFAULT_LOGIN)
.setLocal(false)
.setCreatedAt(PAST)
.setUpdatedAt(PAST)
);
createDefaultGroup();

boolean result = userUpdater.create(NewUser.create()
@@ -502,11 +503,12 @@ public class UserUpdaterTest {
assertThat(dto.getName()).isEqualTo("Marius2");
assertThat(dto.getEmail()).isEqualTo("marius2@mail.com");
assertThat(dto.getScmAccounts()).isNull();
assertThat(dto.isLocal()).isTrue();

assertThat(dto.getSalt()).isNotEqualTo("79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365");
assertThat(dto.getCryptedPassword()).isNotEqualTo("650d2261c98361e2f67f90ce5c65a95e7d8ea2fg");
assertThat(dto.getCreatedAt()).isEqualTo(1418215735482L);
assertThat(dto.getUpdatedAt()).isEqualTo(1418215735486L);
assertThat(dto.getCreatedAt()).isEqualTo(PAST);
assertThat(dto.getUpdatedAt()).isEqualTo(NOW);

assertThat(result).isTrue();
}
@@ -539,8 +541,11 @@ public class UserUpdaterTest {

@Test
public void update_external_provider_when_reactivating_user() {
db.prepareDbUnit(getClass(), "reactivate_user.xml");
when(system2.now()).thenReturn(1418215735486L);
addUser(newDisabledUser(DEFAULT_LOGIN)
.setLocal(true)
.setCreatedAt(PAST)
.setUpdatedAt(PAST)
);
createDefaultGroup();

userUpdater.create(NewUser.create()
@@ -553,6 +558,7 @@ public class UserUpdaterTest {
UserDto dto = userDao.selectByLogin(session, DEFAULT_LOGIN);
assertThat(dto.getExternalIdentity()).isEqualTo("john");
assertThat(dto.getExternalIdentityProvider()).isEqualTo("github");
assertThat(dto.isLocal()).isFalse();
}

@Test
@@ -625,14 +631,17 @@ public class UserUpdaterTest {
}

@Test
public void update_user_external_identity() {
db.prepareDbUnit(getClass(), "update_user.xml");
when(system2.now()).thenReturn(1418215735486L);
public void update_user_external_identity_when_user_was_not_local() {
addUser(UserTesting.newExternalUser(DEFAULT_LOGIN, "Marius", "marius@email.com")
.setCreatedAt(PAST)
.setUpdatedAt(PAST)
);
createDefaultGroup();

userUpdater.update(UpdateUser.create(DEFAULT_LOGIN)
.setName("Marius2")
.setPassword("password2")
.setEmail("marius2@email.com")
.setPassword(null)
.setExternalIdentity(new ExternalIdentity("github", "john")));
session.commit();
session.clearCache();
@@ -640,6 +649,32 @@ public class UserUpdaterTest {
UserDto dto = userDao.selectByLogin(session, DEFAULT_LOGIN);
assertThat(dto.getExternalIdentity()).isEqualTo("john");
assertThat(dto.getExternalIdentityProvider()).isEqualTo("github");
assertThat(dto.getUpdatedAt()).isEqualTo(NOW);
}

@Test
public void update_user_external_identity_when_user_was_local() {
addUser(UserTesting.newLocalUser(DEFAULT_LOGIN, "Marius", "marius@email.com")
.setCreatedAt(PAST)
.setUpdatedAt(PAST)
);
createDefaultGroup();

userUpdater.update(UpdateUser.create(DEFAULT_LOGIN)
.setName("Marius2")
.setEmail("marius2@email.com")
.setPassword(null)
.setExternalIdentity(new ExternalIdentity("github", "john")));
session.commit();
session.clearCache();

UserDto dto = userDao.selectByLogin(session, DEFAULT_LOGIN);
assertThat(dto.getExternalIdentity()).isEqualTo("john");
assertThat(dto.getExternalIdentityProvider()).isEqualTo("github");
// Password must be removed
assertThat(dto.getCryptedPassword()).isNull();
assertThat(dto.getSalt()).isNull();
assertThat(dto.getUpdatedAt()).isEqualTo(NOW);
}

@Test
@@ -802,25 +837,26 @@ public class UserUpdaterTest {
}

@Test
public void update_password_with_null_value() {
db.prepareDbUnit(getClass(), "update_user.xml");
public void fail_to_set_null_password_when_local_user() {
addUser(UserTesting.newLocalUser(DEFAULT_LOGIN, "Marius", "marius@email.com"));
createDefaultGroup();

userUpdater.update(UpdateUser.create(DEFAULT_LOGIN)
.setPassword(null));
session.commit();
session.clearCache();

UserDto dto = userDao.selectByLogin(session, DEFAULT_LOGIN);
assertThat(dto.getSalt()).isNull();
assertThat(dto.getCryptedPassword()).isNull();
try {
userUpdater.update(UpdateUser.create(DEFAULT_LOGIN)
.setPassword(null));
} catch (BadRequestException e) {
assertThat(e.errors().messages()).containsOnly(Message.of("errors.cant_be_empty", "Password"));
}
}

@Test
public void fail_to_update_password_when_external_auth_is_used() {
db.prepareDbUnit(getClass(), "update_user.xml");
public void fail_to_update_password_when_user_is_not_local() {
UserDto user = newUserDto()
.setLogin(DEFAULT_LOGIN)
.setLocal(false);
userDao.insert(session, user);
session.commit();
createDefaultGroup();
when(realmFactory.hasExternalAuthentication()).thenReturn(true);

try {
userUpdater.update(UpdateUser.create(DEFAULT_LOGIN)
@@ -830,19 +866,6 @@ public class UserUpdaterTest {
}
}

@Test
public void update_password_of_technical_user_when_external_auth_is_used() {
db.prepareDbUnit(getClass(), "update_technical_user.xml");
createDefaultGroup();
when(realmFactory.hasExternalAuthentication()).thenReturn(true);

userUpdater.update(UpdateUser.create(TECHNICAL_USER)
.setPassword("password2"));

UserDto dto = userDao.selectByLogin(session, TECHNICAL_USER);
assertThat(dto.getSalt()).isNotEqualTo("79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365");
}

@Test
public void not_associate_default_group_when_updating_user() {
db.prepareDbUnit(getClass(), "associate_default_groups_when_updating_user.xml");
@@ -953,4 +976,10 @@ public class UserUpdaterTest {
groupDao.insert(session, new GroupDto().setName("sonar-users").setDescription("Sonar Users"));
session.commit();
}

private UserDto addUser(UserDto user) {
userDao.insert(session, user);
session.commit();
return user;
}
}

+ 8
- 2
server/sonar-server/src/test/java/org/sonar/server/user/ws/ChangePasswordActionTest.java Прегледај датотеку

@@ -40,6 +40,7 @@ import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.user.ExternalIdentity;
import org.sonar.server.user.NewUser;
import org.sonar.server.user.NewUserNotifier;
import org.sonar.server.user.SecurityRealmFactory;
@@ -99,7 +100,7 @@ public class ChangePasswordActionTest {

userIndexer = (UserIndexer) new UserIndexer(dbClient, esTester.client()).setEnabled(true);
index = new UserIndex(esTester.client());
userUpdater = new UserUpdater(mock(NewUserNotifier.class), settings, dbClient, userIndexer, system2, realmFactory);
userUpdater = new UserUpdater(mock(NewUserNotifier.class), settings, dbClient, userIndexer, system2);
tester = new WsTester(new UsersWs(new ChangePasswordAction(userUpdater, userSessionRule)));
controller = tester.controller("api/users");
}
@@ -190,7 +191,12 @@ public class ChangePasswordActionTest {

@Test(expected = BadRequestException.class)
public void fail_to_update_password_on_external_auth() throws Exception {
createUser();
userUpdater.create(NewUser.create()
.setEmail("john@email.com")
.setLogin("john")
.setName("John")
.setScmAccounts(newArrayList("jn"))
.setExternalIdentity(new ExternalIdentity("gihhub", "john")));
session.clearCache();
when(realmFactory.hasExternalAuthentication()).thenReturn(true);


+ 1
- 3
server/sonar-server/src/test/java/org/sonar/server/user/ws/CreateActionTest.java Прегледај датотеку

@@ -42,7 +42,6 @@ import org.sonar.server.es.EsTester;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.user.NewUserNotifier;
import org.sonar.server.user.SecurityRealmFactory;
import org.sonar.server.user.UserUpdater;
import org.sonar.server.user.index.UserDoc;
import org.sonar.server.user.index.UserIndex;
@@ -54,7 +53,6 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;


public class CreateActionTest {

static final Settings settings = new Settings().setProperty("sonar.defaultGroup", "sonar-users");
@@ -99,7 +97,7 @@ public class CreateActionTest {
userIndexer = (UserIndexer) new UserIndexer(dbClient, esTester.client()).setEnabled(true);
index = new UserIndex(esTester.client());
tester = new WsTester(new UsersWs(new CreateAction(dbClient,
new UserUpdater(mock(NewUserNotifier.class), settings, dbClient, userIndexer, system2, mock(SecurityRealmFactory.class)),
new UserUpdater(mock(NewUserNotifier.class), settings, dbClient, userIndexer, system2),
i18n, userSessionRule, new UserJsonWriter(userSessionRule))));
controller = tester.controller("api/users");
}

+ 1
- 2
server/sonar-server/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java Прегледај датотеку

@@ -40,7 +40,6 @@ import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.user.NewUserNotifier;
import org.sonar.server.user.SecurityRealmFactory;
import org.sonar.server.user.UserUpdater;
import org.sonar.server.user.index.UserDoc;
import org.sonar.server.user.index.UserIndex;
@@ -84,7 +83,7 @@ public class DeactivateActionTest {
userIndexer = (UserIndexer) new UserIndexer(dbClient, esTester.client()).setEnabled(true);
index = new UserIndex(esTester.client());
ws = new WsTester(new UsersWs(new DeactivateAction(
new UserUpdater(mock(NewUserNotifier.class), settings, dbClient, userIndexer, system2, mock(SecurityRealmFactory.class)), userSessionRule,
new UserUpdater(mock(NewUserNotifier.class), settings, dbClient, userIndexer, system2), userSessionRule,
new UserJsonWriter(userSessionRule), dbClient)));
controller = ws.controller("api/users");
}

+ 1
- 2
server/sonar-server/src/test/java/org/sonar/server/user/ws/UpdateActionTest.java Прегледај датотеку

@@ -38,7 +38,6 @@ import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.user.NewUserNotifier;
import org.sonar.server.user.SecurityRealmFactory;
import org.sonar.server.user.UserUpdater;
import org.sonar.server.user.index.UserIndexDefinition;
import org.sonar.server.user.index.UserIndexer;
@@ -82,7 +81,7 @@ public class UpdateActionTest {

userIndexer = (UserIndexer) new UserIndexer(dbClient, esTester.client()).setEnabled(true);
tester = new WsTester(new UsersWs(new UpdateAction(
new UserUpdater(mock(NewUserNotifier.class), settings, dbClient, userIndexer, system2, mock(SecurityRealmFactory.class)), userSessionRule,
new UserUpdater(mock(NewUserNotifier.class), settings, dbClient, userIndexer, system2), userSessionRule,
new UserJsonWriter(userSessionRule), dbClient)));
controller = tester.controller("api/users");
}

+ 41
- 0
sonar-db/src/test/java/org/sonar/db/user/UserTesting.java Прегледај датотеку

@@ -34,6 +34,7 @@ public class UserTesting {
public static UserDto newUserDto(String login, String name, String email) {
return new UserDto()
.setActive(true)
.setLocal(true)
.setName(name)
.setEmail(email)
.setLogin(login)
@@ -47,4 +48,44 @@ public class UserTesting {
.setCreatedAt(nextLong())
.setUpdatedAt(nextLong());
}

public static UserDto newLocalUser(String login, String name, String email) {
return new UserDto()
.setActive(true)
.setLocal(true)
.setName(name)
.setEmail(email)
.setLogin(login)
.setScmAccounts(randomAlphanumeric(40))
.setExternalIdentity(login)
.setExternalIdentityProvider("sonarqube")
.setRememberToken(randomAlphanumeric(500))
.setRememberTokenExpiresAt(new Date((long)nextInt()))
.setSalt(randomAlphanumeric(40))
.setCryptedPassword(randomAlphanumeric(40))
.setCreatedAt(nextLong())
.setUpdatedAt(nextLong());
}

public static UserDto newExternalUser(String login, String name, String email) {
return new UserDto()
.setActive(true)
.setLocal(false)
.setName(name)
.setEmail(email)
.setLogin(login)
.setScmAccounts(randomAlphanumeric(40))
.setExternalIdentity(randomAlphanumeric(40))
.setExternalIdentityProvider(randomAlphanumeric(40))
.setCreatedAt(nextLong())
.setUpdatedAt(nextLong());
}

public static UserDto newDisabledUser(String login){
return new UserDto()
.setLogin(login)
.setActive(false)
.setCreatedAt(nextLong())
.setUpdatedAt(nextLong());
}
}

Loading…
Откажи
Сачувај