Skip to content

Commit 47df4bd

Browse files
authored
[Rule-based Auto-tagging] add the schema for security attributes (opensearch-project#19345)
Signed-off-by: Ruirui Zhang <[email protected]>
1 parent 9cfdd41 commit 47df4bd

File tree

23 files changed

+330
-47
lines changed

23 files changed

+330
-47
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
## [Unreleased 3.x]
77
### Added
88
- Expand fetch phase profiling to support inner hits and top hits aggregation phases ([##18936](https://github.com/opensearch-project/OpenSearch/pull/18936))
9+
- [Rule-based Auto-tagging] add the schema for security attributes ([##19345](https://github.com/opensearch-project/OpenSearch/pull/19345))
910
- Add temporal routing processors for time-based document routing ([#18920](https://github.com/opensearch-project/OpenSearch/issues/18920))
1011
- Implement Query Rewriting Infrastructure ([#19060](https://github.com/opensearch-project/OpenSearch/pull/19060))
1112
- The dynamic mapping parameter supports false_allow_templates ([#19065](https://github.com/opensearch-project/OpenSearch/pull/19065) ([#19097](https://github.com/opensearch-project/OpenSearch/pull/19097)))

modules/autotagging-commons/common/src/main/java/org/opensearch/rule/autotagging/Attribute.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,14 @@
1111
import org.opensearch.core.common.io.stream.StreamInput;
1212
import org.opensearch.core.common.io.stream.StreamOutput;
1313
import org.opensearch.core.common.io.stream.Writeable;
14+
import org.opensearch.core.xcontent.XContentBuilder;
15+
import org.opensearch.core.xcontent.XContentParseException;
16+
import org.opensearch.core.xcontent.XContentParser;
1417

1518
import java.io.IOException;
19+
import java.util.HashSet;
20+
import java.util.Set;
21+
import java.util.TreeMap;
1622

1723
/**
1824
* Represents an attribute within the auto-tagging feature. Attributes define characteristics that can
@@ -29,6 +35,13 @@ public interface Attribute extends Writeable {
2935
*/
3036
String getName();
3137

38+
/**
39+
* Returns a map of subfields ordered by priority, where 1 represents the highest priority.
40+
*/
41+
default TreeMap<Integer, String> getPrioritizedSubfields() {
42+
return new TreeMap<>();
43+
}
44+
3245
/**
3346
* Ensure that `validateAttribute` is called in the constructor of attribute implementations
3447
* to prevent potential serialization issues.
@@ -45,6 +58,39 @@ default void writeTo(StreamOutput out) throws IOException {
4558
out.writeString(getName());
4659
}
4760

61+
/**
62+
* Parses attribute values for specific attributes. This default function takes in parser
63+
* and returns a set of string.
64+
* For example, ["index1", "index2"] will be parsed to a set with values "index1" and "index2"
65+
* @param parser the XContent parser
66+
*/
67+
default Set<String> fromXContentParseAttributeValues(XContentParser parser) throws IOException {
68+
if (parser.currentToken() != XContentParser.Token.START_ARRAY) {
69+
throw new XContentParseException(
70+
parser.getTokenLocation(),
71+
"Expected START_ARRAY token for " + getName() + " attribute but got " + parser.currentToken()
72+
);
73+
}
74+
Set<String> attributeValueSet = new HashSet<>();
75+
while (parser.nextToken() != XContentParser.Token.END_ARRAY) {
76+
if (parser.currentToken() == XContentParser.Token.VALUE_STRING) {
77+
attributeValueSet.add(parser.text());
78+
} else {
79+
throw new XContentParseException("Unexpected token in array: " + parser.currentToken());
80+
}
81+
}
82+
return attributeValueSet;
83+
}
84+
85+
/**
86+
* Writes a set of attribute values for a specific attribute
87+
* @param builder the XContent builder
88+
* @param values the set of string values to write
89+
*/
90+
default void toXContentWriteAttributeValues(XContentBuilder builder, Set<String> values) throws IOException {
91+
builder.array(getName(), values.toArray(new String[0]));
92+
}
93+
4894
/**
4995
* Retrieves an attribute from the given feature type based on its name.
5096
* Implementations of `FeatureType.getAttributeFromName` must be thread-safe as this method

modules/autotagging-commons/common/src/main/java/org/opensearch/rule/autotagging/AutoTaggingRegistry.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ private static void validateFeatureType(FeatureType featureType) {
5959
"Feature type name " + name + " should not be null, empty or have more than " + MAX_FEATURE_TYPE_NAME_LENGTH + "characters"
6060
);
6161
}
62+
if (featureType.getOrderedAttributes() == null) {
63+
throw new IllegalStateException(
64+
"Function getOrderedAttributes() should not return null for feature type: " + featureType.getName()
65+
);
66+
}
6267
if (featureType.getFeatureValueValidator() == null) {
6368
throw new IllegalStateException("FeatureValueValidator is not defined for feature type " + name);
6469
}

modules/autotagging-commons/common/src/main/java/org/opensearch/rule/autotagging/FeatureType.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import java.io.IOException;
1616
import java.util.Map;
17+
import java.util.stream.Collectors;
1718

1819
/**
1920
* Represents a feature type within the auto-tagging feature. Feature types define different categories of
@@ -41,11 +42,19 @@ public interface FeatureType extends Writeable {
4142
*/
4243
String getName();
4344

45+
/**
46+
* Returns a map of top-level attributes sorted by priority, with 1 representing the highest priority.
47+
* Subfields within each attribute are managed separately here {@link org.opensearch.rule.autotagging.Attribute#getPrioritizedSubfields()}.
48+
*/
49+
Map<Attribute, Integer> getOrderedAttributes();
50+
4451
/**
4552
* Returns the registry of allowed attributes for this feature type.
4653
* Implementations must ensure that access to this registry is thread-safe.
4754
*/
48-
Map<String, Attribute> getAllowedAttributesRegistry();
55+
default Map<String, Attribute> getAllowedAttributesRegistry() {
56+
return getOrderedAttributes().keySet().stream().collect(Collectors.toUnmodifiableMap(Attribute::getName, attribute -> attribute));
57+
}
4958

5059
/**
5160
* returns the validator for feature value

modules/autotagging-commons/common/src/main/java/org/opensearch/rule/autotagging/Rule.java

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ public XContentBuilder toXContent(final XContentBuilder builder, final Params pa
177177
builder.field(ID_STRING, id);
178178
builder.field(DESCRIPTION_STRING, description);
179179
for (Map.Entry<Attribute, Set<String>> entry : attributeMap.entrySet()) {
180-
builder.array(entry.getKey().getName(), entry.getValue().toArray(new String[0]));
180+
entry.getKey().toXContentWriteAttributeValues(builder, entry.getValue());
181181
}
182182
builder.field(featureType.getName(), featureValue);
183183
builder.field(UPDATED_AT_STRING, updatedAt);
@@ -258,14 +258,14 @@ public static Builder fromXContent(XContentParser parser, FeatureType featureTyp
258258
builder.featureType(featureType);
259259
builder.featureValue(parser.text());
260260
}
261-
} else if (token == XContentParser.Token.START_ARRAY) {
262-
fromXContentParseArray(parser, fieldName, featureType, attributeMap1);
261+
} else if (token == XContentParser.Token.START_ARRAY || token == XContentParser.Token.START_OBJECT) {
262+
fromXContentParseAttribute(parser, fieldName, featureType, attributeMap1);
263263
}
264264
}
265265
return builder.attributeMap(attributeMap1);
266266
}
267267

268-
private static void fromXContentParseArray(
268+
private static void fromXContentParseAttribute(
269269
XContentParser parser,
270270
String fieldName,
271271
FeatureType featureType,
@@ -275,15 +275,7 @@ private static void fromXContentParseArray(
275275
if (attribute == null) {
276276
throw new XContentParseException(fieldName + " is not a valid attribute within the " + featureType.getName() + " feature.");
277277
}
278-
Set<String> attributeValueSet = new HashSet<>();
279-
while (parser.nextToken() != XContentParser.Token.END_ARRAY) {
280-
if (parser.currentToken() == XContentParser.Token.VALUE_STRING) {
281-
attributeValueSet.add(parser.text());
282-
} else {
283-
throw new XContentParseException("Unexpected token in array: " + parser.currentToken());
284-
}
285-
}
286-
attributeMap.put(attribute, attributeValueSet);
278+
attributeMap.put(attribute, attribute.fromXContentParseAttributeValues(parser));
287279
}
288280

289281
/**

modules/autotagging-commons/common/src/test/java/org/opensearch/rule/RuleAttributeTests.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@
88

99
package org.opensearch.rule;
1010

11+
import org.opensearch.common.xcontent.json.JsonXContent;
12+
import org.opensearch.core.xcontent.XContentParseException;
13+
import org.opensearch.core.xcontent.XContentParser;
1114
import org.opensearch.test.OpenSearchTestCase;
1215

16+
import java.io.IOException;
17+
import java.util.Set;
18+
1319
public class RuleAttributeTests extends OpenSearchTestCase {
1420

1521
public void testGetName() {
@@ -25,4 +31,40 @@ public void testFromName() {
2531
public void testFromName_throwsException() {
2632
assertThrows(IllegalArgumentException.class, () -> RuleAttribute.fromName("invalid_attribute"));
2733
}
34+
35+
public void testGetPrioritizedSubfields() {
36+
assertTrue(RuleAttribute.INDEX_PATTERN.getPrioritizedSubfields().isEmpty());
37+
}
38+
39+
public void testFromXContentParseAttributeValues_success() throws IOException {
40+
String json = "{ \"index_pattern\": [\"val1\", \"val2\"] }";
41+
try (XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, json)) {
42+
skipTokens(parser, 3);
43+
Set<String> result = RuleAttribute.INDEX_PATTERN.fromXContentParseAttributeValues(parser);
44+
assertTrue(result.contains("val1"));
45+
assertTrue(result.contains("val2"));
46+
}
47+
}
48+
49+
public void testFromXContentParseAttributeValues_notStartArray() throws IOException {
50+
String json = "{ \"index_pattern\": \"val1\" }";
51+
try (XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, json)) {
52+
skipTokens(parser, 3);
53+
assertThrows(XContentParseException.class, () -> RuleAttribute.INDEX_PATTERN.fromXContentParseAttributeValues(parser));
54+
}
55+
}
56+
57+
public void testFromXContentParseAttributeValues_unexpectedToken() throws IOException {
58+
String json = "{ \"index_pattern\": [123] }";
59+
try (XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, json)) {
60+
skipTokens(parser, 3);
61+
assertThrows(XContentParseException.class, () -> RuleAttribute.INDEX_PATTERN.fromXContentParseAttributeValues(parser));
62+
}
63+
}
64+
65+
private void skipTokens(XContentParser parser, int count) throws IOException {
66+
for (int i = 0; i < count; i++) {
67+
parser.nextToken();
68+
}
69+
}
2870
}

modules/autotagging-commons/common/src/test/java/org/opensearch/rule/autotagging/AutoTaggingRegistryTests.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import org.opensearch.test.OpenSearchTestCase;
1414
import org.junit.BeforeClass;
1515

16+
import java.util.HashMap;
17+
1618
import static org.opensearch.rule.autotagging.AutoTaggingRegistry.MAX_FEATURE_TYPE_NAME_LENGTH;
1719
import static org.opensearch.rule.autotagging.RuleTests.INVALID_FEATURE;
1820
import static org.opensearch.rule.utils.RuleTestUtils.FEATURE_TYPE_NAME;
@@ -38,10 +40,22 @@ public void testRuntimeException() {
3840

3941
public void testIllegalStateExceptionException() {
4042
assertThrows(IllegalStateException.class, () -> AutoTaggingRegistry.registerFeatureType(null));
43+
4144
FeatureType featureType = mock(FeatureType.class);
4245
when(featureType.getName()).thenReturn(FEATURE_TYPE_NAME);
46+
when(featureType.getOrderedAttributes()).thenReturn(null);
47+
when(featureType.getFeatureValueValidator()).thenReturn(new FeatureValueValidator() {
48+
@Override
49+
public void validate(String featureValue) {}
50+
});
4351
assertThrows(IllegalStateException.class, () -> AutoTaggingRegistry.registerFeatureType(featureType));
52+
4453
when(featureType.getName()).thenReturn(randomAlphaOfLength(MAX_FEATURE_TYPE_NAME_LENGTH + 1));
4554
assertThrows(IllegalStateException.class, () -> AutoTaggingRegistry.registerFeatureType(featureType));
55+
56+
when(featureType.getName()).thenReturn(FEATURE_TYPE_NAME);
57+
when(featureType.getOrderedAttributes()).thenReturn(new HashMap<>());
58+
when(featureType.getFeatureValueValidator()).thenReturn(null);
59+
assertThrows(IllegalStateException.class, () -> AutoTaggingRegistry.registerFeatureType(featureType));
4660
}
4761
}

modules/autotagging-commons/common/src/test/java/org/opensearch/rule/autotagging/RuleTests.java

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,6 @@ public static class TestFeatureType implements FeatureType {
7777
private static final String NAME = TEST_FEATURE_TYPE;
7878
private static final int MAX_ATTRIBUTE_VALUES = 10;
7979
private static final int MAX_ATTRIBUTE_VALUE_LENGTH = 100;
80-
private static final Map<String, Attribute> ALLOWED_ATTRIBUTES = Map.of(
81-
TEST_ATTR1_NAME,
82-
TEST_ATTRIBUTE_1,
83-
TEST_ATTR2_NAME,
84-
TEST_ATTRIBUTE_2
85-
);
8680

8781
public TestFeatureType() {}
8882

@@ -95,6 +89,11 @@ public String getName() {
9589
return NAME;
9690
}
9791

92+
@Override
93+
public Map<Attribute, Integer> getOrderedAttributes() {
94+
return Map.of(TEST_ATTRIBUTE_1, 1, TEST_ATTRIBUTE_2, 2);
95+
}
96+
9897
@Override
9998
public int getMaxNumberOfValuesPerAttribute() {
10099
return MAX_ATTRIBUTE_VALUES;
@@ -104,11 +103,6 @@ public int getMaxNumberOfValuesPerAttribute() {
104103
public int getMaxCharLengthPerAttributeValue() {
105104
return MAX_ATTRIBUTE_VALUE_LENGTH;
106105
}
107-
108-
@Override
109-
public Map<String, Attribute> getAllowedAttributesRegistry() {
110-
return ALLOWED_ATTRIBUTES;
111-
}
112106
}
113107

114108
static Rule buildRule(

modules/autotagging-commons/common/src/test/java/org/opensearch/rule/utils/RuleTestUtils.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ public static void assertEqualRule(Rule one, Rule two, boolean ruleUpdated) {
8888
public static class MockRuleFeatureType implements FeatureType {
8989

9090
public static final MockRuleFeatureType INSTANCE = new MockRuleFeatureType();
91+
private static final Map<Attribute, Integer> PRIORITIZED_ATTRIBUTES = Map.of(
92+
MockRuleAttributes.MOCK_RULE_ATTRIBUTE_ONE,
93+
1,
94+
MockRuleAttributes.MOCK_RULE_ATTRIBUTE_TWO,
95+
2
96+
);
9197

9298
private MockRuleFeatureType() {}
9399

@@ -101,13 +107,8 @@ public String getName() {
101107
}
102108

103109
@Override
104-
public Map<String, Attribute> getAllowedAttributesRegistry() {
105-
return Map.of(
106-
ATTRIBUTE_VALUE_ONE,
107-
MockRuleAttributes.MOCK_RULE_ATTRIBUTE_ONE,
108-
ATTRIBUTE_VALUE_TWO,
109-
MockRuleAttributes.MOCK_RULE_ATTRIBUTE_TWO
110-
);
110+
public Map<Attribute, Integer> getOrderedAttributes() {
111+
return PRIORITIZED_ATTRIBUTES;
111112
}
112113
}
113114

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.rule.spi;
10+
11+
import org.opensearch.rule.autotagging.Attribute;
12+
13+
/**
14+
* Represents a custom attribute extension for the rule framework.
15+
* Implementations provide a single attribute to be used in auto tagging.
16+
*/
17+
public interface AttributesExtension {
18+
/**
19+
* Returns the attribute provided by this extension.
20+
*/
21+
Attribute getAttribute();
22+
}

0 commit comments

Comments
 (0)