@@ -251,6 +251,7 @@ subprojects { | |||
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). |
@@ -27,6 +27,7 @@ import java.util.Map; | |||
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; |
@@ -8,6 +8,7 @@ dependencies { | |||
// 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') |
@@ -22,7 +22,10 @@ package org.sonar.server.setting.ws; | |||
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; | |||
@@ -31,6 +34,11 @@ import java.util.function.Consumer; | |||
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; | |||
@@ -45,10 +53,17 @@ import org.sonar.db.user.UserDto; | |||
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; | |||
@@ -114,6 +129,7 @@ public class SettingValidations { | |||
} | |||
private class ValueTypeValidation implements Consumer<SettingData> { | |||
@Override | |||
public void accept(SettingData data) { | |||
PropertyDefinition definition = definitions.get(data.key); | |||
@@ -126,7 +142,7 @@ public class SettingValidations { | |||
} else if (definition.type() == PropertyType.USER_LOGIN) { | |||
validateLogin(data); | |||
} else if (definition.type() == PropertyType.JSON) { | |||
validateJson(data); | |||
validateJson(data, definition); | |||
} else { | |||
validateOtherTypes(data, definition); | |||
} | |||
@@ -159,15 +175,30 @@ public class SettingValidations { | |||
} | |||
} | |||
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); | |||
} | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,92 @@ | |||
{ | |||
"$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" | |||
} | |||
} |
@@ -21,6 +21,7 @@ package org.sonar.server.setting.ws; | |||
import java.util.Random; | |||
import javax.annotation.Nullable; | |||
import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; |
@@ -21,14 +21,20 @@ package org.sonar.server.setting.ws; | |||
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; | |||
@@ -72,6 +78,7 @@ import static org.sonar.db.property.PropertyTesting.newComponentPropertyDto; | |||
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(); | |||
@@ -103,6 +110,16 @@ public class SetActionTest { | |||
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() | |||
@@ -429,6 +446,107 @@ public class SetActionTest { | |||
.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±∞…"); |
@@ -164,8 +164,8 @@ zip.doFirst { | |||
} | |||
// 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) |