diff options
author | Julien HENRY <julien.henry@sonarsource.com> | 2020-03-19 12:41:40 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2020-03-23 20:03:41 +0000 |
commit | a5e56c8d403ba0bfdb36e94acb8def5ceb065524 (patch) | |
tree | 184bce441b640428b2bc3f1bb6e176e9f8a2d6cd /sonar-plugin-api-impl/src | |
parent | 0c8d18b4ed3e08536eef559153c62fb80cffa253 (diff) | |
download | sonarqube-a5e56c8d403ba0bfdb36e94acb8def5ceb065524.tar.gz sonarqube-a5e56c8d403ba0bfdb36e94acb8def5ceb065524.zip |
SONAR-13214 Remove org.sonar.api.config.Settings from the API
Diffstat (limited to 'sonar-plugin-api-impl/src')
16 files changed, 1011 insertions, 18 deletions
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java index 7236908e426..14806bc4e16 100644 --- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java @@ -77,7 +77,6 @@ import org.sonar.api.batch.sensor.rule.internal.DefaultAdHocRule; import org.sonar.api.batch.sensor.symbol.NewSymbolTable; import org.sonar.api.batch.sensor.symbol.internal.DefaultSymbolTable; import org.sonar.api.config.Configuration; -import org.sonar.api.config.Settings; import org.sonar.api.config.internal.ConfigurationBridge; import org.sonar.api.config.internal.MapSettings; import org.sonar.api.internal.MetadataLoader; @@ -105,7 +104,7 @@ import static java.util.Collections.unmodifiableMap; */ public class SensorContextTester implements SensorContext { - private Settings settings; + private MapSettings settings; private DefaultFileSystem fs; private ActiveRules activeRules; private InMemorySensorStorage sensorStorage; @@ -132,8 +131,7 @@ public class SensorContextTester implements SensorContext { return new SensorContextTester(moduleBaseDir); } - @Override - public Settings settings() { + public MapSettings settings() { return settings; } @@ -142,7 +140,7 @@ public class SensorContextTester implements SensorContext { return new ConfigurationBridge(settings); } - public SensorContextTester setSettings(Settings settings) { + public SensorContextTester setSettings(MapSettings settings) { this.settings = settings; return this; } diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/AesCipher.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/AesCipher.java new file mode 100644 index 00000000000..e6b14f3d9db --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/AesCipher.java @@ -0,0 +1,134 @@ +/* + * 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.api.config.internal; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.security.SecureRandom; +import javax.annotation.Nullable; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.CoreProperties; + +import static java.nio.charset.StandardCharsets.UTF_8; + +final class AesCipher implements Cipher { + + // Can't be increased because of Java 6 policy files : + // https://confluence.terena.org/display/~visser/No+256+bit+ciphers+for+Java+apps + // http://java.sun.com/javase/6/webnotes/install/jre/README + static final int KEY_SIZE_IN_BITS = 128; + + private static final String CRYPTO_KEY = "AES"; + + private String pathToSecretKey; + + AesCipher(@Nullable String pathToSecretKey) { + this.pathToSecretKey = pathToSecretKey; + } + + @Override + public String encrypt(String clearText) { + try { + javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CRYPTO_KEY); + cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, loadSecretFile()); + return Base64.encodeBase64String(cipher.doFinal(clearText.getBytes(StandardCharsets.UTF_8.name()))); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + @Override + public String decrypt(String encryptedText) { + try { + javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CRYPTO_KEY); + cipher.init(javax.crypto.Cipher.DECRYPT_MODE, loadSecretFile()); + byte[] cipherData = cipher.doFinal(Base64.decodeBase64(StringUtils.trim(encryptedText))); + return new String(cipherData, StandardCharsets.UTF_8); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + /** + * This method checks the existence of the file, but not the validity of the contained key. + */ + boolean hasSecretKey() { + String path = getPathToSecretKey(); + if (StringUtils.isNotBlank(path)) { + File file = new File(path); + return file.exists() && file.isFile(); + } + return false; + } + + private Key loadSecretFile() throws IOException { + String path = getPathToSecretKey(); + return loadSecretFileFromFile(path); + } + + Key loadSecretFileFromFile(@Nullable String path) throws IOException { + if (StringUtils.isBlank(path)) { + throw new IllegalStateException("Secret key not found. Please set the property " + CoreProperties.ENCRYPTION_SECRET_KEY_PATH); + } + File file = new File(path); + if (!file.exists() || !file.isFile()) { + throw new IllegalStateException("The property " + CoreProperties.ENCRYPTION_SECRET_KEY_PATH + " does not link to a valid file: " + path); + } + String s = FileUtils.readFileToString(file, UTF_8); + if (StringUtils.isBlank(s)) { + throw new IllegalStateException("No secret key in the file: " + path); + } + return new SecretKeySpec(Base64.decodeBase64(StringUtils.trim(s)), CRYPTO_KEY); + } + + String generateRandomSecretKey() { + try { + KeyGenerator keyGen = KeyGenerator.getInstance(CRYPTO_KEY); + keyGen.init(KEY_SIZE_IN_BITS, new SecureRandom()); + SecretKey secretKey = keyGen.generateKey(); + return Base64.encodeBase64String(secretKey.getEncoded()); + + } catch (Exception e) { + throw new IllegalStateException("Fail to generate secret key", e); + } + } + + String getPathToSecretKey() { + if (StringUtils.isBlank(pathToSecretKey)) { + pathToSecretKey = new File(FileUtils.getUserDirectoryPath(), ".sonar/sonar-secret.txt").getPath(); + } + return pathToSecretKey; + } + + public void setPathToSecretKey(@Nullable String pathToSecretKey) { + this.pathToSecretKey = pathToSecretKey; + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/Base64Cipher.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/Base64Cipher.java new file mode 100644 index 00000000000..4829c075764 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/Base64Cipher.java @@ -0,0 +1,36 @@ +/* + * 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.api.config.internal; + +import org.apache.commons.codec.binary.Base64; + +import java.nio.charset.StandardCharsets; + +final class Base64Cipher implements Cipher { + @Override + public String encrypt(String clearText) { + return Base64.encodeBase64String(clearText.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String decrypt(String encryptedText) { + return new String(Base64.decodeBase64(encryptedText), StandardCharsets.UTF_8); + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/Cipher.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/Cipher.java new file mode 100644 index 00000000000..556bf94c976 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/Cipher.java @@ -0,0 +1,25 @@ +/* + * 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.api.config.internal; + +interface Cipher { + String encrypt(String clearText); + String decrypt(String encryptedText); +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/ConfigurationBridge.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/ConfigurationBridge.java index f6a24adfc88..a89ef0af3b1 100644 --- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/ConfigurationBridge.java +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/ConfigurationBridge.java @@ -20,7 +20,6 @@ package org.sonar.api.config.internal; import java.util.Optional; -import org.sonar.api.config.Settings; import org.sonar.api.config.Configuration; /** diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/Encryption.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/Encryption.java new file mode 100644 index 00000000000..8a4896c4480 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/Encryption.java @@ -0,0 +1,94 @@ +/* + * 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.api.config.internal; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.annotation.Nullable; + +/** + * @since 3.0 + */ +public final class Encryption { + + private static final String BASE64_ALGORITHM = "b64"; + + private static final String AES_ALGORITHM = "aes"; + private final AesCipher aesCipher; + + private final Map<String, Cipher> ciphers; + private static final Pattern ENCRYPTED_PATTERN = Pattern.compile("\\{(.*?)\\}(.*)"); + + public Encryption(@Nullable String pathToSecretKey) { + aesCipher = new AesCipher(pathToSecretKey); + ciphers = new HashMap<>(); + ciphers.put(BASE64_ALGORITHM, new Base64Cipher()); + ciphers.put(AES_ALGORITHM, aesCipher); + } + + public void setPathToSecretKey(@Nullable String pathToSecretKey) { + aesCipher.setPathToSecretKey(pathToSecretKey); + } + + /** + * Checks the availability of the secret key, that is required to encrypt and decrypt. + */ + public boolean hasSecretKey() { + return aesCipher.hasSecretKey(); + } + + public boolean isEncrypted(String value) { + return value.indexOf('{') == 0 && value.indexOf('}') > 1; + } + + public String encrypt(String clearText) { + return encrypt(AES_ALGORITHM, clearText); + } + + public String scramble(String clearText) { + return encrypt(BASE64_ALGORITHM, clearText); + } + + public String generateRandomSecretKey() { + return aesCipher.generateRandomSecretKey(); + } + + public String decrypt(String encryptedText) { + Matcher matcher = ENCRYPTED_PATTERN.matcher(encryptedText); + if (matcher.matches()) { + Cipher cipher = ciphers.get(matcher.group(1).toLowerCase(Locale.ENGLISH)); + if (cipher != null) { + return cipher.decrypt(matcher.group(2)); + } + } + return encryptedText; + } + + private String encrypt(String algorithm, String clearText) { + Cipher cipher = ciphers.get(algorithm); + if (cipher == null) { + throw new IllegalArgumentException("Unknown cipher algorithm: " + algorithm); + } + return String.format("{%s}%s", algorithm, cipher.encrypt(clearText)); + } +} diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/MapSettings.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/MapSettings.java index b53cdb0d0d1..764eef2812d 100644 --- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/MapSettings.java +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/MapSettings.java @@ -23,9 +23,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; import org.sonar.api.config.Configuration; -import org.sonar.api.config.Encryption; import org.sonar.api.config.PropertyDefinitions; -import org.sonar.api.config.Settings; import static java.util.Collections.unmodifiableMap; import static java.util.Objects.requireNonNull; diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/Settings.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/Settings.java new file mode 100644 index 00000000000..a934a807cb5 --- /dev/null +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/Settings.java @@ -0,0 +1,460 @@ +/* + * 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.api.config.internal; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.stream.Collectors; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.ce.ComputeEngineSide; +import org.sonar.api.config.Configuration; +import org.sonar.api.config.PropertyDefinition; +import org.sonar.api.config.PropertyDefinitions; +import org.sonar.api.scanner.ScannerSide; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.DateUtils; +import org.sonarsource.api.sonarlint.SonarLintSide; + +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang.StringUtils.trim; + +/** + * @deprecated since 6.5 use {@link Configuration} + */ +@ServerSide +@ComputeEngineSide +@ScannerSide +@SonarLintSide +@Deprecated +public abstract class Settings { + + private final PropertyDefinitions definitions; + private final Encryption encryption; + + protected Settings(PropertyDefinitions definitions, Encryption encryption) { + this.definitions = requireNonNull(definitions); + this.encryption = requireNonNull(encryption); + } + + protected abstract Optional<String> get(String key); + + /** + * Add the settings with the specified key and value, both are trimmed and neither can be null. + * + * @throws NullPointerException if {@code key} and/or {@code value} is {@code null}. + */ + protected abstract void set(String key, String value); + + protected abstract void remove(String key); + + /** + * Immutable map of the properties that have non-default values. + * The default values defined by {@link PropertyDefinitions} are ignored, + * so the returned values are not the effective values. Basically only + * the non-empty results of {@link #getRawString(String)} are returned. + * <p> + * Values are not decrypted if they are encrypted with a secret key. + * </p> + */ + public abstract Map<String, String> getProperties(); + + public Encryption getEncryption() { + return encryption; + } + + /** + * The value that overrides the default value. It + * may be encrypted with a secret key. Use {@link #getString(String)} to get + * the effective and decrypted value. + * + * @since 6.1 + */ + public Optional<String> getRawString(String key) { + return get(definitions.validKey(requireNonNull(key))); + } + + /** + * All the property definitions declared by core and plugins. + */ + public PropertyDefinitions getDefinitions() { + return definitions; + } + + /** + * The definition related to the specified property. It may + * be empty. + * + * @since 6.1 + */ + public Optional<PropertyDefinition> getDefinition(String key) { + return Optional.ofNullable(definitions.get(key)); + } + + /** + * @return {@code true} if the property has a non-default value, else {@code false}. + */ + public boolean hasKey(String key) { + return getRawString(key).isPresent(); + } + + @CheckForNull + public String getDefaultValue(String key) { + return definitions.getDefaultValue(key); + } + + public boolean hasDefaultValue(String key) { + return StringUtils.isNotEmpty(getDefaultValue(key)); + } + + /** + * The effective value of the specified property. Can return + * {@code null} if the property is not set and has no + * defined default value. + * <p> + * If the property is encrypted with a secret key, + * then the returned value is decrypted. + * </p> + * + * @throws IllegalStateException if value is encrypted but fails to be decrypted. + */ + @CheckForNull + public String getString(String key) { + String effectiveKey = definitions.validKey(key); + Optional<String> value = getRawString(effectiveKey); + if (!value.isPresent()) { + // default values cannot be encrypted, so return value as-is. + return getDefaultValue(effectiveKey); + } + if (encryption.isEncrypted(value.get())) { + try { + return encryption.decrypt(value.get()); + } catch (Exception e) { + throw new IllegalStateException("Fail to decrypt the property " + effectiveKey + ". Please check your secret key.", e); + } + } + return value.get(); + } + + /** + * Effective value as boolean. It is {@code false} if {@link #getString(String)} + * does not return {@code "true"}, even if it's not a boolean representation. + * + * @return {@code true} if the effective value is {@code "true"}, else {@code false}. + */ + public boolean getBoolean(String key) { + String value = getString(key); + return StringUtils.isNotEmpty(value) && Boolean.parseBoolean(value); + } + + /** + * Effective value as {@code int}. + * + * @return the value as {@code int}. If the property does not have value nor default value, then {@code 0} is returned. + * @throws NumberFormatException if value is not empty and is not a parsable integer + */ + public int getInt(String key) { + String value = getString(key); + if (StringUtils.isNotEmpty(value)) { + return Integer.parseInt(value); + } + return 0; + } + + /** + * Effective value as {@code long}. + * + * @return the value as {@code long}. If the property does not have value nor default value, then {@code 0L} is returned. + * @throws NumberFormatException if value is not empty and is not a parsable {@code long} + */ + public long getLong(String key) { + String value = getString(key); + if (StringUtils.isNotEmpty(value)) { + return Long.parseLong(value); + } + return 0L; + } + + /** + * Effective value as {@link Date}, without time fields. Format is {@link DateUtils#DATE_FORMAT}. + * + * @return the value as a {@link Date}. If the property does not have value nor default value, then {@code null} is returned. + * @throws RuntimeException if value is not empty and is not in accordance with {@link DateUtils#DATE_FORMAT}. + */ + @CheckForNull + public Date getDate(String key) { + String value = getString(key); + if (StringUtils.isNotEmpty(value)) { + return DateUtils.parseDate(value); + } + return null; + } + + /** + * Effective value as {@link Date}, with time fields. Format is {@link DateUtils#DATETIME_FORMAT}. + * + * @return the value as a {@link Date}. If the property does not have value nor default value, then {@code null} is returned. + * @throws RuntimeException if value is not empty and is not in accordance with {@link DateUtils#DATETIME_FORMAT}. + */ + @CheckForNull + public Date getDateTime(String key) { + String value = getString(key); + if (StringUtils.isNotEmpty(value)) { + return DateUtils.parseDateTime(value); + } + return null; + } + + /** + * Effective value as {@code Float}. + * + * @return the value as {@code Float}. If the property does not have value nor default value, then {@code null} is returned. + * @throws NumberFormatException if value is not empty and is not a parsable number + */ + @CheckForNull + public Float getFloat(String key) { + String value = getString(key); + if (StringUtils.isNotEmpty(value)) { + try { + return Float.valueOf(value); + } catch (NumberFormatException e) { + throw new IllegalStateException(String.format("The property '%s' is not a float value", key)); + } + } + return null; + } + + /** + * Effective value as {@code Double}. + * + * @return the value as {@code Double}. If the property does not have value nor default value, then {@code null} is returned. + * @throws NumberFormatException if value is not empty and is not a parsable number + */ + @CheckForNull + public Double getDouble(String key) { + String value = getString(key); + if (StringUtils.isNotEmpty(value)) { + try { + return Double.valueOf(value); + } catch (NumberFormatException e) { + throw new IllegalStateException(String.format("The property '%s' is not a double value", key)); + } + } + return null; + } + + /** + * Value is split by comma and trimmed. Never returns null. + * <br> + * Examples : + * <ul> + * <li>"one,two,three " -> ["one", "two", "three"]</li> + * <li>" one, two, three " -> ["one", "two", "three"]</li> + * <li>"one, , three" -> ["one", "", "three"]</li> + * </ul> + */ + public String[] getStringArray(String key) { + String effectiveKey = definitions.validKey(key); + Optional<PropertyDefinition> def = getDefinition(effectiveKey); + if ((def.isPresent()) && (def.get().multiValues())) { + String value = getString(key); + if (value == null) { + return ArrayUtils.EMPTY_STRING_ARRAY; + } + + return Arrays.stream(value.split(",", -1)).map(String::trim) + .map(s -> s.replace("%2C", ",")) + .toArray(String[]::new); + } + + return getStringArrayBySeparator(key, ","); + } + + /** + * Value is split by carriage returns. + * + * @return non-null array of lines. The line termination characters are excluded. + * @since 3.2 + */ + public String[] getStringLines(String key) { + String value = getString(key); + if (StringUtils.isEmpty(value)) { + return new String[0]; + } + return value.split("\r?\n|\r", -1); + } + + /** + * Value is split and trimmed. + */ + public String[] getStringArrayBySeparator(String key, String separator) { + String value = getString(key); + if (value != null) { + String[] strings = StringUtils.splitByWholeSeparator(value, separator); + String[] result = new String[strings.length]; + for (int index = 0; index < strings.length; index++) { + result[index] = trim(strings[index]); + } + return result; + } + return ArrayUtils.EMPTY_STRING_ARRAY; + } + + public Settings appendProperty(String key, @Nullable String value) { + Optional<String> existingValue = getRawString(definitions.validKey(key)); + String newValue; + if (!existingValue.isPresent()) { + newValue = trim(value); + } else { + newValue = existingValue.get() + "," + trim(value); + } + return setProperty(key, newValue); + } + + public Settings setProperty(String key, @Nullable String[] values) { + requireNonNull(key, "key can't be null"); + String effectiveKey = key.trim(); + Optional<PropertyDefinition> def = getDefinition(effectiveKey); + if (!def.isPresent() || (!def.get().multiValues())) { + throw new IllegalStateException("Fail to set multiple values on a single value property " + key); + } + + String text = null; + if (values != null) { + List<String> escaped = new ArrayList<>(); + for (String value : values) { + if (null != value) { + escaped.add(value.replace(",", "%2C")); + } else { + escaped.add(""); + } + } + + String escapedValue = escaped.stream().collect(Collectors.joining(",")); + text = trim(escapedValue); + } + return setProperty(key, text); + } + + /** + * Change a property value in a restricted scope only, depending on execution context. New value + * is <b>never</b> persisted. New value is ephemeral and kept in memory only: + * <ul> + * <li>during current analysis in the case of scanner stack</li> + * <li>during processing of current HTTP request in the case of web server stack</li> + * <li>during execution of current task in the case of Compute Engine stack</li> + * </ul> + * Property is temporarily removed if the parameter {@code value} is {@code null} + */ + public Settings setProperty(String key, @Nullable String value) { + String validKey = definitions.validKey(key); + if (value == null) { + removeProperty(validKey); + } else { + set(validKey, trim(value)); + } + return this; + } + + /** + * @see #setProperty(String, String) + */ + public Settings setProperty(String key, @Nullable Boolean value) { + return setProperty(key, value == null ? null : String.valueOf(value)); + } + + /** + * @see #setProperty(String, String) + */ + public Settings setProperty(String key, @Nullable Integer value) { + return setProperty(key, value == null ? null : String.valueOf(value)); + } + + /** + * @see #setProperty(String, String) + */ + public Settings setProperty(String key, @Nullable Long value) { + return setProperty(key, value == null ? null : String.valueOf(value)); + } + + /** + * @see #setProperty(String, String) + */ + public Settings setProperty(String key, @Nullable Double value) { + return setProperty(key, value == null ? null : String.valueOf(value)); + } + + /** + * @see #setProperty(String, String) + */ + public Settings setProperty(String key, @Nullable Float value) { + return setProperty(key, value == null ? null : String.valueOf(value)); + } + + /** + * @see #setProperty(String, String) + */ + public Settings setProperty(String key, @Nullable Date date) { + return setProperty(key, date, false); + } + + public Settings addProperties(Map<String, String> props) { + for (Map.Entry<String, String> entry : props.entrySet()) { + setProperty(entry.getKey(), entry.getValue()); + } + return this; + } + + public Settings addProperties(Properties props) { + for (Map.Entry<Object, Object> entry : props.entrySet()) { + setProperty(entry.getKey().toString(), entry.getValue().toString()); + } + return this; + } + + /** + * @see #setProperty(String, String) + */ + public Settings setProperty(String key, @Nullable Date date, boolean includeTime) { + if (date == null) { + return removeProperty(key); + } + return setProperty(key, includeTime ? DateUtils.formatDateTime(date) : DateUtils.formatDate(date)); + } + + public Settings removeProperty(String key) { + remove(key); + return this; + } + + public List<String> getKeysStartingWith(String prefix) { + return getProperties().keySet().stream() + .filter(key -> StringUtils.startsWith(key, prefix)) + .collect(Collectors.toList()); + } + +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java index 66d0448ee4b..c336e9c4fa5 100644 --- a/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java @@ -28,23 +28,22 @@ import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import org.sonar.api.batch.bootstrap.ProjectDefinition; import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.fs.internal.DefaultFileSystem; +import org.sonar.api.batch.fs.internal.DefaultInputFile; +import org.sonar.api.batch.fs.internal.DefaultInputModule; +import org.sonar.api.batch.fs.internal.DefaultTextPointer; +import org.sonar.api.batch.fs.internal.TestInputFileBuilder; import org.sonar.api.batch.rule.ActiveRules; +import org.sonar.api.batch.rule.Severity; import org.sonar.api.batch.rule.internal.ActiveRulesBuilder; import org.sonar.api.batch.rule.internal.NewActiveRule; -import org.sonar.api.batch.rule.Severity; import org.sonar.api.batch.sensor.error.AnalysisError; import org.sonar.api.batch.sensor.error.NewAnalysisError; import org.sonar.api.batch.sensor.highlighting.TypeOfText; import org.sonar.api.batch.sensor.issue.NewExternalIssue; import org.sonar.api.batch.sensor.issue.NewIssue; import org.sonar.api.batch.sensor.symbol.NewSymbolTable; -import org.sonar.api.config.Settings; import org.sonar.api.config.internal.MapSettings; -import org.sonar.api.batch.fs.internal.DefaultFileSystem; -import org.sonar.api.batch.fs.internal.DefaultInputFile; -import org.sonar.api.batch.fs.internal.DefaultInputModule; -import org.sonar.api.batch.fs.internal.DefaultTextPointer; -import org.sonar.api.batch.fs.internal.TestInputFileBuilder; import org.sonar.api.measures.CoreMetrics; import org.sonar.api.rule.RuleKey; import org.sonar.api.rules.RuleType; @@ -72,10 +71,10 @@ public class SensorContextTesterTest { @Test public void testSettings() { - Settings settings = new MapSettings(); + MapSettings settings = new MapSettings(); settings.setProperty("foo", "bar"); tester.setSettings(settings); - assertThat(tester.settings().getString("foo")).isEqualTo("bar"); + assertThat(tester.config().get("foo")).contains("bar"); } @Test diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/config/internal/AesCipherTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/config/internal/AesCipherTest.java new file mode 100644 index 00000000000..900fe691286 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/config/internal/AesCipherTest.java @@ -0,0 +1,182 @@ +/* + * 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.api.config.internal; + +import java.io.File; +import java.net.URL; +import java.security.InvalidKeyException; +import java.security.Key; +import javax.crypto.BadPaddingException; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang.StringUtils; +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 AesCipherTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void generateRandomSecretKey() { + AesCipher cipher = new AesCipher(null); + + String key = cipher.generateRandomSecretKey(); + + assertThat(StringUtils.isNotBlank(key)).isTrue(); + assertThat(Base64.isArrayByteBase64(key.getBytes())).isTrue(); + } + + @Test + public void encrypt() throws Exception { + AesCipher cipher = new AesCipher(pathToSecretKey()); + + String encryptedText = cipher.encrypt("this is a secret"); + + assertThat(StringUtils.isNotBlank(encryptedText)).isTrue(); + assertThat(Base64.isArrayByteBase64(encryptedText.getBytes())).isTrue(); + } + + @Test + public void encrypt_bad_key() throws Exception { + thrown.expect(RuntimeException.class); + thrown.expectMessage("Invalid AES key"); + + URL resource = getClass().getResource("/org/sonar/api/config/internal/AesCipherTest/bad_secret_key.txt"); + AesCipher cipher = new AesCipher(new File(resource.toURI()).getCanonicalPath()); + + cipher.encrypt("this is a secret"); + } + + @Test + public void decrypt() throws Exception { + AesCipher cipher = new AesCipher(pathToSecretKey()); + + // the following value has been encrypted with the key /org/sonar/api/config/internal/AesCipherTest/aes_secret_key.txt + String clearText = cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY="); + + assertThat(clearText).isEqualTo("this is a secret"); + } + + @Test + public void decrypt_bad_key() throws Exception { + URL resource = getClass().getResource("/org/sonar/api/config/internal/AesCipherTest/bad_secret_key.txt"); + AesCipher cipher = new AesCipher(new File(resource.toURI()).getCanonicalPath()); + + try { + cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY="); + fail(); + + } catch (RuntimeException e) { + assertThat(e.getCause()).isInstanceOf(InvalidKeyException.class); + } + } + + @Test + public void decrypt_other_key() throws Exception { + URL resource = getClass().getResource("/org/sonar/api/config/internal/AesCipherTest/other_secret_key.txt"); + AesCipher cipher = new AesCipher(new File(resource.toURI()).getCanonicalPath()); + + try { + // text encrypted with another key + cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY="); + fail(); + + } catch (RuntimeException e) { + assertThat(e.getCause()).isInstanceOf(BadPaddingException.class); + } + } + + @Test + public void encryptThenDecrypt() throws Exception { + AesCipher cipher = new AesCipher(pathToSecretKey()); + + assertThat(cipher.decrypt(cipher.encrypt("foo"))).isEqualTo("foo"); + } + + @Test + public void testDefaultPathToSecretKey() { + AesCipher cipher = new AesCipher(null); + + String path = cipher.getPathToSecretKey(); + + assertThat(StringUtils.isNotBlank(path)).isTrue(); + assertThat(new File(path).getName()).isEqualTo("sonar-secret.txt"); + } + + @Test + public void loadSecretKeyFromFile() throws Exception { + AesCipher cipher = new AesCipher(null); + Key secretKey = cipher.loadSecretFileFromFile(pathToSecretKey()); + assertThat(secretKey.getAlgorithm()).isEqualTo("AES"); + assertThat(secretKey.getEncoded().length).isGreaterThan(10); + } + + @Test + public void loadSecretKeyFromFile_trim_content() throws Exception { + URL resource = getClass().getResource("/org/sonar/api/config/internal/AesCipherTest/non_trimmed_secret_key.txt"); + String path = new File(resource.toURI()).getCanonicalPath(); + AesCipher cipher = new AesCipher(null); + + Key secretKey = cipher.loadSecretFileFromFile(path); + + assertThat(secretKey.getAlgorithm()).isEqualTo("AES"); + assertThat(secretKey.getEncoded().length).isGreaterThan(10); + } + + @Test + public void loadSecretKeyFromFile_file_does_not_exist() throws Exception { + thrown.expect(IllegalStateException.class); + + AesCipher cipher = new AesCipher(null); + cipher.loadSecretFileFromFile("/file/does/not/exist"); + } + + @Test + public void loadSecretKeyFromFile_no_property() throws Exception { + thrown.expect(IllegalStateException.class); + + AesCipher cipher = new AesCipher(null); + cipher.loadSecretFileFromFile(null); + } + + @Test + public void hasSecretKey() throws Exception { + AesCipher cipher = new AesCipher(pathToSecretKey()); + + assertThat(cipher.hasSecretKey()).isTrue(); + } + + @Test + public void doesNotHaveSecretKey() { + AesCipher cipher = new AesCipher("/my/twitter/id/is/SimonBrandhof"); + + assertThat(cipher.hasSecretKey()).isFalse(); + } + + private String pathToSecretKey() throws Exception { + URL resource = getClass().getResource("/org/sonar/api/config/internal/AesCipherTest/aes_secret_key.txt"); + return new File(resource.toURI()).getCanonicalPath(); + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/config/internal/EncryptionTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/config/internal/EncryptionTest.java new file mode 100644 index 00000000000..5bfcf34f2c6 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/config/internal/EncryptionTest.java @@ -0,0 +1,63 @@ +/* + * 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.api.config.internal; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EncryptionTest { + + @Test + public void isEncrypted() { + Encryption encryption = new Encryption(null); + assertThat(encryption.isEncrypted("{aes}ADASDASAD")).isTrue(); + assertThat(encryption.isEncrypted("{b64}ADASDASAD")).isTrue(); + assertThat(encryption.isEncrypted("{abc}ADASDASAD")).isTrue(); + + assertThat(encryption.isEncrypted("{}")).isFalse(); + assertThat(encryption.isEncrypted("{foo")).isFalse(); + assertThat(encryption.isEncrypted("foo{aes}")).isFalse(); + } + + @Test + public void scramble() { + Encryption encryption = new Encryption(null); + assertThat(encryption.scramble("foo")).isEqualTo("{b64}Zm9v"); + } + + @Test + public void decrypt() { + Encryption encryption = new Encryption(null); + assertThat(encryption.decrypt("{b64}Zm9v")).isEqualTo("foo"); + } + + @Test + public void decrypt_unknown_algorithm() { + Encryption encryption = new Encryption(null); + assertThat(encryption.decrypt("{xxx}Zm9v")).isEqualTo("{xxx}Zm9v"); + } + + @Test + public void decrypt_uncrypted_text() { + Encryption encryption = new Encryption(null); + assertThat(encryption.decrypt("foo")).isEqualTo("foo"); + } +} diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/config/internal/MapSettingsTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/config/internal/MapSettingsTest.java index d4554b2fc55..89845b01dbb 100644 --- a/sonar-plugin-api-impl/src/test/java/org/sonar/api/config/internal/MapSettingsTest.java +++ b/sonar-plugin-api-impl/src/test/java/org/sonar/api/config/internal/MapSettingsTest.java @@ -40,7 +40,6 @@ import org.sonar.api.Property; import org.sonar.api.PropertyType; import org.sonar.api.config.PropertyDefinition; import org.sonar.api.config.PropertyDefinitions; -import org.sonar.api.config.Settings; import org.sonar.api.utils.DateUtils; import static java.util.Collections.singletonList; diff --git a/sonar-plugin-api-impl/src/test/resources/org/sonar/api/config/internal/AesCipherTest/aes_secret_key.txt b/sonar-plugin-api-impl/src/test/resources/org/sonar/api/config/internal/AesCipherTest/aes_secret_key.txt new file mode 100644 index 00000000000..65b98c522da --- /dev/null +++ b/sonar-plugin-api-impl/src/test/resources/org/sonar/api/config/internal/AesCipherTest/aes_secret_key.txt @@ -0,0 +1 @@ +0PZz+G+f8mjr3sPn4+AhHg==
\ No newline at end of file diff --git a/sonar-plugin-api-impl/src/test/resources/org/sonar/api/config/internal/AesCipherTest/bad_secret_key.txt b/sonar-plugin-api-impl/src/test/resources/org/sonar/api/config/internal/AesCipherTest/bad_secret_key.txt new file mode 100644 index 00000000000..b33e179e5c8 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/resources/org/sonar/api/config/internal/AesCipherTest/bad_secret_key.txt @@ -0,0 +1 @@ +badbadbad==
\ No newline at end of file diff --git a/sonar-plugin-api-impl/src/test/resources/org/sonar/api/config/internal/AesCipherTest/non_trimmed_secret_key.txt b/sonar-plugin-api-impl/src/test/resources/org/sonar/api/config/internal/AesCipherTest/non_trimmed_secret_key.txt new file mode 100644 index 00000000000..ab83e4adc03 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/resources/org/sonar/api/config/internal/AesCipherTest/non_trimmed_secret_key.txt @@ -0,0 +1,3 @@ + + 0PZz+G+f8mjr3sPn4+AhHg== + diff --git a/sonar-plugin-api-impl/src/test/resources/org/sonar/api/config/internal/AesCipherTest/other_secret_key.txt b/sonar-plugin-api-impl/src/test/resources/org/sonar/api/config/internal/AesCipherTest/other_secret_key.txt new file mode 100644 index 00000000000..23f5ecf5104 --- /dev/null +++ b/sonar-plugin-api-impl/src/test/resources/org/sonar/api/config/internal/AesCipherTest/other_secret_key.txt @@ -0,0 +1 @@ +IBxEUxZ41c8XTxyaah1Qlg==
\ No newline at end of file |