Browse Source

SONAR-14499 Support schema validation for JSON property types

tags/8.8.0.42792
Zipeng WU 3 years ago
parent
commit
9a4cafe8c6

+ 1
- 0
build.gradle View File

entry 'scribejava-apis' entry 'scribejava-apis'
entry 'scribejava-core' 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 // This project is no longer maintained and was forked
// by https://github.com/java-diff-utils/java-diff-utils // by https://github.com/java-diff-utils/java-diff-utils
// (io.github.java-diff-utils:java-diff-utils). // (io.github.java-diff-utils:java-diff-utils).

+ 1
- 0
server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/ServerPluginRepository.java View File

import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.CheckForNull; import javax.annotation.CheckForNull;

import org.sonar.api.Plugin; import org.sonar.api.Plugin;
import org.sonar.core.platform.PluginInfo; import org.sonar.core.platform.PluginInfo;
import org.sonar.core.platform.PluginRepository; import org.sonar.core.platform.PluginRepository;

+ 1
- 0
server/sonar-webserver-webapi/build.gradle View File

// please keep the list grouped by configuration and ordered by name // please keep the list grouped by configuration and ordered by name


compile 'com.google.guava:guava' 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-common')
compile project(':server:sonar-ce-task') compile project(':server:sonar-ce-task')
compile project(':server:sonar-db-dao') compile project(':server:sonar-db-dao')

+ 34
- 3
server/sonar-webserver-webapi/src/main/java/org/sonar/server/setting/ws/SettingValidations.java View File

import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;

import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.CheckForNull; import javax.annotation.CheckForNull;
import javax.annotation.Nullable; 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.PropertyType;
import org.sonar.api.config.PropertyDefinition; import org.sonar.api.config.PropertyDefinition;
import org.sonar.api.config.PropertyDefinitions; import org.sonar.api.config.PropertyDefinitions;
import org.sonar.server.exceptions.BadRequestException; import org.sonar.server.exceptions.BadRequestException;


import static java.lang.String.format; import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static org.sonar.server.exceptions.BadRequestException.checkRequest; import static org.sonar.server.exceptions.BadRequestException.checkRequest;


public class SettingValidations { 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 PropertyDefinitions definitions;
private final DbClient dbClient; private final DbClient dbClient;
private final I18n i18n; private final I18n i18n;
} }


private class ValueTypeValidation implements Consumer<SettingData> { private class ValueTypeValidation implements Consumer<SettingData> {

@Override @Override
public void accept(SettingData data) { public void accept(SettingData data) {
PropertyDefinition definition = definitions.get(data.key); PropertyDefinition definition = definitions.get(data.key);
} else if (definition.type() == PropertyType.USER_LOGIN) { } else if (definition.type() == PropertyType.USER_LOGIN) {
validateLogin(data); validateLogin(data);
} else if (definition.type() == PropertyType.JSON) { } else if (definition.type() == PropertyType.JSON) {
validateJson(data);
validateJson(data, definition);
} else { } else {
validateOtherTypes(data, definition); validateOtherTypes(data, definition);
} }
} }
} }


private void validateJson(SettingData data) {
private void validateJson(SettingData data, PropertyDefinition definition) {
Optional<String> jsonContent = data.values.stream().findFirst(); Optional<String> jsonContent = data.values.stream().findFirst();
if (jsonContent.isPresent()) { if (jsonContent.isPresent()) {
try { try {
new Gson().getAdapter(JsonElement.class).fromJson(jsonContent.get()); 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"); 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);
}
}

}
} }
} }

+ 92
- 0
server/sonar-webserver-webapi/src/main/resources/json-schemas/security.json View File

{
"$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"
}
}

+ 1
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/setting/ws/ResetActionTest.java View File



import java.util.Random; import java.util.Random;
import javax.annotation.Nullable; import javax.annotation.Nullable;

import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;

+ 118
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/setting/ws/SetActionTest.java View File



import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson; 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.net.HttpURLConnection;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import javax.annotation.Nullable; import javax.annotation.Nullable;

import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.sonar.api.PropertyType; import org.sonar.api.PropertyType;
import org.sonar.api.config.PropertyDefinition; import org.sonar.api.config.PropertyDefinition;
import org.sonar.api.config.PropertyDefinitions; import org.sonar.api.config.PropertyDefinitions;
import static org.sonar.db.property.PropertyTesting.newGlobalPropertyDto; import static org.sonar.db.property.PropertyTesting.newGlobalPropertyDto;
import static org.sonar.db.user.UserTesting.newUserDto; import static org.sonar.db.user.UserTesting.newUserDto;


@RunWith(DataProviderRunner.class)
public class SetActionTest { public class SetActionTest {


private static final Gson GSON = GsonHelper.create(); private static final Gson GSON = GsonHelper.create();
userSession.logIn().setSystemAdministrator(); 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 @Test
public void empty_204_response() { public void empty_204_response() {
TestResponse result = ws.newRequest() TestResponse result = ws.newRequest()
.hasMessage("Provided JSON is invalid"); .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 @Test
public void persist_global_setting_with_non_ascii_characters() { public void persist_global_setting_with_non_ascii_characters() {
callForGlobalSetting("my.key", "fi±∞…"); callForGlobalSetting("my.key", "fi±∞…");

+ 2
- 2
sonar-application/build.gradle View File

} }
// Check the size of the archive // Check the size of the archive
zip.doLast { zip.doLast {
def minLength = 237000000
def maxLength = 257000000
def minLength = 238000000
def maxLength = 258000000


def length = archiveFile.get().asFile.length() def length = archiveFile.get().asFile.length()
if (length < minLength) if (length < minLength)

Loading…
Cancel
Save