entry 'scribejava-apis'
entry 'scribejava-core'
}
+ dependency 'com.github.everit-org.json-schema:org.everit.json.schema:1.12.2'
// This project is no longer maintained and was forked
// by https://github.com/java-diff-utils/java-diff-utils
// (io.github.java-diff-utils:java-diff-utils).
import java.util.Optional;
import java.util.stream.Collectors;
import javax.annotation.CheckForNull;
+
import org.sonar.api.Plugin;
import org.sonar.core.platform.PluginInfo;
import org.sonar.core.platform.PluginRepository;
// please keep the list grouped by configuration and ordered by name
compile 'com.google.guava:guava'
+ compile 'com.github.everit-org.json-schema:org.everit.json.schema'
compile project(':server:sonar-ce-common')
compile project(':server:sonar-ce-task')
compile project(':server:sonar-db-dao')
import com.google.common.collect.ImmutableSet;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
+
import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
+
+import org.everit.json.schema.ValidationException;
+import org.everit.json.schema.loader.SchemaLoader;
+import org.json.JSONObject;
+import org.json.JSONTokener;
import org.sonar.api.PropertyType;
import org.sonar.api.config.PropertyDefinition;
import org.sonar.api.config.PropertyDefinitions;
import org.sonar.server.exceptions.BadRequestException;
import static java.lang.String.format;
+import static java.util.Arrays.asList;
import static java.util.Objects.requireNonNull;
import static org.sonar.server.exceptions.BadRequestException.checkRequest;
public class SettingValidations {
+ private static final Collection<String> SECURITY_JSON_PROPERTIES = asList(
+ "sonar.security.config.javasecurity",
+ "sonar.security.config.phpsecurity",
+ "sonar.security.config.pythonsecurity",
+ "sonar.security.config.roslyn.sonaranalyzer.security.cs"
+ );
private final PropertyDefinitions definitions;
private final DbClient dbClient;
private final I18n i18n;
}
private class ValueTypeValidation implements Consumer<SettingData> {
+
@Override
public void accept(SettingData data) {
PropertyDefinition definition = definitions.get(data.key);
} else if (definition.type() == PropertyType.USER_LOGIN) {
validateLogin(data);
} else if (definition.type() == PropertyType.JSON) {
- validateJson(data);
+ validateJson(data, definition);
} else {
validateOtherTypes(data, definition);
}
}
}
- private void validateJson(SettingData data) {
+ private void validateJson(SettingData data, PropertyDefinition definition) {
Optional<String> jsonContent = data.values.stream().findFirst();
if (jsonContent.isPresent()) {
try {
new Gson().getAdapter(JsonElement.class).fromJson(jsonContent.get());
- } catch (IOException e) {
+ validateJsonSchema(jsonContent.get(), definition);
+ } catch (ValidationException e) {
+ throw new IllegalArgumentException(String.format("Provided JSON is invalid [%s]", e.getMessage()));
+ } catch (IOException e){
throw new IllegalArgumentException("Provided JSON is invalid");
}
}
}
+
+ private void validateJsonSchema(String json, PropertyDefinition definition) {
+ if(SECURITY_JSON_PROPERTIES.contains(definition.key())){
+ InputStream jsonSchemaInputStream = this.getClass().getClassLoader().getResourceAsStream("json-schemas/security.json");
+ if(jsonSchemaInputStream != null){
+ JSONObject jsonSchema = new JSONObject(new JSONTokener(jsonSchemaInputStream));
+ JSONObject jsonSubject = new JSONObject(new JSONTokener(json));
+ SchemaLoader.load(jsonSchema).validate(jsonSubject);
+ }
+ }
+
+ }
}
}
--- /dev/null
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Custom configuration schema",
+ "description": "Schema to validate custom configuration given as input to the custom configuration properties",
+ "definitions": {
+ "Interval": {
+ "type": "object",
+ "properties": {
+ "fromIndex": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false
+ },
+ "CommonConfiguration": {
+ "type": "object",
+ "properties": {
+ "args": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ "interval": {
+ "$ref": "#/definitions/Interval"
+ },
+ "isMethodPrefix": {
+ "type": "boolean"
+ },
+ "isShallow": {
+ "type": "boolean"
+ },
+ "isWhitelist": {
+ "type": "boolean"
+ },
+ "methodId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "methodId"
+ ],
+ "additionalProperties": false
+ },
+ "RuleConfiguration": {
+ "type": "object",
+ "properties": {
+ "decoders": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/CommonConfiguration"
+ }
+ },
+ "encoders": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/CommonConfiguration"
+ }
+ },
+ "passthroughs": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/CommonConfiguration"
+ }
+ },
+ "sanitizers": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/CommonConfiguration"
+ }
+ },
+ "sinks": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/CommonConfiguration"
+ }
+ },
+ "sources": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/CommonConfiguration"
+ }
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/definitions/RuleConfiguration"
+ }
+}
\ No newline at end of file
import java.util.Random;
import javax.annotation.Nullable;
+
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+
import java.net.HttpURLConnection;
import java.util.List;
import java.util.Random;
import javax.annotation.Nullable;
+
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
import org.sonar.api.PropertyType;
import org.sonar.api.config.PropertyDefinition;
import org.sonar.api.config.PropertyDefinitions;
import static org.sonar.db.property.PropertyTesting.newGlobalPropertyDto;
import static org.sonar.db.user.UserTesting.newUserDto;
+@RunWith(DataProviderRunner.class)
public class SetActionTest {
private static final Gson GSON = GsonHelper.create();
userSession.logIn().setSystemAdministrator();
}
+ @DataProvider
+ public static Object[][] securityJsonProperties() {
+ return new Object[][] {
+ {"sonar.security.config.javasecurity"},
+ {"sonar.security.config.phpsecurity"},
+ {"sonar.security.config.pythonsecurity"},
+ {"sonar.security.config.roslyn.sonaranalyzer.security.cs"}
+ };
+ }
+
@Test
public void empty_204_response() {
TestResponse result = ws.newRequest()
.hasMessage("Provided JSON is invalid");
}
+ @Test
+ @UseDataProvider("securityJsonProperties")
+ public void successfully_validate_json_schema(String securityPropertyKey) {
+ String security_custom_config = "{\n" +
+ " \"S3649\": {\n" +
+ " \"sources\": [\n" +
+ " {\n" +
+ " \"methodId\": \"My\\\\Namespace\\\\ClassName\\\\ServerRequest::getQuery\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"sanitizers\": [\n" +
+ " {\n" +
+ " \"methodId\": \"str_replace\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"sinks\": [\n" +
+ " {\n" +
+ " \"methodId\": \"mysql_query\",\n" +
+ " \"args\": [1]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ "}";
+ definitions.addComponent(PropertyDefinition
+ .builder(securityPropertyKey)
+ .name("foo")
+ .description("desc")
+ .category("cat")
+ .subCategory("subCat")
+ .type(PropertyType.JSON)
+ .build());
+
+ callForGlobalSetting(securityPropertyKey, security_custom_config);
+
+ assertGlobalSetting(securityPropertyKey, security_custom_config);
+ }
+
+ @Test
+ @UseDataProvider("securityJsonProperties")
+ public void fail_json_schema_validation_when_property_has_incorrect_type(String securityPropertyKey) {
+ String security_custom_config = "{\n" +
+ " \"S3649\": {\n" +
+ " \"sources\": [\n" +
+ " {\n" +
+ " \"methodId\": \"My\\\\Namespace\\\\ClassName\\\\ServerRequest::getQuery\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"sinks\": [\n" +
+ " {\n" +
+ " \"methodId\": 12345,\n" +
+ " \"args\": [1]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ "}";
+ definitions.addComponent(PropertyDefinition
+ .builder(securityPropertyKey)
+ .name("foo")
+ .description("desc")
+ .category("cat")
+ .subCategory("subCat")
+ .type(PropertyType.JSON)
+ .build());
+
+ assertThatThrownBy(() -> callForGlobalSetting(securityPropertyKey, security_custom_config))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("S3649/sinks/0/methodId: expected type: String, found: Integer");
+ }
+
+ @Test
+ @UseDataProvider("securityJsonProperties")
+ public void fail_json_schema_validation_when_property_has_unknown_attribute(String securityPropertyKey) {
+ String security_custom_config = "{\n" +
+ " \"S3649\": {\n" +
+ " \"sources\": [\n" +
+ " {\n" +
+ " \"methodId\": \"My\\\\Namespace\\\\ClassName\\\\ServerRequest::getQuery\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"unknown\": [\n" +
+ " {\n" +
+ " \"methodId\": 12345,\n" +
+ " \"args\": [1]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ "}";
+ definitions.addComponent(PropertyDefinition
+ .builder(securityPropertyKey)
+ .name("foo")
+ .description("desc")
+ .category("cat")
+ .subCategory("subCat")
+ .type(PropertyType.JSON)
+ .build());
+
+ assertThatThrownBy(() -> callForGlobalSetting(securityPropertyKey, security_custom_config))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("extraneous key [unknown] is not permitted");
+ }
+
@Test
public void persist_global_setting_with_non_ascii_characters() {
callForGlobalSetting("my.key", "fi±∞…");
}
// Check the size of the archive
zip.doLast {
- def minLength = 237000000
- def maxLength = 257000000
+ def minLength = 238000000
+ def maxLength = 258000000
def length = archiveFile.get().asFile.length()
if (length < minLength)