@@ -1,30 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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.organization; | |||
import org.sonar.api.server.ServerSide; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.organization.OrganizationDto; | |||
@ServerSide | |||
public interface OrganizationAlmBinding { | |||
void bindOrganization(DbSession dbSession, OrganizationDto organization, String installationId, boolean isNewOrganization); | |||
} |
@@ -1,180 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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.organization; | |||
import java.util.function.Consumer; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import org.sonar.api.web.UserRole; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.organization.OrganizationDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.usergroups.DefaultGroupCreatorImpl; | |||
import static java.util.Objects.requireNonNull; | |||
public interface OrganizationUpdater { | |||
String OWNERS_GROUP_NAME = "Owners"; | |||
String OWNERS_GROUP_DESCRIPTION = "Owners of organization"; | |||
String PERM_TEMPLATE_NAME = "Default template"; | |||
String PERM_TEMPLATE_DESCRIPTION_PATTERN = "Default permission template of organization %s"; | |||
/** | |||
* Create a new organization with the specified properties and of which the specified user will assign | |||
* Administer Organization permission. | |||
* <p> | |||
* This method does several operations at once: | |||
* <ol> | |||
* <li>create an ungarded organization with the specified details</li> | |||
* <li>create a group called {@link #OWNERS_GROUP_NAME Owners} with all organization wide permissions</li> | |||
* <li>create a group called {@link DefaultGroupCreatorImpl#DEFAULT_GROUP_NAME members} with browse permissions</li> | |||
* <li>make the specified user a member of these groups</li> | |||
* <li>create a default template for the organization | |||
* <ul> | |||
* <li>name is {@link #PERM_TEMPLATE_NAME Default template}</li> | |||
* <li>description follows pattern {@link #PERM_TEMPLATE_DESCRIPTION_PATTERN} based on the organization name</li> | |||
* </ul> | |||
* </li> | |||
* <li>this permission template defines the specified permissions (which effectively makes projects public): | |||
* <ul> | |||
* <li>group {@link #OWNERS_GROUP_NAME Owners} : {@link UserRole#ADMIN ADMIN}</li> | |||
* <li>group {@link #OWNERS_GROUP_NAME Owners} : {@link UserRole#ISSUE_ADMIN ISSUE_ADMIN}</li> | |||
* <li>group {@link #OWNERS_GROUP_NAME Owners} : {@link UserRole#SECURITYHOTSPOT_ADMIN SECURITYHOTSPOT_ADMIN}</li> | |||
* <li>group {@link #OWNERS_GROUP_NAME Owners} : {@link UserRole#SCAN SCAN}</li> | |||
* <li>group {@link DefaultGroupCreatorImpl#DEFAULT_GROUP_NAME members} : {@link UserRole#USER USER}</li> | |||
* <li>group {@link DefaultGroupCreatorImpl#DEFAULT_GROUP_NAME members} : {@link UserRole#CODEVIEWER CODEVIEWER}</li> | |||
* </ul> | |||
* </li> | |||
* </ol> | |||
* </p> | |||
* | |||
* @return the created organization | |||
* | |||
* @throws KeyConflictException if an organization with the specified key already exists | |||
* @throws IllegalArgumentException if any field of {@code newOrganization} is invalid according to {@link OrganizationValidation} | |||
*/ | |||
OrganizationDto create(DbSession dbSession, UserDto userCreator, NewOrganization newOrganization, Consumer<OrganizationDto> beforeCommit) throws KeyConflictException; | |||
/** | |||
* Update the personal organization key of a user. | |||
* No update will be performed if generated key match the same key as existing one. | |||
* | |||
* @throws IllegalStateException if user has no no personal organization | |||
* @throws IllegalStateException if personal organization uuid does not exist | |||
* @throws IllegalStateException if an organization with the key generated from the login already exists | |||
*/ | |||
void updateOrganizationKey(DbSession dbSession, OrganizationDto organization, String newKey); | |||
final class KeyConflictException extends Exception { | |||
KeyConflictException(String message) { | |||
super(message); | |||
} | |||
} | |||
final class NewOrganization { | |||
private final String key; | |||
private final String name; | |||
@CheckForNull | |||
private final String description; | |||
@CheckForNull | |||
private final String url; | |||
@CheckForNull | |||
private final String avatar; | |||
private NewOrganization(Builder builder) { | |||
this.key = builder.key; | |||
this.name = builder.name; | |||
this.description = builder.description; | |||
this.url = builder.url; | |||
this.avatar = builder.avatarUrl; | |||
} | |||
public String getKey() { | |||
return key; | |||
} | |||
public String getName() { | |||
return name; | |||
} | |||
@CheckForNull | |||
public String getDescription() { | |||
return description; | |||
} | |||
@CheckForNull | |||
public String getUrl() { | |||
return url; | |||
} | |||
@CheckForNull | |||
public String getAvatar() { | |||
return avatar; | |||
} | |||
public static NewOrganization.Builder newOrganizationBuilder() { | |||
return new Builder(); | |||
} | |||
public static final class Builder { | |||
private String key; | |||
private String name; | |||
private String description; | |||
private String url; | |||
private String avatarUrl; | |||
private Builder() { | |||
// use factory method | |||
} | |||
public Builder setKey(String key) { | |||
this.key = requireNonNull(key, "key can't be null"); | |||
return this; | |||
} | |||
public Builder setName(String name) { | |||
this.name = requireNonNull(name, "name can't be null"); | |||
return this; | |||
} | |||
public Builder setDescription(@Nullable String description) { | |||
this.description = description; | |||
return this; | |||
} | |||
public Builder setUrl(@Nullable String url) { | |||
this.url = url; | |||
return this; | |||
} | |||
public Builder setAvatarUrl(@Nullable String avatarUrl) { | |||
this.avatarUrl = avatarUrl; | |||
return this; | |||
} | |||
public NewOrganization build() { | |||
requireNonNull(key, "key can't be null"); | |||
requireNonNull(name, "name can't be null"); | |||
return new NewOrganization(this); | |||
} | |||
} | |||
} | |||
} |
@@ -1,240 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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.organization; | |||
import java.util.ArrayList; | |||
import java.util.Arrays; | |||
import java.util.Date; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.function.Consumer; | |||
import javax.annotation.Nullable; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.core.util.UuidFactory; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.organization.OrganizationDto; | |||
import org.sonar.db.permission.GlobalPermission; | |||
import org.sonar.db.permission.GroupPermissionDto; | |||
import org.sonar.db.permission.template.DefaultTemplates; | |||
import org.sonar.db.permission.template.PermissionTemplateDto; | |||
import org.sonar.db.qualitygate.QualityGateDto; | |||
import org.sonar.db.qualityprofile.DefaultQProfileDto; | |||
import org.sonar.db.qualityprofile.OrgQProfileDto; | |||
import org.sonar.db.user.GroupDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.db.user.UserGroupDto; | |||
import org.sonar.server.permission.PermissionService; | |||
import org.sonar.server.qualityprofile.BuiltInQProfile; | |||
import org.sonar.server.qualityprofile.BuiltInQProfileRepository; | |||
import org.sonar.server.qualityprofile.QProfileName; | |||
import org.sonar.server.user.index.UserIndexer; | |||
import org.sonar.server.usergroups.DefaultGroupCreator; | |||
import static com.google.common.base.Preconditions.checkState; | |||
import static java.lang.String.format; | |||
import static java.util.Objects.requireNonNull; | |||
import static org.sonar.api.web.UserRole.ADMIN; | |||
import static org.sonar.api.web.UserRole.CODEVIEWER; | |||
import static org.sonar.api.web.UserRole.ISSUE_ADMIN; | |||
import static org.sonar.api.web.UserRole.SECURITYHOTSPOT_ADMIN; | |||
import static org.sonar.api.web.UserRole.USER; | |||
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; | |||
import static org.sonar.db.organization.OrganizationDto.Subscription.FREE; | |||
import static org.sonar.db.permission.GlobalPermission.SCAN; | |||
// TODO remove | |||
@Deprecated | |||
public class OrganizationUpdaterImpl implements OrganizationUpdater { | |||
private final DbClient dbClient; | |||
private final System2 system2; | |||
private final UuidFactory uuidFactory; | |||
private final OrganizationValidation organizationValidation; | |||
private final BuiltInQProfileRepository builtInQProfileRepository; | |||
private final DefaultGroupCreator defaultGroupCreator; | |||
private final UserIndexer userIndexer; | |||
private final PermissionService permissionService; | |||
public OrganizationUpdaterImpl(DbClient dbClient, System2 system2, UuidFactory uuidFactory, | |||
OrganizationValidation organizationValidation, UserIndexer userIndexer, | |||
BuiltInQProfileRepository builtInQProfileRepository, DefaultGroupCreator defaultGroupCreator, PermissionService permissionService) { | |||
this.dbClient = dbClient; | |||
this.system2 = system2; | |||
this.uuidFactory = uuidFactory; | |||
this.organizationValidation = organizationValidation; | |||
this.userIndexer = userIndexer; | |||
this.builtInQProfileRepository = builtInQProfileRepository; | |||
this.defaultGroupCreator = defaultGroupCreator; | |||
this.permissionService = permissionService; | |||
} | |||
@Override | |||
public OrganizationDto create(DbSession dbSession, UserDto userCreator, NewOrganization newOrganization, Consumer<OrganizationDto> beforeCommit) throws KeyConflictException { | |||
validate(newOrganization); | |||
String key = newOrganization.getKey(); | |||
if (organizationKeyIsUsed(dbSession, key)) { | |||
throw new KeyConflictException(format("Organization key '%s' is already used", key)); | |||
} | |||
QualityGateDto builtInQualityGate = dbClient.qualityGateDao().selectBuiltIn(dbSession); | |||
OrganizationDto organization = insertOrganization(dbSession, newOrganization, builtInQualityGate); | |||
beforeCommit.accept(organization); | |||
GroupDto ownerGroup = insertOwnersGroup(dbSession); | |||
GroupDto defaultGroup = defaultGroupCreator.create(dbSession); | |||
insertDefaultTemplateOnGroups(dbSession, organization, ownerGroup, defaultGroup); | |||
addCurrentUserToGroup(dbSession, ownerGroup, userCreator.getUuid()); | |||
addCurrentUserToGroup(dbSession, defaultGroup, userCreator.getUuid()); | |||
try (DbSession batchDbSession = dbClient.openSession(true)) { | |||
insertQualityProfiles(dbSession, batchDbSession); | |||
batchDbSession.commit(); | |||
// Elasticsearch is updated when DB session is committed | |||
userIndexer.commitAndIndex(dbSession, userCreator); | |||
return organization; | |||
} | |||
} | |||
@Override | |||
public void updateOrganizationKey(DbSession dbSession, OrganizationDto organization, String newKey) { | |||
String sanitizedKey = organizationValidation.generateKeyFrom(newKey); | |||
if (organization.getKey().equals(sanitizedKey)) { | |||
return; | |||
} | |||
checkKey(dbSession, sanitizedKey); | |||
dbClient.organizationDao().update(dbSession, organization.setKey(sanitizedKey)); | |||
} | |||
private void checkKey(DbSession dbSession, String key) { | |||
checkState(!organizationKeyIsUsed(dbSession, key), | |||
"Can't create organization with key '%s' because an organization with this key already exists", key); | |||
} | |||
private void validate(NewOrganization newOrganization) { | |||
requireNonNull(newOrganization, "newOrganization can't be null"); | |||
organizationValidation.checkName(newOrganization.getName()); | |||
organizationValidation.checkKey(newOrganization.getKey()); | |||
organizationValidation.checkDescription(newOrganization.getDescription()); | |||
organizationValidation.checkUrl(newOrganization.getUrl()); | |||
organizationValidation.checkAvatar(newOrganization.getAvatar()); | |||
} | |||
private OrganizationDto insertOrganization(DbSession dbSession, NewOrganization newOrganization, QualityGateDto builtInQualityGate, Consumer<OrganizationDto>... extendCreation) { | |||
OrganizationDto res = new OrganizationDto() | |||
.setUuid(uuidFactory.create()) | |||
.setName(newOrganization.getName()) | |||
.setKey(newOrganization.getKey()) | |||
.setDescription(newOrganization.getDescription()) | |||
.setUrl(newOrganization.getUrl()) | |||
.setDefaultQualityGateUuid(builtInQualityGate.getUuid()) | |||
.setAvatarUrl(newOrganization.getAvatar()) | |||
.setSubscription(FREE); | |||
Arrays.stream(extendCreation).forEach(c -> c.accept(res)); | |||
dbClient.organizationDao().insert(dbSession, res, false); | |||
return res; | |||
} | |||
private boolean organizationKeyIsUsed(DbSession dbSession, String key) { | |||
return dbClient.organizationDao().selectByKey(dbSession, key).isPresent(); | |||
} | |||
private void insertDefaultTemplateOnGroups(DbSession dbSession, OrganizationDto organizationDto, GroupDto ownerGroup, GroupDto defaultGroup) { | |||
Date now = new Date(system2.now()); | |||
PermissionTemplateDto permissionTemplateDto = dbClient.permissionTemplateDao().insert( | |||
dbSession, | |||
new PermissionTemplateDto() | |||
.setUuid(uuidFactory.create()) | |||
.setName(PERM_TEMPLATE_NAME) | |||
.setDescription(format(PERM_TEMPLATE_DESCRIPTION_PATTERN, organizationDto.getName())) | |||
.setCreatedAt(now) | |||
.setUpdatedAt(now)); | |||
insertGroupPermission(dbSession, permissionTemplateDto, ADMIN, ownerGroup); | |||
insertGroupPermission(dbSession, permissionTemplateDto, SCAN.getKey(), ownerGroup); | |||
insertGroupPermission(dbSession, permissionTemplateDto, USER, defaultGroup); | |||
insertGroupPermission(dbSession, permissionTemplateDto, CODEVIEWER, defaultGroup); | |||
insertGroupPermission(dbSession, permissionTemplateDto, ISSUE_ADMIN, defaultGroup); | |||
insertGroupPermission(dbSession, permissionTemplateDto, SECURITYHOTSPOT_ADMIN, defaultGroup); | |||
dbClient.organizationDao().setDefaultTemplates( | |||
dbSession, | |||
organizationDto.getUuid(), | |||
new DefaultTemplates().setProjectUuid(permissionTemplateDto.getUuid())); | |||
} | |||
private void insertGroupPermission(DbSession dbSession, PermissionTemplateDto template, String permission, @Nullable GroupDto group) { | |||
dbClient.permissionTemplateDao().insertGroupPermission(dbSession, template.getUuid(), group == null ? null : group.getUuid(), permission); | |||
} | |||
private void insertQualityProfiles(DbSession dbSession, DbSession batchDbSession) { | |||
Map<QProfileName, BuiltInQProfile> builtInsPerName = builtInQProfileRepository.get().stream() | |||
.collect(uniqueIndex(BuiltInQProfile::getQProfileName)); | |||
List<DefaultQProfileDto> defaults = new ArrayList<>(); | |||
dbClient.qualityProfileDao().selectBuiltInRuleProfiles(dbSession).forEach(rulesProfile -> { | |||
OrgQProfileDto dto = new OrgQProfileDto() | |||
.setRulesProfileUuid(rulesProfile.getUuid()) | |||
.setUuid(uuidFactory.create()); | |||
QProfileName name = new QProfileName(rulesProfile.getLanguage(), rulesProfile.getName()); | |||
BuiltInQProfile builtIn = builtInsPerName.get(name); | |||
if (builtIn == null || builtIn.isDefault()) { | |||
// If builtIn == null, the plugin has been removed | |||
// rows of table default_qprofiles must be inserted after org_qprofiles | |||
// in order to benefit from batch SQL inserts | |||
defaults.add(new DefaultQProfileDto() | |||
.setQProfileUuid(dto.getUuid()) | |||
.setLanguage(rulesProfile.getLanguage())); | |||
} | |||
dbClient.qualityProfileDao().insert(batchDbSession, dto); | |||
}); | |||
defaults.forEach(defaultQProfileDto -> dbClient.defaultQProfileDao().insertOrUpdate(dbSession, defaultQProfileDto)); | |||
} | |||
/** | |||
* Owners group has an hard coded name, a description based on the organization's name and has all global permissions. | |||
*/ | |||
private GroupDto insertOwnersGroup(DbSession dbSession) { | |||
GroupDto group = dbClient.groupDao().insert(dbSession, new GroupDto() | |||
.setUuid(uuidFactory.create()) | |||
.setName(OWNERS_GROUP_NAME) | |||
.setDescription(OWNERS_GROUP_DESCRIPTION)); | |||
permissionService.getGlobalPermissions().forEach(p -> addPermissionToGroup(dbSession, group, p)); | |||
return group; | |||
} | |||
private void addPermissionToGroup(DbSession dbSession, GroupDto group, GlobalPermission permission) { | |||
dbClient.groupPermissionDao().insert( | |||
dbSession, | |||
new GroupPermissionDto() | |||
.setUuid(uuidFactory.create()) | |||
.setGroupUuid(group.getUuid()) | |||
.setRole(permission.getKey())); | |||
} | |||
private void addCurrentUserToGroup(DbSession dbSession, GroupDto group, String createUserUuid) { | |||
dbClient.userGroupDao().insert( | |||
dbSession, | |||
new UserGroupDto().setGroupUuid(group.getUuid()).setUserUuid(createUserUuid)); | |||
} | |||
} |
@@ -1,107 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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.organization; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
public interface OrganizationValidation { | |||
int KEY_MIN_LENGTH = 1; | |||
int KEY_MAX_LENGTH = 255; | |||
int NAME_MIN_LENGTH = 1; | |||
int NAME_MAX_LENGTH = 255; | |||
int DESCRIPTION_MAX_LENGTH = 256; | |||
int URL_MAX_LENGTH = 256; | |||
/** | |||
* Ensures the specified argument is a valid key by failing with an exception if it is not so. | |||
* <p> | |||
* A valid key is non null and its length is between {@link #KEY_MIN_LENGTH} and {@link #KEY_MAX_LENGTH}. | |||
* </p> | |||
* | |||
* @return the argument | |||
* | |||
* @throws NullPointerException if argument is {@code null}. | |||
* @throws IllegalArgumentException if argument is not a valid key. | |||
*/ | |||
String checkKey(String keyCandidate); | |||
/** | |||
* Ensures the specified argument is a valid name by failing with an exception if it is not so. | |||
* <p> | |||
* A valid name is non null and its length is between {@link #NAME_MIN_LENGTH} and {@link #NAME_MAX_LENGTH}. | |||
* </p> | |||
* | |||
* @return the argument | |||
* | |||
* @throws NullPointerException if argument is {@code null}. | |||
* @throws IllegalArgumentException if argument is not a valid name. | |||
*/ | |||
String checkName(String nameCandidate); | |||
/** | |||
* Ensures the specified argument is either {@code null}, empty or a valid description by failing with an exception | |||
* if it is not so. | |||
* <p> | |||
* The length of a valid url can't be more than {@link #DESCRIPTION_MAX_LENGTH 256}. | |||
* </p> | |||
* | |||
* @return the argument | |||
* | |||
* @throws IllegalArgumentException if argument is not a valid description. | |||
*/ | |||
@CheckForNull | |||
String checkDescription(@Nullable String descriptionCandidate); | |||
/** | |||
* Ensures the specified argument is either {@code null}, empty or a valid URL by failing with an exception if it is | |||
* not so. | |||
* <p> | |||
* The length of a valid URL can't be more than {@link #URL_MAX_LENGTH 256}. | |||
* </p> | |||
* | |||
* @return the argument | |||
* | |||
* @throws IllegalArgumentException if argument is not a valid url. | |||
*/ | |||
@CheckForNull | |||
String checkUrl(@Nullable String urlCandidate); | |||
/** | |||
* Ensures the specified argument is either {@code null}, empty or a valid avatar URL by failing with an exception if | |||
* it is not so. | |||
* <p> | |||
* The length of a valid avatar URL can't be more than {@link #URL_MAX_LENGTH 256}. | |||
* </p> | |||
* | |||
* @return the argument | |||
* | |||
* @throws IllegalArgumentException if argument is not a valid avatar url. | |||
*/ | |||
@CheckForNull | |||
String checkAvatar(@Nullable String avatarCandidate); | |||
/** | |||
* Transforms the specified string into a valid key. | |||
* | |||
* @see #checkKey(String) | |||
*/ | |||
String generateKeyFrom(String source); | |||
} |
@@ -1,83 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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.organization; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import static com.google.common.base.Preconditions.checkArgument; | |||
import static java.util.Objects.requireNonNull; | |||
import static org.sonar.core.util.Slug.slugify; | |||
public class OrganizationValidationImpl implements OrganizationValidation { | |||
@Override | |||
public String checkKey(String keyCandidate) { | |||
requireNonNull(keyCandidate, "key can't be null"); | |||
checkArgument(keyCandidate.length() >= KEY_MIN_LENGTH, "Key must not be empty"); | |||
checkArgument(keyCandidate.length() <= KEY_MAX_LENGTH, "Key '%s' must be at most %s chars long", keyCandidate, KEY_MAX_LENGTH); | |||
checkArgument(slugify(keyCandidate).equals(keyCandidate), "Key '%s' contains at least one invalid char", keyCandidate); | |||
return keyCandidate; | |||
} | |||
@Override | |||
public String checkName(String nameCandidate) { | |||
requireNonNull(nameCandidate, "name can't be null"); | |||
checkArgument(nameCandidate.length() >= NAME_MIN_LENGTH, "Name must not be empty"); | |||
checkArgument(nameCandidate.length() <= NAME_MAX_LENGTH, "Name '%s' must be at most %s chars long", nameCandidate, NAME_MAX_LENGTH); | |||
return nameCandidate; | |||
} | |||
@Override | |||
public String checkDescription(@Nullable String descriptionCandidate) { | |||
checkParamMaxLength(descriptionCandidate, "Description", DESCRIPTION_MAX_LENGTH); | |||
return descriptionCandidate; | |||
} | |||
@Override | |||
public String checkUrl(@Nullable String urlCandidate) { | |||
checkParamMaxLength(urlCandidate, "Url", URL_MAX_LENGTH); | |||
return urlCandidate; | |||
} | |||
@Override | |||
public String checkAvatar(@Nullable String avatarCandidate) { | |||
checkParamMaxLength(avatarCandidate, "Avatar", URL_MAX_LENGTH); | |||
return avatarCandidate; | |||
} | |||
@CheckForNull | |||
private static void checkParamMaxLength(@Nullable String value, String label, int maxLength) { | |||
if (value != null) { | |||
checkArgument(value.length() <= maxLength, "%s '%s' must be at most %s chars long", label, value, maxLength); | |||
} | |||
} | |||
@Override | |||
public String generateKeyFrom(String source) { | |||
return slugify(source); | |||
} | |||
} |
@@ -1,336 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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.organization; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import java.util.function.Consumer; | |||
import org.apache.commons.lang.RandomStringUtils; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import org.sonar.api.impl.utils.TestSystem2; | |||
import org.sonar.api.resources.Qualifiers; | |||
import org.sonar.api.resources.ResourceTypes; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.core.util.SequenceUuidFactory; | |||
import org.sonar.core.util.UuidFactory; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.component.ResourceTypesRule; | |||
import org.sonar.db.organization.OrganizationDto; | |||
import org.sonar.db.organization.OrganizationDto.Subscription; | |||
import org.sonar.db.qualitygate.QualityGateDto; | |||
import org.sonar.db.qualityprofile.QProfileDto; | |||
import org.sonar.db.qualityprofile.RulesProfileDto; | |||
import org.sonar.db.user.GroupDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.db.user.UserMembershipDto; | |||
import org.sonar.db.user.UserMembershipQuery; | |||
import org.sonar.server.es.EsTester; | |||
import org.sonar.server.es.SearchOptions; | |||
import org.sonar.server.permission.PermissionService; | |||
import org.sonar.server.permission.PermissionServiceImpl; | |||
import org.sonar.server.qualityprofile.BuiltInQProfile; | |||
import org.sonar.server.qualityprofile.BuiltInQProfileRepositoryRule; | |||
import org.sonar.server.qualityprofile.QProfileName; | |||
import org.sonar.server.user.index.UserIndex; | |||
import org.sonar.server.user.index.UserIndexer; | |||
import org.sonar.server.user.index.UserQuery; | |||
import org.sonar.server.usergroups.DefaultGroupCreator; | |||
import org.sonar.server.usergroups.DefaultGroupCreatorImpl; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.fail; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
import static org.sonar.server.language.LanguageTesting.newLanguage; | |||
import static org.sonar.server.organization.OrganizationUpdater.NewOrganization.newOrganizationBuilder; | |||
public class OrganizationUpdaterImplTest { | |||
private static final long A_DATE = 12893434L; | |||
private final OrganizationUpdater.NewOrganization FULL_POPULATED_NEW_ORGANIZATION = newOrganizationBuilder() | |||
.setName("a-name") | |||
.setKey("a-key") | |||
.setDescription("a-description") | |||
.setUrl("a-url") | |||
.setAvatarUrl("a-avatar") | |||
.build(); | |||
private final System2 system2 = new TestSystem2().setNow(A_DATE); | |||
private static final Consumer<OrganizationDto> EMPTY_ORGANIZATION_CONSUMER = o -> { | |||
}; | |||
@Rule | |||
public DbTester db = DbTester.create(system2); | |||
@Rule | |||
public EsTester es = EsTester.create(); | |||
@Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
@Rule | |||
public BuiltInQProfileRepositoryRule builtInQProfileRepositoryRule = new BuiltInQProfileRepositoryRule(); | |||
private final DbSession dbSession = db.getSession(); | |||
private final IllegalArgumentException exceptionThrownByOrganizationValidation = new IllegalArgumentException("simulate IAE thrown by OrganizationValidation"); | |||
private final DbClient dbClient = db.getDbClient(); | |||
private final UuidFactory uuidFactory = new SequenceUuidFactory(); | |||
private final OrganizationValidation organizationValidation = mock(OrganizationValidation.class); | |||
private final UserIndexer userIndexer = new UserIndexer(dbClient, es.client()); | |||
private final UserIndex userIndex = new UserIndex(es.client(), system2); | |||
private final DefaultGroupCreator defaultGroupCreator = new DefaultGroupCreatorImpl(dbClient, uuidFactory, TestDefaultOrganizationProvider.from(db)); | |||
private final ResourceTypes resourceTypes = new ResourceTypesRule().setRootQualifiers(Qualifiers.PROJECT); | |||
private final PermissionService permissionService = new PermissionServiceImpl(resourceTypes); | |||
private final OrganizationUpdaterImpl underTest = new OrganizationUpdaterImpl(dbClient, system2, uuidFactory, organizationValidation, userIndexer, | |||
builtInQProfileRepositoryRule, defaultGroupCreator, permissionService); | |||
@Test | |||
public void create_creates_organization_with_properties_from_NewOrganization_arg() throws OrganizationUpdater.KeyConflictException { | |||
builtInQProfileRepositoryRule.initialize(); | |||
UserDto user = db.users().insertUser(); | |||
db.qualityGates().insertBuiltInQualityGate(); | |||
underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION, EMPTY_ORGANIZATION_CONSUMER); | |||
OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, FULL_POPULATED_NEW_ORGANIZATION.getKey()).get(); | |||
assertThat(organization.getUuid()).isNotEmpty(); | |||
assertThat(organization.getKey()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getKey()); | |||
assertThat(organization.getName()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getName()); | |||
assertThat(organization.getDescription()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getDescription()); | |||
assertThat(organization.getUrl()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getUrl()); | |||
assertThat(organization.getAvatarUrl()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getAvatar()); | |||
assertThat(organization.getSubscription()).isEqualTo(Subscription.FREE); | |||
assertThat(organization.getCreatedAt()).isEqualTo(A_DATE); | |||
assertThat(organization.getUpdatedAt()).isEqualTo(A_DATE); | |||
} | |||
@Test | |||
public void create_creates_members_group_and_add_current_user_to_it() throws OrganizationUpdater.KeyConflictException { | |||
UserDto user = db.users().insertUser(); | |||
builtInQProfileRepositoryRule.initialize(); | |||
db.qualityGates().insertBuiltInQualityGate(); | |||
underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION, EMPTY_ORGANIZATION_CONSUMER); | |||
verifyMembersGroup(user); | |||
} | |||
@Test | |||
public void create_does_not_require_description_url_and_avatar_to_be_non_null() throws OrganizationUpdater.KeyConflictException { | |||
builtInQProfileRepositoryRule.initialize(); | |||
UserDto user = db.users().insertUser(); | |||
db.qualityGates().insertBuiltInQualityGate(); | |||
underTest.create(dbSession, user, newOrganizationBuilder() | |||
.setKey("key") | |||
.setName("name") | |||
.build(), EMPTY_ORGANIZATION_CONSUMER); | |||
OrganizationDto organization = dbClient.organizationDao().selectByKey(dbSession, "key").get(); | |||
assertThat(organization.getKey()).isEqualTo("key"); | |||
assertThat(organization.getName()).isEqualTo("name"); | |||
assertThat(organization.getDescription()).isNull(); | |||
assertThat(organization.getUrl()).isNull(); | |||
assertThat(organization.getAvatarUrl()).isNull(); | |||
} | |||
// TODO this test should be removed when default organization entry is removed from db. For now regardless of which org we provide, the test | |||
// makes sure the user is assigned to default org | |||
@Test | |||
public void create_add_current_user_as_member_of_default_organization() throws OrganizationUpdater.KeyConflictException { | |||
UserDto user = db.users().insertUser(); | |||
builtInQProfileRepositoryRule.initialize(); | |||
db.qualityGates().insertBuiltInQualityGate(); | |||
underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION, EMPTY_ORGANIZATION_CONSUMER); | |||
assertThat( | |||
userIndex.search(UserQuery.builder().setOrganizationUuid(db.getDefaultOrganization().getUuid()).setTextQuery(user.getLogin()).build(), new SearchOptions()).getTotal()) | |||
.isEqualTo(1L); | |||
} | |||
@Test | |||
public void create_associates_to_built_in_quality_profiles() throws OrganizationUpdater.KeyConflictException { | |||
BuiltInQProfile builtIn1 = builtInQProfileRepositoryRule.add(newLanguage("foo"), "qp1", true); | |||
BuiltInQProfile builtIn2 = builtInQProfileRepositoryRule.add(newLanguage("foo"), "qp2"); | |||
builtInQProfileRepositoryRule.initialize(); | |||
insertRulesProfile(builtIn1); | |||
insertRulesProfile(builtIn2); | |||
UserDto user = db.users().insertUser(); | |||
db.qualityGates().insertBuiltInQualityGate(); | |||
underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION, EMPTY_ORGANIZATION_CONSUMER); | |||
List<QProfileDto> profiles = dbClient.qualityProfileDao().selectAll(dbSession); | |||
assertThat(profiles).extracting(p -> new QProfileName(p.getLanguage(), p.getName())).containsExactlyInAnyOrder( | |||
builtIn1.getQProfileName(), builtIn2.getQProfileName()); | |||
assertThat(dbClient.qualityProfileDao().selectDefaultProfile(dbSession, "foo").getName()) | |||
.isEqualTo("qp1"); | |||
} | |||
private void insertRulesProfile(BuiltInQProfile builtIn) { | |||
RulesProfileDto dto = new RulesProfileDto() | |||
.setIsBuiltIn(true) | |||
.setUuid(RandomStringUtils.randomAlphabetic(40)) | |||
.setLanguage(builtIn.getLanguage()) | |||
.setName(builtIn.getName()); | |||
dbClient.qualityProfileDao().insert(db.getSession(), dto); | |||
db.commit(); | |||
} | |||
@Test | |||
public void create_calls_consumer() throws OrganizationUpdater.KeyConflictException { | |||
UserDto user = db.users().insertUser(); | |||
builtInQProfileRepositoryRule.initialize(); | |||
db.qualityGates().insertBuiltInQualityGate(); | |||
Boolean[] isConsumerCalled = new Boolean[] {false}; | |||
underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION, o -> { | |||
isConsumerCalled[0] = true; | |||
}); | |||
assertThat(isConsumerCalled[0]).isEqualTo(true); | |||
} | |||
@Test | |||
public void create_throws_NPE_if_NewOrganization_arg_is_null() throws OrganizationUpdater.KeyConflictException { | |||
UserDto user = db.users().insertUser(); | |||
expectedException.expect(NullPointerException.class); | |||
expectedException.expectMessage("newOrganization can't be null"); | |||
underTest.create(dbSession, user, null, EMPTY_ORGANIZATION_CONSUMER); | |||
} | |||
@Test | |||
public void create_throws_exception_thrown_by_checkValidKey() throws OrganizationUpdater.KeyConflictException { | |||
UserDto user = db.users().insertUser(); | |||
when(organizationValidation.checkKey(FULL_POPULATED_NEW_ORGANIZATION.getKey())) | |||
.thenThrow(exceptionThrownByOrganizationValidation); | |||
createThrowsExceptionThrownByOrganizationValidation(user); | |||
} | |||
@Test | |||
public void create_throws_exception_thrown_by_checkValidDescription() throws OrganizationUpdater.KeyConflictException { | |||
UserDto user = db.users().insertUser(); | |||
when(organizationValidation.checkDescription(FULL_POPULATED_NEW_ORGANIZATION.getDescription())).thenThrow(exceptionThrownByOrganizationValidation); | |||
createThrowsExceptionThrownByOrganizationValidation(user); | |||
} | |||
@Test | |||
public void create_throws_exception_thrown_by_checkValidUrl() throws OrganizationUpdater.KeyConflictException { | |||
UserDto user = db.users().insertUser(); | |||
when(organizationValidation.checkUrl(FULL_POPULATED_NEW_ORGANIZATION.getUrl())).thenThrow(exceptionThrownByOrganizationValidation); | |||
createThrowsExceptionThrownByOrganizationValidation(user); | |||
} | |||
@Test | |||
public void create_throws_exception_thrown_by_checkValidAvatar() throws OrganizationUpdater.KeyConflictException { | |||
UserDto user = db.users().insertUser(); | |||
when(organizationValidation.checkAvatar(FULL_POPULATED_NEW_ORGANIZATION.getAvatar())).thenThrow(exceptionThrownByOrganizationValidation); | |||
createThrowsExceptionThrownByOrganizationValidation(user); | |||
} | |||
private void createThrowsExceptionThrownByOrganizationValidation(UserDto user) throws OrganizationUpdater.KeyConflictException { | |||
try { | |||
underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION, EMPTY_ORGANIZATION_CONSUMER); | |||
fail(exceptionThrownByOrganizationValidation + " should have been thrown"); | |||
} catch (IllegalArgumentException e) { | |||
assertThat(e).isSameAs(exceptionThrownByOrganizationValidation); | |||
} | |||
} | |||
@Test | |||
public void create_fails_with_KeyConflictException_if_org_with_key_in_NewOrganization_arg_already_exists_in_db() throws OrganizationUpdater.KeyConflictException { | |||
db.organizations().insertForKey(FULL_POPULATED_NEW_ORGANIZATION.getKey()); | |||
UserDto user = db.users().insertUser(); | |||
expectedException.expect(OrganizationUpdater.KeyConflictException.class); | |||
expectedException.expectMessage("Organization key '" + FULL_POPULATED_NEW_ORGANIZATION.getKey() + "' is already used"); | |||
underTest.create(dbSession, user, FULL_POPULATED_NEW_ORGANIZATION, EMPTY_ORGANIZATION_CONSUMER); | |||
} | |||
@Test | |||
public void update_personal_organization() { | |||
OrganizationDto organization = db.organizations().insert(o -> o.setKey("old login")); | |||
when(organizationValidation.generateKeyFrom("new_login")).thenReturn("new_login"); | |||
underTest.updateOrganizationKey(dbSession, organization, "new_login"); | |||
OrganizationDto organizationReloaded = dbClient.organizationDao().selectByUuid(dbSession, organization.getUuid()).get(); | |||
assertThat(organizationReloaded.getKey()).isEqualTo("new_login"); | |||
} | |||
@Test | |||
public void does_not_update_personal_organization_when_generated_organization_key_does_not_change() { | |||
OrganizationDto organization = db.organizations().insert(o -> o.setKey("login")); | |||
when(organizationValidation.generateKeyFrom("Login")).thenReturn("login"); | |||
underTest.updateOrganizationKey(dbSession, organization, "Login"); | |||
OrganizationDto organizationReloaded = dbClient.organizationDao().selectByUuid(dbSession, organization.getUuid()).get(); | |||
assertThat(organizationReloaded.getKey()).isEqualTo("login"); | |||
} | |||
@Test | |||
public void fail_to_update_personal_organization_when_new_key_already_exist() { | |||
OrganizationDto organization = db.organizations().insert(); | |||
db.organizations().insert(o -> o.setKey("new_login")); | |||
when(organizationValidation.generateKeyFrom("new_login")).thenReturn("new_login"); | |||
expectedException.expect(IllegalStateException.class); | |||
expectedException.expectMessage("Can't create organization with key 'new_login' because an organization with this key already exists"); | |||
underTest.updateOrganizationKey(dbSession, organization, "new_login"); | |||
} | |||
private void verifyMembersGroup(UserDto user) { | |||
Optional<GroupDto> groupOpt = dbClient.groupDao().selectByName(dbSession, "Members"); | |||
assertThat(groupOpt).isPresent(); | |||
GroupDto groupDto = groupOpt.get(); | |||
assertThat(groupDto.getDescription()).isEqualTo("All members of the organization"); | |||
assertThat(dbClient.groupPermissionDao().selectGlobalPermissionsOfGroup(dbSession, groupDto.getUuid())).isEmpty(); | |||
List<UserMembershipDto> members = dbClient.groupMembershipDao().selectMembers( | |||
dbSession, | |||
UserMembershipQuery.builder() | |||
.groupUuid(groupDto.getUuid()) | |||
.membership(UserMembershipQuery.IN).build(), | |||
0, Integer.MAX_VALUE); | |||
assertThat(members) | |||
.extracting(UserMembershipDto::getLogin) | |||
.containsOnly(user.getLogin()); | |||
} | |||
} |
@@ -1,240 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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.organization; | |||
import com.google.common.base.Strings; | |||
import java.util.Random; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.junit.Assert.fail; | |||
public class OrganizationValidationImplTest { | |||
private static final String STRING_32_CHARS = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; | |||
private static final String STRING_64_CHARS = STRING_32_CHARS + STRING_32_CHARS; | |||
private static final String STRING_256_CHARS = STRING_64_CHARS + STRING_64_CHARS + STRING_64_CHARS + STRING_64_CHARS; | |||
private static final String STRING_255_CHARS = Strings.repeat("a", 255); | |||
@Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
private OrganizationValidationImpl underTest = new OrganizationValidationImpl(); | |||
@Test | |||
public void checkValidKey_throws_NPE_if_arg_is_null() { | |||
expectedException.expect(NullPointerException.class); | |||
expectedException.expectMessage("key can't be null"); | |||
underTest.checkKey(null); | |||
} | |||
@Test | |||
public void checkValidKey_throws_IAE_if_arg_is_empty() { | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage("Key must not be empty"); | |||
underTest.checkKey(""); | |||
} | |||
@Test | |||
public void checkValidKey_throws_IAE_if_key_is_empty() { | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage("Key must not be empty"); | |||
underTest.checkKey(""); | |||
} | |||
@Test | |||
public void checkValidKey_does_not_fail_if_arg_is_1_to_255_chars_long() { | |||
String str = "a"; | |||
for (int i = 0; i < 254; i++) { | |||
underTest.checkKey(str); | |||
str += "a"; | |||
} | |||
} | |||
@Test | |||
public void checkValidKey_throws_IAE_when_more_than_300_characters() { | |||
String key = STRING_255_CHARS + "b"; | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage("Key '" + key + "' must be at most 255 chars long"); | |||
underTest.checkKey(key); | |||
} | |||
@Test | |||
public void checkValidKey_throws_IAE_if_arg_contains_invalid_chars() { | |||
char[] invalidChars = {'é', '<', '@'}; | |||
for (char invalidChar : invalidChars) { | |||
String str = "aa" + invalidChar; | |||
try { | |||
underTest.checkKey(str); | |||
fail("A IllegalArgumentException should have been thrown"); | |||
} catch (IllegalArgumentException e) { | |||
assertThat(e).hasMessage("Key '" + str + "' contains at least one invalid char"); | |||
} | |||
} | |||
} | |||
@Test | |||
public void checkValidName_throws_NPE_if_arg_is_null() { | |||
expectedException.expect(NullPointerException.class); | |||
expectedException.expectMessage("name can't be null"); | |||
underTest.checkName(null); | |||
} | |||
@Test | |||
public void checkValidName_throws_IAE_if_empty() { | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage("Name must not be empty"); | |||
underTest.checkName(""); | |||
} | |||
@Test | |||
public void checkValidName_does_not_fail_if_arg_is_1_to_255_chars_long() { | |||
String str = "a"; | |||
for (int i = 0; i < 254; i++) { | |||
underTest.checkName(str); | |||
str += "a"; | |||
} | |||
} | |||
@Test | |||
public void checkValidName_throws_IAE_when_more_than_255_characters() { | |||
String str = STRING_255_CHARS + "b"; | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage("Name '" + str + "' must be at most 255 chars long"); | |||
underTest.checkName(str); | |||
} | |||
@Test | |||
public void checkValidDescription_does_not_fail_if_arg_is_null() { | |||
underTest.checkDescription(null); | |||
} | |||
@Test | |||
public void checkValidDescription_does_not_fail_if_arg_is_empty() { | |||
underTest.checkDescription(""); | |||
} | |||
@Test | |||
public void checkValidDescription_does_not_fail_if_arg_is_1_to_256_chars_long() { | |||
String str = "1"; | |||
for (int i = 0; i < 256; i++) { | |||
underTest.checkDescription(str); | |||
str += "a"; | |||
} | |||
} | |||
@Test | |||
public void checkValidDescription_throws_IAE_if_arg_is_more_than_256_chars_long() { | |||
String str = STRING_256_CHARS; | |||
underTest.checkDescription(str); | |||
for (int i = 0; i < 5 + Math.abs(new Random().nextInt(10)); i++) { | |||
str += "c"; | |||
try { | |||
underTest.checkDescription(str); | |||
fail("A IllegalArgumentException should have been thrown"); | |||
} catch (IllegalArgumentException e) { | |||
assertThat(e).hasMessage("Description '" + str + "' must be at most 256 chars long"); | |||
} | |||
} | |||
} | |||
@Test | |||
public void checkValidUrl_does_not_fail_if_arg_is_null() { | |||
underTest.checkUrl(null); | |||
} | |||
@Test | |||
public void checkValidUrl_does_not_fail_if_arg_is_1_to_256_chars_long() { | |||
String str = "1"; | |||
for (int i = 0; i < 256; i++) { | |||
underTest.checkUrl(str); | |||
str += "a"; | |||
} | |||
} | |||
@Test | |||
public void checkValidUrl_throws_IAE_if_arg_is_more_than_256_chars_long() { | |||
String str = STRING_256_CHARS; | |||
underTest.checkUrl(str); | |||
for (int i = 0; i < 5 + Math.abs(new Random().nextInt(10)); i++) { | |||
str += "c"; | |||
try { | |||
underTest.checkUrl(str); | |||
fail("A IllegalArgumentException should have been thrown"); | |||
} catch (IllegalArgumentException e) { | |||
assertThat(e).hasMessage("Url '" + str + "' must be at most 256 chars long"); | |||
} | |||
} | |||
} | |||
@Test | |||
public void checkValidAvatar_does_not_fail_if_arg_is_null() { | |||
underTest.checkAvatar(null); | |||
} | |||
@Test | |||
public void checkValidAvatar_does_not_fail_if_arg_is_1_to_256_chars_long() { | |||
String str = "1"; | |||
for (int i = 0; i < 256; i++) { | |||
underTest.checkAvatar(str); | |||
str += "a"; | |||
} | |||
} | |||
@Test | |||
public void checkValidAvatar_throws_IAE_if_arg_is_more_than_256_chars_long() { | |||
String str = STRING_256_CHARS; | |||
underTest.checkAvatar(str); | |||
for (int i = 0; i < 5 + Math.abs(new Random().nextInt(10)); i++) { | |||
str += "c"; | |||
try { | |||
underTest.checkAvatar(str); | |||
fail("A IllegalArgumentException should have been thrown"); | |||
} catch (IllegalArgumentException e) { | |||
assertThat(e).hasMessage("Avatar '" + str + "' must be at most 256 chars long"); | |||
} | |||
} | |||
} | |||
@Test | |||
public void generateKeyFrom_returns_slug_of_arg() { | |||
assertThat(underTest.generateKeyFrom("foo")).isEqualTo("foo"); | |||
assertThat(underTest.generateKeyFrom(" FOO ")).isEqualTo("foo"); | |||
assertThat(underTest.generateKeyFrom("he's here")).isEqualTo("he-s-here"); | |||
assertThat(underTest.generateKeyFrom("foo-bar")).isEqualTo("foo-bar"); | |||
assertThat(underTest.generateKeyFrom("foo_bar")).isEqualTo("foo_bar"); | |||
assertThat(underTest.generateKeyFrom("accents éà")).isEqualTo("accents-ea"); | |||
assertThat(underTest.generateKeyFrom("<foo>")).isEqualTo("foo"); | |||
assertThat(underTest.generateKeyFrom("<\"foo:\">")).isEqualTo("foo"); | |||
} | |||
} |
@@ -32,7 +32,6 @@ public class OrganizationsWsModule extends Module { | |||
protected void configureModule() { | |||
add( | |||
OrganizationsWs.class, | |||
OrganizationsWsSupport.class, | |||
MemberUpdater.class, | |||
// actions | |||
SearchAction.class, |
@@ -1,131 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 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.organization.ws; | |||
import javax.annotation.CheckForNull; | |||
import org.sonar.api.server.ws.Request; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.organization.OrganizationDto; | |||
import org.sonar.server.organization.OrganizationValidation; | |||
import org.sonarqube.ws.Organizations.Organization; | |||
import static com.google.common.base.Preconditions.checkArgument; | |||
import static java.util.Optional.ofNullable; | |||
import static org.sonar.server.organization.OrganizationValidation.DESCRIPTION_MAX_LENGTH; | |||
import static org.sonar.server.organization.OrganizationValidation.NAME_MAX_LENGTH; | |||
import static org.sonar.server.organization.OrganizationValidation.NAME_MIN_LENGTH; | |||
import static org.sonar.server.organization.OrganizationValidation.URL_MAX_LENGTH; | |||
/** | |||
* Factorizes code and constants between Organization WS's actions. | |||
*/ | |||
public class OrganizationsWsSupport { | |||
static final String PARAM_ORGANIZATION = "organization"; | |||
static final String PARAM_KEY = "key"; | |||
static final String PARAM_NAME = "name"; | |||
static final String PARAM_DESCRIPTION = "description"; | |||
static final String PARAM_URL = "url"; | |||
static final String PARAM_AVATAR_URL = "avatar"; | |||
static final String PARAM_LOGIN = "login"; | |||
private final OrganizationValidation organizationValidation; | |||
private final DbClient dbClient; | |||
public OrganizationsWsSupport(OrganizationValidation organizationValidation, DbClient dbClient) { | |||
this.organizationValidation = organizationValidation; | |||
this.dbClient = dbClient; | |||
} | |||
String getAndCheckMandatoryName(Request request) { | |||
String name = request.mandatoryParam(PARAM_NAME); | |||
organizationValidation.checkName(name); | |||
return name; | |||
} | |||
@CheckForNull | |||
String getAndCheckName(Request request) { | |||
String name = request.param(PARAM_NAME); | |||
if (name != null) { | |||
organizationValidation.checkName(name); | |||
} | |||
return name; | |||
} | |||
@CheckForNull | |||
String getAndCheckAvatar(Request request) { | |||
return organizationValidation.checkAvatar(request.param(PARAM_AVATAR_URL)); | |||
} | |||
@CheckForNull | |||
String getAndCheckUrl(Request request) { | |||
return organizationValidation.checkUrl(request.param(PARAM_URL)); | |||
} | |||
@CheckForNull | |||
String getAndCheckDescription(Request request) { | |||
return organizationValidation.checkDescription(request.param(PARAM_DESCRIPTION)); | |||
} | |||
void addOrganizationDetailsParams(WebService.NewAction action, boolean isNameRequired) { | |||
action.createParam(PARAM_NAME) | |||
.setRequired(isNameRequired) | |||
.setMinimumLength(NAME_MIN_LENGTH) | |||
.setMaximumLength(NAME_MAX_LENGTH) | |||
.setDescription("Name of the organization") | |||
.setExampleValue("Foo Company"); | |||
action.createParam(PARAM_DESCRIPTION) | |||
.setRequired(false) | |||
.setMaximumLength(DESCRIPTION_MAX_LENGTH) | |||
.setDescription("Description of the organization.<br/> It must be less than 256 chars long.") | |||
.setExampleValue("The Foo company produces quality software for Bar."); | |||
action.createParam(PARAM_URL) | |||
.setRequired(false) | |||
.setMaximumLength(URL_MAX_LENGTH) | |||
.setDescription("URL of the organization.<br/> It must be less than 256 chars long.") | |||
.setExampleValue("https://www.foo.com"); | |||
action.createParam(PARAM_AVATAR_URL) | |||
.setRequired(false) | |||
.setMaximumLength(URL_MAX_LENGTH) | |||
.setDescription("URL of the organization avatar.<br/> It must be less than 256 chars long.") | |||
.setExampleValue("https://www.foo.com/foo.png"); | |||
} | |||
Organization.Builder toOrganization(OrganizationDto dto) { | |||
Organization.Builder builder = Organization.newBuilder(); | |||
builder | |||
.setName(dto.getName()) | |||
.setKey(dto.getKey()); | |||
ofNullable(dto.getDescription()).ifPresent(builder::setDescription); | |||
ofNullable(dto.getUrl()).ifPresent(builder::setUrl); | |||
ofNullable(dto.getAvatarUrl()).ifPresent(builder::setAvatar); | |||
return builder; | |||
} | |||
void checkMemberSyncIsDisabled(DbSession dbSession, OrganizationDto organization) { | |||
dbClient.organizationAlmBindingDao().selectByOrganization(dbSession, organization).ifPresent(orgAlmBindingDto -> | |||
checkArgument(!orgAlmBindingDto.isMembersSyncEnable(), "You can't add or remove members when synchronization of organization with alm is enabled.")); | |||
} | |||
} |
@@ -54,10 +54,10 @@ import static org.sonar.api.server.ws.WebService.SelectionMode.SELECTED; | |||
import static org.sonar.db.permission.GlobalPermission.ADMINISTER; | |||
import static org.sonar.server.es.SearchOptions.MAX_PAGE_SIZE; | |||
import static org.sonar.server.exceptions.NotFoundException.checkFoundWithOptional; | |||
import static org.sonar.server.organization.ws.OrganizationsWsSupport.PARAM_ORGANIZATION; | |||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | |||
public class SearchMembersAction implements OrganizationsWsAction { | |||
private static final String ORGANIZATION_PARAM = "organization"; | |||
private final DbClient dbClient; | |||
private final UserIndex userIndex; | |||
@@ -94,7 +94,7 @@ public class SearchMembersAction implements OrganizationsWsAction { | |||
.setDefaultValue(SELECTED.value()) | |||
.setPossibleValues(SELECTED.value(), SelectionMode.DESELECTED.value()); | |||
action.createParam(PARAM_ORGANIZATION) | |||
action.createParam(ORGANIZATION_PARAM) | |||
.setDescription("Organization key") | |||
.setInternal(true) | |||
.setRequired(false); |
@@ -111,8 +111,6 @@ import org.sonar.server.newcodeperiod.ws.NewCodePeriodsWsModule; | |||
import org.sonar.server.notification.NotificationModule; | |||
import org.sonar.server.notification.ws.NotificationWsModule; | |||
import org.sonar.server.organization.BillingValidationsProxyImpl; | |||
import org.sonar.server.organization.OrganizationUpdaterImpl; | |||
import org.sonar.server.organization.OrganizationValidationImpl; | |||
import org.sonar.server.organization.ws.OrganizationsWsModule; | |||
import org.sonar.server.permission.DefaultTemplatesResolverImpl; | |||
import org.sonar.server.permission.GroupPermissionChanger; | |||
@@ -268,8 +266,6 @@ public class PlatformLevel4 extends PlatformLevel { | |||
UpdateCenterWsModule.class, | |||
// organizations | |||
OrganizationValidationImpl.class, | |||
OrganizationUpdaterImpl.class, | |||
OrganizationsWsModule.class, | |||
BillingValidationsProxyImpl.class, | |||