Changing delimiter char for field/decorator in Scripting API (#25155)

* adding risk score

* move risk score slicing into enterprise

* fix field name

* adding changelog

* using ".." temporarily to separate field/decorator

* using pipe symbol th separate field/decorator

* settling on # to separate field/decorator

* adjusting test

* Update pr-25155.toml

* Update pr-25155.toml

* adjusting test

* adjusting test
This commit is contained in:
Jan Heise
2026-03-09 11:51:56 +01:00
committed by GitHub
parent 97bbfff89d
commit b3b43448db
9 changed files with 44 additions and 33 deletions

View File

@@ -0,0 +1,4 @@
type = "a"
message = "Changing delimiter char for decorators in ScriptingApi from '.' to '#' to avoid parsing problems when querying a nested field."
pulls = ["25155"]

View File

@@ -108,7 +108,7 @@ public class ScriptingApiResourceIT {
{
"group_by": [
{
"field": "streams.id"
"field": "streams#id"
}
],
"metrics": [
@@ -186,7 +186,7 @@ public class ScriptingApiResourceIT {
{
"group_by": [
{
"field": "streams.title"
"field": "streams#title"
}
],
"metrics": [

View File

@@ -70,7 +70,7 @@ public class EventsSearchService extends AbstractEventsSearchService {
return new EventsFilterBuilder(parameters).build();
}
private Set<String> allowedEventStreams(Subject subject) {
public Set<String> allowedEventStreams(Subject subject) {
final var eventStreams = defaultEventStreams();
if (subject.isPermitted(RestPermissions.STREAMS_READ)) {
return eventStreams;
@@ -113,28 +113,29 @@ public class EventsSearchService extends AbstractEventsSearchService {
/**
* In the Open Source part, we only map priority and type, both of which are augmented in the FE regarding the title.
* So we only need a simple mapping function
*
* @param slicingColumn
* @param result
* @return Slice
*/
Slice mapAggregationResultsToSlice(final String slicingColumn, final List<Object> result) {
public Slice mapAggregationResultsToSlice(final String slicingColumn, final List<Object> result) {
return new Slice(result.getFirst().toString(), null, Integer.valueOf(result.getLast().toString()));
}
// the alert can either be true or false
List<Slice> handleAlertColumn(final List<Slice> slices) {
if(slices.size() == 2) {
if (slices.size() == 2) {
return slices;
}
final var TRUE = new Slice( "true", null, 0);
final var FALSE = new Slice( "false", null, 0);
final var TRUE = new Slice("true", null, 0);
final var FALSE = new Slice("false", null, 0);
if(slices.isEmpty()) {
if (slices.isEmpty()) {
return List.of(TRUE, FALSE);
}
if(slices.getFirst().value().equals("true")) {
if (slices.getFirst().value().equals("true")) {
return List.of(slices.getFirst(), FALSE);
} else {
return List.of(TRUE, slices.getFirst());
@@ -143,16 +144,16 @@ public class EventsSearchService extends AbstractEventsSearchService {
// priority can be 1 (low) to 4 (critical), see EventDefinitionPriority.ts
List<Slice> handlePriorityColumn(final List<Slice> slices) {
if(slices.size() == 4) {
if (slices.size() == 4) {
return slices;
}
final var LOW = new Slice( "1", null, 0);
final var MEDIUM = new Slice( "2", null, 0);
final var HIGH = new Slice( "3", null, 0);
final var CRITICAL = new Slice( "4", null, 0);
final var LOW = new Slice("1", null, 0);
final var MEDIUM = new Slice("2", null, 0);
final var HIGH = new Slice("3", null, 0);
final var CRITICAL = new Slice("4", null, 0);
if(slices.isEmpty()) {
if (slices.isEmpty()) {
return List.of(LOW, MEDIUM, HIGH, CRITICAL);
}
@@ -165,7 +166,7 @@ public class EventsSearchService extends AbstractEventsSearchService {
return fixedList;
}
private List<Slice> addMissingOptions(final List<Slice> slices, final String slicingColumn) {
public List<Slice> addMissingOptions(final List<Slice> slices, final String slicingColumn) {
return switch (slicingColumn) {
case FIELD_ALERT -> handleAlertColumn(slices);
case FIELD_PRIORITY -> handlePriorityColumn(slices);

View File

@@ -66,6 +66,11 @@ public record Grouping(@JsonProperty("field") @Valid @NotBlank String fieldName,
this(fieldName, Optional.of(limit), Optional.empty(), Optional.empty(), Optional.empty());
}
public Grouping(@JsonProperty("field") @Valid @NotBlank String fieldName,
@JsonProperty("ranges") List<NumberRange> ranges) {
this(fieldName, Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(ranges));
}
@Deprecated
@Override
public String fieldName() {

View File

@@ -16,16 +16,17 @@
*/
package org.graylog.plugins.views.search.rest.scriptingapi.request;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.google.common.base.Splitter;
import javax.annotation.Nullable;
import java.util.List;
public record RequestedField(String name, @Nullable String decorator) {
// according to https://discuss.elastic.co/t/legal-character-set-for-field-names/190796, # seems to be a good separator char
public static String DECORATOR_SEPARATOR = "#";
public static RequestedField parse(String value) {
final List<String> parts = Splitter.on(".")
final List<String> parts = Splitter.on(DECORATOR_SEPARATOR)
.limit(2)
.trimResults()
.omitEmptyStrings()
@@ -43,7 +44,7 @@ public record RequestedField(String name, @Nullable String decorator) {
if (decorator == null) {
return name;
} else {
return name + "." + decorator;
return name + DECORATOR_SEPARATOR + decorator;
}
}

View File

@@ -40,7 +40,7 @@ class TabularResponseCreatorTest {
(field) -> Objects.equals(field.decorator(), "uppercase"),
(field, o, searchUser) -> String.valueOf(o).toUpperCase(Locale.ROOT))
);
final Object decorated = creator.decorate(decorators, RequestedField.parse("myfield.uppercase"), "my-value", TestSearchUser.builder().build());
final Object decorated = creator.decorate(decorators, RequestedField.parse("myfield#uppercase"), "my-value", TestSearchUser.builder().build());
Assertions.assertThat(decorated).isEqualTo("MY-VALUE");
}
@@ -53,7 +53,7 @@ class TabularResponseCreatorTest {
(field, o, searchUser) -> String.valueOf(o).toUpperCase(Locale.ROOT))
);
Assertions.assertThatThrownBy(() -> creator.decorate(decorators, RequestedField.parse("myfield.lowercase"), "my-value", TestSearchUser.builder().build()))
Assertions.assertThatThrownBy(() -> creator.decorate(decorators, RequestedField.parse("myfield#lowercase"), "my-value", TestSearchUser.builder().build()))
.isInstanceOf(UnsupportedOperationException.class)
.hasMessageContaining("Unsupported property 'lowercase' on field 'myfield'");
}
@@ -71,7 +71,7 @@ class TabularResponseCreatorTest {
(field, o, searchUser) -> String.valueOf(o).toLowerCase(Locale.ROOT))
);
Assertions.assertThatThrownBy(() -> creator.decorate(decorators, RequestedField.parse("myfield.lowercase"), "my-value", TestSearchUser.builder().build()))
Assertions.assertThatThrownBy(() -> creator.decorate(decorators, RequestedField.parse("myfield#lowercase"), "my-value", TestSearchUser.builder().build()))
.isInstanceOf(UnsupportedOperationException.class)
.hasMessageContaining("Found more decorators supporting 'lowercase' on field 'myfield', this is not supported operation.");
}

View File

@@ -22,11 +22,11 @@ import org.junit.jupiter.api.Test;
class RequestedFieldTest {
@Test
void testParsing() {
final RequestedField streamId = RequestedField.parse("streams.id");
final RequestedField streamId = RequestedField.parse("streams#id");
Assertions.assertThat(streamId.name()).isEqualTo("streams");
Assertions.assertThat(streamId.decorator()).isEqualTo("id");
final RequestedField streamName = RequestedField.parse("streams.name");
final RequestedField streamName = RequestedField.parse("streams#name");
Assertions.assertThat(streamName.name()).isEqualTo("streams");
Assertions.assertThat(streamName.decorator()).isEqualTo("name");

View File

@@ -27,9 +27,9 @@ class IdDecoratorTest {
@Test
void accept() {
final FieldDecorator decorator = new IdDecorator();
Assertions.assertThat(decorator.accept(RequestedField.parse("streams.id"))).isTrue();
Assertions.assertThat(decorator.accept(RequestedField.parse("gl2_source_input.id"))).isTrue();
Assertions.assertThat(decorator.accept(RequestedField.parse("gl2_source_node.id"))).isTrue();
Assertions.assertThat(decorator.accept(RequestedField.parse("streams#id"))).isTrue();
Assertions.assertThat(decorator.accept(RequestedField.parse("gl2_source_input#id"))).isTrue();
Assertions.assertThat(decorator.accept(RequestedField.parse("gl2_source_node#id"))).isTrue();
// default is to decorate as title, we don't want IDs
Assertions.assertThat(decorator.accept(RequestedField.parse("gl2_source_node"))).isFalse();
@@ -41,7 +41,7 @@ class IdDecoratorTest {
void decorate() {
final FieldDecorator decorator = new IdDecorator();
final SearchUser searchUser = TestSearchUser.builder().build();
Assertions.assertThat(decorator.decorate(RequestedField.parse("streams.id"), "123", searchUser))
Assertions.assertThat(decorator.decorate(RequestedField.parse("streams#id"), "123", searchUser))
.isEqualTo("123");
}
}

View File

@@ -33,19 +33,19 @@ class TitleDecoratorTest {
void testPermitted() {
final FieldDecorator decorator = new TitleDecorator((request, permissions) -> EntitiesTitleResponse.EMPTY_RESPONSE);
Assertions.assertThat(decorator.accept(RequestedField.parse("streams"))).isTrue();
Assertions.assertThat(decorator.accept(RequestedField.parse("streams.title"))).isTrue();
Assertions.assertThat(decorator.accept(RequestedField.parse("streams#title"))).isTrue();
Assertions.assertThat(decorator.accept(RequestedField.parse("gl2_source_input"))).isTrue();
Assertions.assertThat(decorator.accept(RequestedField.parse("gl2_source_input.title"))).isTrue();
Assertions.assertThat(decorator.accept(RequestedField.parse("gl2_source_input#title"))).isTrue();
// For IDs we have a different decorator
Assertions.assertThat(decorator.accept(RequestedField.parse("streams.id"))).isFalse();
Assertions.assertThat(decorator.accept(RequestedField.parse("gl2_source_input.id"))).isFalse();
Assertions.assertThat(decorator.accept(RequestedField.parse("gl2_source_input#id"))).isFalse();
// unknown decorator
Assertions.assertThat(decorator.accept(RequestedField.parse("gl2_source_input.uppercase"))).isFalse();
Assertions.assertThat(decorator.accept(RequestedField.parse("gl2_source_input#uppercase"))).isFalse();
// other fields and entities are not supported
Assertions.assertThat(decorator.accept(RequestedField.parse("http_response_code"))).isFalse();
Assertions.assertThat(decorator.accept(RequestedField.parse("http_response_code.title"))).isFalse();
Assertions.assertThat(decorator.accept(RequestedField.parse("http_response_code#title"))).isFalse();
}
@Test